mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-04 21:15:45 +02:00
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
This commit is contained in:
@@ -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 <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[] = [];
|
||||
|
||||
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>`);
|
||||
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(`<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 +215,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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user