diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2026-03-05 10:56:50 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-05 10:56:50 +0000 |
| commit | fe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch) | |
| tree | af6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/backend/src/server/oauth/OAuth2ProviderService.ts | |
| parent | Merge pull request #16998 from misskey-dev/develop (diff) | |
| parent | Release: 2026.3.0 (diff) | |
| download | misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.gz misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.bz2 misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.zip | |
Merge pull request #17217 from misskey-dev/develop
Release: 2026.3.0
Diffstat (limited to 'packages/backend/src/server/oauth/OAuth2ProviderService.ts')
| -rw-r--r-- | packages/backend/src/server/oauth/OAuth2ProviderService.ts | 91 |
1 files changed, 69 insertions, 22 deletions
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index d2391c43ab..840c34b806 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -123,41 +123,86 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str return { name, logo }; } -// https://indieauth.spec.indieweb.org/#client-information-discovery -// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, -// and if there is an [h-app] with a url property matching the client_id URL, -// then it should use the name and icon and display them on the authorization prompt." -// (But we don't display any icon for now) -// https://indieauth.spec.indieweb.org/#redirect-url -// "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute -// of redirect_uri at the client_id URL. -// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST -// look for an exact match of the given redirect_uri in the request against the list of -// redirect_uris discovered after resolving any relative URLs." async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise<ClientInformation> { try { const res = await httpRequestService.send(id); + const redirectUris: string[] = []; + let name = id; + let logo: string | null = null; + // https://indieauth.spec.indieweb.org/#redirect-url + // "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute + // of redirect_uri at the client_id URL. + // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST + // look for an exact match of the given redirect_uri in the request against the list of + // redirect_uris discovered after resolving any relative URLs." const linkHeader = res.headers.get('link'); if (linkHeader) { redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri)); } - const text = await res.text(); - const doc = htmlParser.parse(`<div>${text}</div>`); + const contentType = res.headers.get('content-type'); + const mediaType = contentType ? contentType.split(';')[0].trim() : null; + if (mediaType === 'application/json') { + // Client discovery via JSON document (11 July 2024 spec) + // https://indieauth.spec.indieweb.org/#client-metadata + // "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing + // client metadata defined in [RFC7591], the minimum properties for an IndieAuth + // client defined below." - redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href)); + const json = await res.json() as { + client_id: string; + client_name?: string; + client_uri: string; + logo_uri?: string; + redirect_uris?: string[]; + }; - let name = id; - let logo: string | null = null; - if (text) { - const microformats = parseMicroformats(doc, res.url, id); - if (typeof microformats.name === 'string') { - name = microformats.name; + // https://indieauth.spec.indieweb.org/#client-metadata-li-1 + // "The authorization server MUST verify that the client_id in the document matches the + // client_id of the URL where the document was retrieved." + if (json.client_id !== id) { + throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request'); + } + + // https://indieauth.spec.indieweb.org/#client-metadata-li-1 + // "The client_uri MUST be a prefix of the client_id." + if (!json.client_uri || !id.startsWith(json.client_uri)) { + throw new AuthorizationError('client_uri is not a prefix of client_id', 'invalid_request'); + } + + if (typeof json.client_name === 'string') { + name = json.client_name; } - if (typeof microformats.logo === 'string') { - logo = microformats.logo; + + if (typeof json.logo_uri === 'string') { + // Since uri can be relative, resolve it against the document URL + logo = new URL(json.logo_uri, res.url).toString(); + } + + if (Array.isArray(json.redirect_uris)) { + redirectUris.push(...json.redirect_uris.filter((uri): uri is string => typeof uri === 'string')); + } + } else { + // Client discovery via HTML microformats (12 February 2022 spec) + // https://indieauth.spec.indieweb.org/20220212/#client-information-discovery + // "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, + // and if there is an [h-app] with a url property matching the client_id URL, + // then it should use the name and icon and display them on the authorization prompt." + const text = await res.text(); + const doc = htmlParser.parse(`<div>${text}</div>`); + + redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href)); + + if (text) { + const microformats = parseMicroformats(doc, res.url, id); + if (typeof microformats.name === 'string') { + name = microformats.name; + } + if (typeof microformats.logo === 'string') { + logo = microformats.logo; + } } } @@ -172,6 +217,8 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt logger.error('Error while fetching client information', { err }); if (err instanceof StatusError) { throw new AuthorizationError('Failed to fetch client information', 'invalid_request'); + } else if (err instanceof AuthorizationError) { + throw err; } else { throw new AuthorizationError('Failed to parse client information', 'server_error'); } |