diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index b0fdc558e6..677a85473f 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -4,7 +4,8 @@ import fastifyMiddie, { IncomingMessageExtended } from '@fastify/middie'; import { JSDOM } from 'jsdom'; import parseLinkHeader from 'parse-link-header'; import ipaddr from 'ipaddr.js'; -import oauth2orize, { OAuth2 } from 'oauth2orize'; +import oauth2orize, { type OAuth2 } from 'oauth2orize'; +import * as oauth2Query from 'oauth2orize/lib/response/query.js'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -339,8 +340,17 @@ export class OAuth2ProviderService { scopes: string[], }> = {}; + const query = (txn, res, params) => { + // RFC 9207 + // TODO: Oh no, perhaps returning to oidc-provider is better. Hacks everywhere here. + params.iss = config.url; + oauth2Query.default(txn, res, params); + }; + this.#server.grant(oauth2Pkce.extensions()); - this.#server.grant(oauth2orize.grant.code((client, redirectUri, token, ares, areq, done) => { + this.#server.grant(oauth2orize.grant.code({ + modes: { query } + }, (client, redirectUri, token, ares, areq, done) => { (async (): Promise>> => { console.log('HIT grant code:', client, redirectUri, token, ares, areq); const code = secureRndstr(32, true); @@ -483,7 +493,7 @@ export class OAuth2ProviderService { // https://indieauth.spec.indieweb.org/#authorization-request // Allow same-origin redirection if (redirectUrl.protocol !== clientUrl.protocol || redirectUrl.host !== clientUrl.host) { - // TODO: allow more redirect_uri by Client Information Discovery + // TODO: allow only explicit redirect_uri by Client Information Discovery throw new Error('cross-origin redirect_uri is not supported yet.'); } diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 66c3a970bc..5dd0d7c39f 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -6,7 +6,6 @@ import type { INestApplicationContext } from '@nestjs/common'; import { AuthorizationCode } from 'simple-oauth2'; import pkceChallenge from 'pkce-challenge'; import { JSDOM } from 'jsdom'; -import { api } from '../utils.js'; const clientPort = port + 1; const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`; @@ -97,6 +96,7 @@ describe('OAuth', () => { assert.strictEqual(location.origin + location.pathname, redirect_uri); assert.ok(location.searchParams.has('code')); assert.strictEqual(location.searchParams.get('state'), 'state'); + assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); // RFC 9207 const token = await client.getToken({ code: location.searchParams.get('code')!, @@ -380,6 +380,19 @@ describe('OAuth', () => { assert.strictEqual(response.status, 500); }); + test('No redirect_uri at authorization endpoint', async () => { + const client = getClient(); + + const response = await fetch(client.authorizeURL({ + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + })); + // TODO: status code + assert.strictEqual(response.status, 500); + }); + test('Invalid redirect_uri at token endpoint', async () => { const { code_challenge, code_verifier } = pkceChallenge.default(128); @@ -407,6 +420,32 @@ describe('OAuth', () => { })); }); + test('No redirect_uri at token endpoint', async () => { + const { code_challenge, code_verifier } = pkceChallenge.default(128); + + const client = getClient(); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge, + code_challenge_method: 'S256', + })); + assert.strictEqual(response.status, 200); + + const decisionResponse = await fetchDecisionFromResponse(response, alice); + assert.strictEqual(decisionResponse.status, 302); + + const location = new URL(decisionResponse.headers.get('location')!); + assert.ok(location.searchParams.has('code')); + + await assert.rejects(client.getToken({ + code: location.searchParams.get('code')!, + code_verifier, + })); + }); + // TODO: disallow random same-origin URLs with strict redirect_uris with client information discovery }); @@ -415,4 +454,6 @@ describe('OAuth', () => { // TODO: authorizing two users concurrently // TODO: Error format required by OAuth spec + + // TODO: Client Information Discovery });