grant type tests

This commit is contained in:
Kagami Sascha Rosylight 2023-06-17 00:22:19 +02:00
parent b57d40ed09
commit 628377187a
2 changed files with 90 additions and 14 deletions

View file

@ -18,11 +18,11 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { AccessTokensRepository, UsersRepository } from '@/models/index.js'; import type { AccessTokensRepository, UsersRepository } from '@/models/index.js';
import type { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
import { MemoryKVCache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import type { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import type { ServerResponse } from 'node:http'; import type { ServerResponse } from 'node:http';
import type { FastifyInstance } from 'fastify'; import type { FastifyInstance } from 'fastify';
@ -376,9 +376,9 @@ export class OAuth2ProviderService {
} }
areq.scope = scopes; areq.scope = scopes;
if (type !== 'code') { // Require PKCE parameters.
throw new AuthorizationError('`response_type` parameter must be set as "code"', 'invalid_request'); // Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
} // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
if (typeof codeChallenge !== 'string') { if (typeof codeChallenge !== 'string') {
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request'); throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
} }

View file

@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { AuthorizationCode, type AuthorizationTokenConfig } from 'simple-oauth2'; import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials } from 'simple-oauth2';
import pkceChallenge from 'pkce-challenge'; import pkceChallenge from 'pkce-challenge';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
@ -375,7 +375,10 @@ describe('OAuth', () => {
code, code,
redirect_uri, redirect_uri,
code_verifier: wrong_verifier, code_verifier: wrong_verifier,
} as AuthorizationTokenConfigExtended)); } as AuthorizationTokenConfigExtended), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
}); });
} }
}); });
@ -400,20 +403,29 @@ describe('OAuth', () => {
code, code,
redirect_uri, redirect_uri,
code_verifier, code_verifier,
} as AuthorizationTokenConfigExtended)); } as AuthorizationTokenConfigExtended), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
}); });
test('On failure', async () => { test('On failure', async () => {
const { code_challenge, code_verifier } = await pkceChallenge(128); const { code_challenge, code_verifier } = await pkceChallenge(128);
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
await assert.rejects(client.getToken({ code, redirect_uri })); await assert.rejects(client.getToken({ code, redirect_uri }), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
await assert.rejects(client.getToken({ await assert.rejects(client.getToken({
code, code,
redirect_uri, redirect_uri,
code_verifier, code_verifier,
} as AuthorizationTokenConfigExtended)); } as AuthorizationTokenConfigExtended), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
}); });
}); });
@ -660,7 +672,10 @@ describe('OAuth', () => {
code, code,
redirect_uri: 'http://127.0.0.2/', redirect_uri: 'http://127.0.0.2/',
code_verifier, code_verifier,
} as AuthorizationTokenConfigExtended)); } as AuthorizationTokenConfigExtended), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
}); });
test('Invalid redirect_uri including the valid one at token endpoint', async () => { test('Invalid redirect_uri including the valid one at token endpoint', async () => {
@ -672,7 +687,10 @@ describe('OAuth', () => {
code, code,
redirect_uri: 'http://127.0.0.1/redirection', redirect_uri: 'http://127.0.0.1/redirection',
code_verifier, code_verifier,
} as AuthorizationTokenConfigExtended)); } as AuthorizationTokenConfigExtended), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
}); });
test('No redirect_uri at token endpoint', async () => { test('No redirect_uri at token endpoint', async () => {
@ -683,7 +701,10 @@ describe('OAuth', () => {
await assert.rejects(client.getToken({ await assert.rejects(client.getToken({
code, code,
code_verifier, code_verifier,
} as AuthorizationTokenConfigExtended)); } as AuthorizationTokenConfigExtended), (err: any) => {
assert.strictEqual(err.data.payload.error, 'invalid_grant');
return true;
});
}); });
}); });
@ -752,6 +773,61 @@ describe('OAuth', () => {
}); });
}); });
// Only authorization code grant is supported
describe('Grant type', () => {
test('Implicit grant is not supported', async () => {
const url = new URL('/oauth/authorize', host);
url.searchParams.append('response_type', 'token');
const response = await fetch(url);
assertDirectError(response, 501, 'unsupported_response_type');
});
test('Resource owner grant is not supported', async () => {
const client = new ResourceOwnerPassword({
client: {
id: `http://127.0.0.1:${clientPort}/`,
secret: '',
},
auth: {
tokenHost: host,
tokenPath: '/oauth/token',
},
options: {
authorizationMethod: 'body',
},
});
await assert.rejects(client.getToken({
username: 'alice',
password: 'test',
}), (err: any) => {
assert.strictEqual(err.data.payload.error, 'unsupported_grant_type');
return true;
});
});
test('Client credential grant is not supported', async () => {
const client = new ClientCredentials({
client: {
id: `http://127.0.0.1:${clientPort}/`,
secret: '',
},
auth: {
tokenHost: host,
tokenPath: '/oauth/token',
},
options: {
authorizationMethod: 'body',
},
});
await assert.rejects(client.getToken({}), (err: any) => {
assert.strictEqual(err.data.payload.error, 'unsupported_grant_type');
return true;
});
});
});
// https://indieauth.spec.indieweb.org/#client-information-discovery // https://indieauth.spec.indieweb.org/#client-information-discovery
describe('Client Information Discovery', () => { describe('Client Information Discovery', () => {
describe('Redirection', () => { describe('Redirection', () => {