fix(backend/oauth2): Token Grantエンドポイントのバリデーションを修正 (#17580)

This commit is contained in:
かっこかり 2026-06-18 18:40:37 +09:00 committed by GitHub
commit d7c11a61c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 92 additions and 16 deletions

View file

@ -273,8 +273,9 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
}
}
function firstValue(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
function firstValue(value: unknown | unknown[] | undefined): string | undefined {
const firstElement = Array.isArray(value) ? value[0] : value;
return typeof firstElement === 'string' ? firstElement : undefined;
}
function normalizeScope(scope: string | string[] | undefined): string[] {
@ -282,12 +283,39 @@ function normalizeScope(scope: string | string[] | undefined): string[] {
return raw.flatMap(value => value.split(/\s+/)).filter(Boolean);
}
function parseUrlEncodedParameters(rawBody: string): OAuthRequestParameters {
const parsed: OAuthRequestParameters = {};
for (const [key, value] of new URLSearchParams(rawBody).entries()) {
const current = parsed[key];
if (current == null) {
parsed[key] = value;
} else if (Array.isArray(current)) {
current.push(value);
} else {
parsed[key] = [current, value];
}
}
return parsed;
}
function toRequestParameters(body: unknown): OAuthRequestParameters {
if (typeof body === 'string') {
return parseUrlEncodedParameters(body);
}
if (body instanceof URLSearchParams) {
return parseUrlEncodedParameters(body.toString());
}
if (body == null || typeof body !== 'object' || Array.isArray(body)) {
return {};
}
return body as OAuthRequestParameters;
return Object.fromEntries(Object.entries(body).filter(([_, value]) => (
typeof value === 'string' ||
(Array.isArray(value) && value.every(v => typeof v === 'string'))
)));
}
function applyNoStore(reply: FastifyReply): void {
@ -360,19 +388,7 @@ function registerFormBodyParser(fastify: FastifyInstance): void {
fastify.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, (_request, body, done) => {
try {
const parsed: OAuthRequestParameters = {};
for (const [key, value] of new URLSearchParams(typeof body === 'string' ? body : body.toString('utf8')).entries()) {
const current = parsed[key];
if (current == null) {
parsed[key] = value;
} else if (Array.isArray(current)) {
current.push(value);
} else {
parsed[key] = [current, value];
}
}
done(null, parsed);
done(null, parseUrlEncodedParameters(typeof body === 'string' ? body : body.toString('utf8')));
} catch (error) {
done(error as Error, undefined);
}

View file

@ -809,6 +809,66 @@ describe('OAuth', () => {
});
});
describe('Token endpoint', () => {
test('Accept JSON payload', async () => {
const { code_challenge, code_verifier } = await pkceChallenge(128);
const { code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
const response = await fetch(new URL('/oauth/token', host), {
method: 'post',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: clientConfig.client.id,
redirect_uri,
code_verifier,
}),
});
assert.strictEqual(response.status, 200);
const tokenResponse = await response.json() as {
access_token: string;
token_type: string;
scope: string;
};
assert.strictEqual(typeof tokenResponse.access_token, 'string');
assert.strictEqual(tokenResponse.token_type, 'Bearer');
assert.strictEqual(tokenResponse.scope, 'write:notes');
});
test('Accept x-www-form-urlencoded payload', async () => {
const { code_challenge, code_verifier } = await pkceChallenge(128);
const { code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
const response = await fetch(new URL('/oauth/token', host), {
method: 'post',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: clientConfig.client.id,
redirect_uri,
code_verifier,
}),
});
assert.strictEqual(response.status, 200);
const tokenResponse = await response.json() as {
access_token: string;
token_type: string;
scope: string;
};
assert.strictEqual(typeof tokenResponse.access_token, 'string');
assert.strictEqual(tokenResponse.token_type, 'Bearer');
assert.strictEqual(tokenResponse.scope, 'write:notes');
});
});
describe('Client Information Discovery', () => {
// https://indieauth.spec.indieweb.org/#client-information-discovery
describe('JSON client metadata (11 July 2024)', () => {