From 22228b6756ff534a94930774d9b6744dfb5961fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:05:14 +0900 Subject: [PATCH] =?UTF-8?q?enhance:=20OAuth2=20(IndieAuth)=20=E3=81=A7?= =?UTF-8?q?=E3=83=AD=E3=82=B4=E3=81=8C=E6=8F=90=E4=BE=9B=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=82=8B=E5=A0=B4=E5=90=88=E3=81=AF=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#1557?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance: OAuthでロゴが提供されている場合は表示するように * Update Changelog * refactor * fix * fix test --- CHANGELOG.md | 2 + .../src/server/oauth/OAuth2ProviderService.ts | 17 +++++- .../backend/src/server/web/views/oauth.pug | 2 + packages/backend/test/e2e/oauth.ts | 56 ++++++++++++++++++- packages/frontend/src/pages/oauth.vue | 2 + 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c589376..197de5aec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### General - Enhance: プロキシアカウントをシステムアカウントとして作成するように +- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように + 書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。 - Fix: システムアカウントが削除できる問題を修正 ### Client diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index e065c451f..cdd710266 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -95,6 +95,7 @@ interface ClientInformation { id: string; redirectUris: string[]; name: string; + logo: string | null; } // https://indieauth.spec.indieweb.org/#client-information-discovery @@ -124,11 +125,19 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt redirectUris.push(...[...fragment.querySelectorAll<HTMLLinkElement>('link[rel=redirect_uri][href]')].map(el => el.href)); let name = id; + let logo: string | null = null; if (text) { const microformats = mf2(text, { baseUrl: res.url }); - const nameProperty = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id))?.properties.name[0]; - if (typeof nameProperty === 'string') { - name = nameProperty; + const correspondingProperties = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id)); + if (correspondingProperties) { + const nameProperty = correspondingProperties.properties.name?.[0]; + if (typeof nameProperty === 'string') { + name = nameProperty; + } + const logoProperty = correspondingProperties.properties.logo?.[0]; + if (typeof logoProperty === 'string') { + logo = logoProperty; + } } } @@ -136,6 +145,7 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt id, redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()), name: typeof name === 'string' ? name : id, + logo, }; } catch (err) { console.error(err); @@ -379,6 +389,7 @@ export class OAuth2ProviderService { return await reply.view('oauth', { transactionId: oauth2.transactionID, clientName: oauth2.client.name, + clientLogo: oauth2.client.logo, scope: oauth2.req.scope.join(' '), }); }); diff --git a/packages/backend/src/server/web/views/oauth.pug b/packages/backend/src/server/web/views/oauth.pug index 1470dbfbd..4195ccc3a 100644 --- a/packages/backend/src/server/web/views/oauth.pug +++ b/packages/backend/src/server/web/views/oauth.pug @@ -6,4 +6,6 @@ block meta //- XXX: Remove navigation bar in auth page? meta(name='misskey:oauth:transaction-id' content=transactionId) meta(name='misskey:oauth:client-name' content=clientName) + if clientLogo + meta(name='misskey:oauth:client-logo' content=clientLogo) meta(name='misskey:oauth:scope' content=scope) diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index ef7a6a579..f639f90ea 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -72,11 +72,12 @@ const clientConfig: ModuleOptions<'client_id'> = { }, }; -function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } { +function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } { const fragment = JSDOM.fragment(html); return { transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content, clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content, + clientLogo: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content, }; } @@ -915,6 +916,59 @@ describe('OAuth', () => { 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(); + }; + + 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`); + }); + + 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 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"'); diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue index 8719a769e..860c884d1 100644 --- a/packages/frontend/src/pages/oauth.vue +++ b/packages/frontend/src/pages/oauth.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAuthConfirm ref="authRoot" :name="name" + :icon="logo" :permissions="permissions" :waitOnDeny="true" @accept="onAccept" @@ -33,6 +34,7 @@ if (transactionIdMeta) { } const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content; +const logo = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content; const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? []; function doPost(token: string, decision: 'accept' | 'deny') {