Skip to content

Commit 01aa56c

Browse files
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
1 parent ff7d2c1 commit 01aa56c

3 files changed

Lines changed: 346 additions & 169 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
1111

1212
### Server
13-
-
13+
- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
14+
- JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
15+
- 従来の実装(12 February 2022版・HTML Microformat形式)も引き続きサポートされます
1416

1517

1618
## 2025.12.2

packages/backend/src/server/oauth/OAuth2ProviderService.ts

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -123,41 +123,84 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str
123123
return { name, logo };
124124
}
125125

126-
// https://indieauth.spec.indieweb.org/#client-information-discovery
127-
// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
128-
// and if there is an [h-app] with a url property matching the client_id URL,
129-
// then it should use the name and icon and display them on the authorization prompt."
130-
// (But we don't display any icon for now)
131-
// https://indieauth.spec.indieweb.org/#redirect-url
132-
// "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
133-
// of redirect_uri at the client_id URL.
134-
// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
135-
// look for an exact match of the given redirect_uri in the request against the list of
136-
// redirect_uris discovered after resolving any relative URLs."
137126
async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise<ClientInformation> {
138127
try {
139128
const res = await httpRequestService.send(id);
129+
140130
const redirectUris: string[] = [];
131+
let name = id;
132+
let logo: string | null = null;
141133

134+
// https://indieauth.spec.indieweb.org/#redirect-url
135+
// "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
136+
// of redirect_uri at the client_id URL.
137+
// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
138+
// look for an exact match of the given redirect_uri in the request against the list of
139+
// redirect_uris discovered after resolving any relative URLs."
142140
const linkHeader = res.headers.get('link');
143141
if (linkHeader) {
144142
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
145143
}
146144

147-
const text = await res.text();
148-
const doc = htmlParser.parse(`<div>${text}</div>`);
145+
if (res.headers.get('content-type')?.includes('application/json')) {
146+
// Client discovery via JSON document (11 July 2024 spec)
147+
// https://indieauth.spec.indieweb.org/#client-metadata
148+
// "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing
149+
// client metadata defined in [RFC7591], the minimum properties for an IndieAuth
150+
// client defined below."
151+
152+
const json = await res.json() as {
153+
client_id: string;
154+
client_name?: string;
155+
client_uri: string;
156+
logo_uri?: string;
157+
redirect_uris?: string[];
158+
};
159+
160+
// https://indieauth.spec.indieweb.org/#client-metadata-li-1
161+
// "The authorization server MUST verify that the client_id in the document matches the
162+
// client_id of the URL where the document was retrieved."
163+
if (json.client_id !== id) {
164+
throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request');
165+
}
149166

150-
redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
167+
// https://indieauth.spec.indieweb.org/#client-metadata-li-1
168+
// "The client_uri MUST be a prefix of the client_id."
169+
if (!json.client_uri || !id.startsWith(json.client_uri)) {
170+
throw new AuthorizationError('client_uri is not a prefix of client_id', 'invalid_request');
171+
}
151172

152-
let name = id;
153-
let logo: string | null = null;
154-
if (text) {
155-
const microformats = parseMicroformats(doc, res.url, id);
156-
if (typeof microformats.name === 'string') {
157-
name = microformats.name;
173+
if (typeof json.client_name === 'string') {
174+
name = json.client_name;
158175
}
159-
if (typeof microformats.logo === 'string') {
160-
logo = microformats.logo;
176+
177+
if (typeof json.logo_uri === 'string') {
178+
// Since uri can be relative, resolve it against the document URL
179+
logo = new URL(json.logo_uri, res.url).toString();
180+
}
181+
182+
if (Array.isArray(json.redirect_uris)) {
183+
redirectUris.push(...json.redirect_uris.filter((uri): uri is string => typeof uri === 'string'));
184+
}
185+
} else {
186+
// Client discovery via HTML microformats (12 February 2022 spec)
187+
// https://indieauth.spec.indieweb.org/20220212/#client-information-discovery
188+
// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
189+
// and if there is an [h-app] with a url property matching the client_id URL,
190+
// then it should use the name and icon and display them on the authorization prompt."
191+
const text = await res.text();
192+
const doc = htmlParser.parse(`<div>${text}</div>`);
193+
194+
redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
195+
196+
if (text) {
197+
const microformats = parseMicroformats(doc, res.url, id);
198+
if (typeof microformats.name === 'string') {
199+
name = microformats.name;
200+
}
201+
if (typeof microformats.logo === 'string') {
202+
logo = microformats.logo;
203+
}
161204
}
162205
}
163206

@@ -172,6 +215,8 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
172215
logger.error('Error while fetching client information', { err });
173216
if (err instanceof StatusError) {
174217
throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
218+
} else if (err instanceof AuthorizationError) {
219+
throw err;
175220
} else {
176221
throw new AuthorizationError('Failed to parse client information', 'server_error');
177222
}

0 commit comments

Comments
 (0)