merge authorization validation logic
This commit is contained in:
parent
8e7fc1ed98
commit
94ea15d2d7
|
@ -261,15 +261,14 @@ type OmitFirstElement<T extends unknown[]> = T extends [unknown, ...(infer R)]
|
||||||
? R
|
? R
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
interface OAuthRequestQuery {
|
interface OAuthRequest {
|
||||||
response_type: string;
|
type: string;
|
||||||
client_id: string;
|
clientID: string;
|
||||||
redirect_uri: string;
|
redirectURI: string;
|
||||||
state: string;
|
state: string;
|
||||||
code_challenge: string;
|
codeChallenge: string;
|
||||||
code_challenge_method: string;
|
codeChallengeMethod: string;
|
||||||
scope?: string;
|
scope: string[];
|
||||||
me?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -431,37 +430,15 @@ export class OAuth2ProviderService {
|
||||||
|
|
||||||
// For now only allow the basic OAuth endpoints, to start small and evaluate
|
// For now only allow the basic OAuth endpoints, to start small and evaluate
|
||||||
// this feature for some time, given that this is security related.
|
// this feature for some time, given that this is security related.
|
||||||
fastify.get<{ Querystring: OAuthRequestQuery }>('/oauth/authorize', async (request, reply) => {
|
fastify.get('/oauth/authorize', async (request, reply) => {
|
||||||
console.log('HIT /oauth/authorize', request.query);
|
|
||||||
const oauth2 = (request.raw as any).oauth2 as OAuth2;
|
const oauth2 = (request.raw as any).oauth2 as OAuth2;
|
||||||
console.log(oauth2, request.raw.session);
|
console.log('HIT /oauth/authorize', request.query, oauth2, request.raw.session);
|
||||||
|
|
||||||
const scopes = [...new Set(oauth2.req.scope)].filter(s => kinds.includes(s));
|
|
||||||
try {
|
|
||||||
if (!scopes.length) {
|
|
||||||
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
|
|
||||||
}
|
|
||||||
oauth2.req.scope = scopes;
|
|
||||||
|
|
||||||
if (request.query.response_type !== 'code') {
|
|
||||||
throw new AuthorizationError('`response_type` parameter must be set as "code"', 'invalid_request');
|
|
||||||
}
|
|
||||||
if (typeof request.query.code_challenge !== 'string') {
|
|
||||||
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
|
|
||||||
}
|
|
||||||
if (request.query.code_challenge_method !== 'S256') {
|
|
||||||
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
this.#server.errorHandler()(err, request.raw, reply.raw, null as any);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.header('Cache-Control', 'no-store');
|
reply.header('Cache-Control', 'no-store');
|
||||||
return await reply.view('oauth', {
|
return await reply.view('oauth', {
|
||||||
transactionId: oauth2.transactionID,
|
transactionId: oauth2.transactionID,
|
||||||
clientName: oauth2.client.name,
|
clientName: oauth2.client.name,
|
||||||
scope: scopes.join(' '),
|
scope: (oauth2.req.scope as any as string[]).join(' '),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
fastify.post('/oauth/decision', async () => { });
|
fastify.post('/oauth/decision', async () => { });
|
||||||
|
@ -479,12 +456,31 @@ export class OAuth2ProviderService {
|
||||||
});
|
});
|
||||||
|
|
||||||
await fastify.register(fastifyExpress);
|
await fastify.register(fastifyExpress);
|
||||||
|
// TODO: use redis session store to prevent memory leak
|
||||||
fastify.use(expressSession({ secret: 'keyboard cat', resave: false, saveUninitialized: false }) as any);
|
fastify.use(expressSession({ secret: 'keyboard cat', resave: false, saveUninitialized: false }) as any);
|
||||||
fastify.use('/oauth/authorize', this.#server.authorization((clientId, redirectUri, done) => {
|
fastify.use('/oauth/authorize', this.#server.authorization((areq: OAuthRequest, done: (err: Error | null, client?: any, redirectURI?: string) => void) => {
|
||||||
(async (): Promise<OmitFirstElement<Parameters<typeof done>>> => {
|
(async (): Promise<OmitFirstElement<Parameters<typeof done>>> => {
|
||||||
console.log('HIT /oauth/authorize validation middleware');
|
console.log('HIT /oauth/authorize validation middleware', areq);
|
||||||
|
|
||||||
const clientUrl = validateClientId(clientId);
|
const { codeChallenge, codeChallengeMethod, clientID, redirectURI, scope, type } = areq;
|
||||||
|
|
||||||
|
const scopes = [...new Set(scope)].filter(s => kinds.includes(s));
|
||||||
|
if (!scopes.length) {
|
||||||
|
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
|
||||||
|
}
|
||||||
|
areq.scope = scopes;
|
||||||
|
|
||||||
|
if (type !== 'code') {
|
||||||
|
throw new AuthorizationError('`response_type` parameter must be set as "code"', 'invalid_request');
|
||||||
|
}
|
||||||
|
if (typeof codeChallenge !== 'string') {
|
||||||
|
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
|
||||||
|
}
|
||||||
|
if (codeChallengeMethod !== 'S256') {
|
||||||
|
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientUrl = validateClientId(clientID);
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_DISALLOW_LOOPBACK === '1') {
|
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_DISALLOW_LOOPBACK === '1') {
|
||||||
const lookup = await dns.lookup(clientUrl.hostname);
|
const lookup = await dns.lookup(clientUrl.hostname);
|
||||||
|
@ -495,14 +491,17 @@ export class OAuth2ProviderService {
|
||||||
|
|
||||||
// Find client information from the remote.
|
// Find client information from the remote.
|
||||||
const clientInfo = await discoverClientInformation(this.httpRequestService, clientUrl.href);
|
const clientInfo = await discoverClientInformation(this.httpRequestService, clientUrl.href);
|
||||||
if (!clientInfo.redirectUris.includes(redirectUri)) {
|
if (!clientInfo.redirectUris.includes(redirectURI)) {
|
||||||
throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
|
throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
|
||||||
}
|
}
|
||||||
|
|
||||||
return [clientInfo, redirectUri];
|
return [clientInfo, redirectURI];
|
||||||
})().then(args => done(null, ...args), err => done(err));
|
})().then(args => done(null, ...args), err => done(err));
|
||||||
}));
|
}));
|
||||||
fastify.use('/oauth/authorize', this.#server.errorHandler()); // TODO: use mode: indirect?
|
// TODO: use mode: indirect
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2.1
|
||||||
|
// But make sure not to redirect to an invalid redirect_uri
|
||||||
|
fastify.use('/oauth/authorize', this.#server.errorHandler());
|
||||||
// for (const middleware of this.#server.decision()) {
|
// for (const middleware of this.#server.decision()) {
|
||||||
|
|
||||||
fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false }));
|
fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false }));
|
||||||
|
|
|
@ -871,4 +871,6 @@ describe('OAuth', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Invalid decision endpoint parameters
|
// TODO: Invalid decision endpoint parameters
|
||||||
|
|
||||||
|
// TODO: Unknown OAuth endpoint
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue