From 01aa56c60201aed3e278193a0aee7c13b1f0aef7 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: Wed, 31 Dec 2025 14:50:01 +0900
Subject: [PATCH] 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
---
CHANGELOG.md | 4 +-
.../src/server/oauth/OAuth2ProviderService.ts | 91 +++-
packages/backend/test/e2e/oauth.ts | 434 ++++++++++++------
3 files changed, 353 insertions(+), 176 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6b374360c..db60e06b99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,9 @@
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
### Server
--
+- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
+ - JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
+ - 従来の実装(12 February 2022版・HTML Microformat形式)も引き続きサポートされます
## 2025.12.2
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index d2391c43ab..47f4bf947d 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -123,41 +123,84 @@ 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 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 {
try {
const res = await httpRequestService.send(id);
- const redirectUris: string[] = [];
+ 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 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(`
${text}
`);
+ if (res.headers.get('content-type')?.includes('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');
}
- if (typeof microformats.logo === 'string') {
- logo = microformats.logo;
+
+ // 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 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(`