diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-12-31 14:50:01 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-31 14:50:01 +0900 |
| commit | 01aa56c60201aed3e278193a0aee7c13b1f0aef7 (patch) | |
| tree | 85b75feb469be79f743a36efb47076156d2dac50 /packages/backend/test/e2e | |
| parent | refactor(frontend): remove undefined css rules (#17051) (diff) | |
| download | misskey-01aa56c60201aed3e278193a0aee7c13b1f0aef7.tar.gz misskey-01aa56c60201aed3e278193a0aee7c13b1f0aef7.tar.bz2 misskey-01aa56c60201aed3e278193a0aee7c13b1f0aef7.zip | |
enhance(backend/oauth): Support client information discovery in the IndieAuth 11 July 2024 spec (#17030)
* enhance(backend): Support client information discovery in the IndieAuth 11 July 2024 spec
* add tests
* Update Changelog
* Update Changelog
* fix tests
* fix test describe to align with the other describe format
Diffstat (limited to 'packages/backend/test/e2e')
| -rw-r--r-- | packages/backend/test/e2e/oauth.ts | 422 |
1 files changed, 276 insertions, 146 deletions
diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 96a6311a5a..67a9026eb5 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -28,6 +28,7 @@ const host = `http://127.0.0.1:${port}`; const clientPort = port + 1; const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`; +const redirect_uri2 = `http://127.0.0.1:${clientPort}/redirect2`; const basicAuthParams: AuthorizationParamsExtended = { redirect_uri, @@ -807,45 +808,193 @@ describe('OAuth', () => { }); }); - // https://indieauth.spec.indieweb.org/#client-information-discovery describe('Client Information Discovery', () => { - describe('Redirection', () => { - const tests: Record<string, (reply: FastifyReply) => void> = { - 'Read HTTP header': reply => { - reply.header('Link', '</redirect>; rel="redirect_uri"'); - reply.send(` - <!DOCTYPE html> - <div class="h-app"><a href="/" class="u-url p-name">Misklient - `); - }, - 'Mixed links': reply => { - reply.header('Link', '</redirect>; rel="redirect_uri"'); - reply.send(` - <!DOCTYPE html> - <link rel="redirect_uri" href="/redirect2" /> - <div class="h-app"><a href="/" class="u-url p-name">Misklient - `); - }, - 'Multiple items in Link header': reply => { - reply.header('Link', '</redirect2>; rel="redirect_uri",</redirect>; rel="redirect_uri"'); - reply.send(` - <!DOCTYPE html> - <div class="h-app"><a href="/" class="u-url p-name">Misklient - `); - }, - 'Multiple items in HTML': reply => { - reply.send(` - <!DOCTYPE html> - <link rel="redirect_uri" href="/redirect2" /> - <link rel="redirect_uri" href="/redirect" /> - <div class="h-app"><a href="/" class="u-url p-name">Misklient - `); - }, - }; + // https://indieauth.spec.indieweb.org/#client-information-discovery + describe('JSON client metadata (11 July 2024)', () => { + test('Read JSON document', async () => { + sender = (reply): void => { + reply.header('content-type', 'application/json'); + reply.send({ + client_id: `http://127.0.0.1:${clientPort}/`, + client_uri: `http://127.0.0.1:${clientPort}/`, + client_name: 'Misklient JSON', + logo_uri: '/logo.png', + redirect_uris: ['/redirect'], + }); + }; - for (const [title, replyFunc] of Object.entries(tests)) { - test(title, async () => { - sender = replyFunc; + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + const meta = getMeta(await response.text()); + assert.strictEqual(meta.clientName, 'Misklient JSON'); + assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`); + }); + + test('Merge Link header redirect_uri with JSON redirect_uris', async () => { + sender = (reply): void => { + reply.header('Link', '</redirect2>; rel="redirect_uri"'); + reply.header('content-type', 'application/json'); + reply.send({ + client_id: `http://127.0.0.1:${clientPort}/`, + client_uri: `http://127.0.0.1:${clientPort}/`, + client_name: 'Misklient JSON', + redirect_uris: ['/redirect'], + }); + }; + + const client = new AuthorizationCode(clientConfig); + + const ok1 = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(ok1.status, 200); + + const ok2 = await fetch(client.authorizeURL({ + redirect_uri: redirect_uri2, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(ok2.status, 200); + }); + + test('Reject when client_id does not match retrieved URL', async () => { + sender = (reply): void => { + reply.header('content-type', 'application/json'); + reply.send({ + client_id: `http://127.0.0.1:${clientPort}/mismatch`, + client_uri: `http://127.0.0.1:${clientPort}/`, + redirect_uris: ['/redirect'], + }); + }; + + const client = new AuthorizationCode(clientConfig); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + + test('Reject when client_uri is not a prefix of client_id', async () => { + sender = (reply): void => { + reply.header('content-type', 'application/json'); + reply.send({ + client_id: `http://127.0.0.1:${clientPort}/`, + client_uri: `http://127.0.0.1:${clientPort}/no-prefix/`, + redirect_uris: ['/redirect'], + }); + }; + + const client = new AuthorizationCode(clientConfig); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + + test('Reject when JSON metadata has no redirect_uris and no Link header', async () => { + sender = (reply): void => { + reply.header('content-type', 'application/json'); + reply.send({ + client_id: `http://127.0.0.1:${clientPort}/`, + client_uri: `http://127.0.0.1:${clientPort}/`, + client_name: 'Misklient JSON', + }); + }; + + const client = new AuthorizationCode(clientConfig); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + }); + + // https://indieauth.spec.indieweb.org/20220212/#client-information-discovery + describe('HTML link client metadata (12 Feb 2022)', () => { + describe('Redirection', () => { + const tests: Record<string, (reply: FastifyReply) => void> = { + 'Read HTTP header': reply => { + reply.header('Link', '</redirect>; rel="redirect_uri"'); + reply.send(` + <!DOCTYPE html> + <div class="h-app"><a href="/" class="u-url p-name">Misklient + `); + }, + 'Mixed links': reply => { + reply.header('Link', '</redirect>; rel="redirect_uri"'); + reply.send(` + <!DOCTYPE html> + <link rel="redirect_uri" href="/redirect2" /> + <div class="h-app"><a href="/" class="u-url p-name">Misklient + `); + }, + 'Multiple items in Link header': reply => { + reply.header('Link', '</redirect2>; rel="redirect_uri",</redirect>; rel="redirect_uri"'); + reply.send(` + <!DOCTYPE html> + <div class="h-app"><a href="/" class="u-url p-name">Misklient + `); + }, + 'Multiple items in HTML': reply => { + reply.send(` + <!DOCTYPE html> + <link rel="redirect_uri" href="/redirect2" /> + <link rel="redirect_uri" href="/redirect" /> + <div class="h-app"><a href="/" class="u-url p-name">Misklient + `); + }, + }; + + for (const [title, replyFunc] of Object.entries(tests)) { + test(title, async () => { + sender = replyFunc; + + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + }); + } + + test('No item', async () => { + sender = (reply): void => { + reply.send(` + <!DOCTYPE html> + <div class="h-app"><a href="/" class="u-url p-name">Misklient + `); + }; const client = new AuthorizationCode(clientConfig); @@ -856,20 +1005,17 @@ describe('OAuth', () => { code_challenge: 'code', code_challenge_method: 'S256', } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); + + // direct error because there's no redirect URI to ping + await assertDirectError(response, 400, 'invalid_request'); }); - } + }); - test('No item', async () => { - sender = (reply): void => { - reply.send(` - <!DOCTYPE html> - <div class="h-app"><a href="/" class="u-url p-name">Misklient - `); - }; - const client = new AuthorizationCode(clientConfig); + test('Disallow loopback', async () => { + await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' }); + const client = new AuthorizationCode(clientConfig); const response = await fetch(client.authorizeURL({ redirect_uri, scope: 'write:notes', @@ -877,119 +1023,103 @@ describe('OAuth', () => { code_challenge: 'code', code_challenge_method: 'S256', } as AuthorizationParamsExtended)); - - // direct error because there's no redirect URI to ping await assertDirectError(response, 400, 'invalid_request'); }); - }); - test('Disallow loopback', async () => { - await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' }); - - const client = new AuthorizationCode(clientConfig); - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - await assertDirectError(response, 400, 'invalid_request'); - }); - - test('Missing name', async () => { - sender = (reply): void => { - reply.header('Link', '</redirect>; rel="redirect_uri"'); - reply.send(); - }; + test('Missing name', async () => { + sender = (reply): void => { + reply.header('Link', '</redirect>; rel="redirect_uri"'); + reply.send(); + }; - const client = new AuthorizationCode(clientConfig); + const client = new AuthorizationCode(clientConfig); - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); - }); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); + }); - test('With Logo', async () => { - sender = (reply): void => { - reply.header('Link', '</redirect>; rel="redirect_uri"'); - reply.send(` - <!DOCTYPE html> - <div class="h-app"> - <a href="/" class="u-url p-name">Misklient</a> - <img src="/logo.png" class="u-logo" /> - </div> - `); - reply.send(); - }; + test('With Logo', async () => { + sender = (reply): void => { + reply.header('Link', '</redirect>; rel="redirect_uri"'); + reply.send(` + <!DOCTYPE html> + <div class="h-app"> + <a href="/" class="u-url p-name">Misklient</a> + <img src="/logo.png" class="u-logo" /> + </div> + `); + reply.send(); + }; - const client = new AuthorizationCode(clientConfig); + const client = new AuthorizationCode(clientConfig); - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - const meta = getMeta(await response.text()); - assert.strictEqual(meta.clientName, 'Misklient'); - assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`); - }); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + const meta = getMeta(await response.text()); + assert.strictEqual(meta.clientName, 'Misklient'); + assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`); + }); - test('Missing Logo', async () => { - sender = (reply): void => { - reply.header('Link', '</redirect>; rel="redirect_uri"'); - reply.send(` - <!DOCTYPE html> - <div class="h-app"><a href="/" class="u-url p-name">Misklient - `); - reply.send(); - }; + test('Missing Logo', async () => { + sender = (reply): void => { + reply.header('Link', '</redirect>; rel="redirect_uri"'); + reply.send(` + <!DOCTYPE html> + <div class="h-app"><a href="/" class="u-url p-name">Misklient + `); + reply.send(); + }; - const client = new AuthorizationCode(clientConfig); + const client = new AuthorizationCode(clientConfig); - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - const meta = getMeta(await response.text()); - assert.strictEqual(meta.clientName, 'Misklient'); - assert.strictEqual(meta.clientLogo, undefined); - }); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + const meta = getMeta(await response.text()); + assert.strictEqual(meta.clientName, 'Misklient'); + assert.strictEqual(meta.clientLogo, undefined); + }); - test('Mismatching URL in h-app', async () => { - sender = (reply): void => { - reply.header('Link', '</redirect>; rel="redirect_uri"'); - reply.send(` - <!DOCTYPE html> - <div class="h-app"><a href="/foo" class="u-url p-name">Misklient - `); - reply.send(); - }; + test('Mismatching URL in h-app', async () => { + sender = (reply): void => { + reply.header('Link', '</redirect>; rel="redirect_uri"'); + reply.send(` + <!DOCTYPE html> + <div class="h-app"><a href="/foo" class="u-url p-name">Misklient + `); + reply.send(); + }; - const client = new AuthorizationCode(clientConfig); + const client = new AuthorizationCode(clientConfig); - const response = await fetch(client.authorizeURL({ - redirect_uri, - scope: 'write:notes', - state: 'state', - code_challenge: 'code', - code_challenge_method: 'S256', - } as AuthorizationParamsExtended)); - assert.strictEqual(response.status, 200); - assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); + }); }); }); |