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:
parent
ff7d2c1083
commit
01aa56c602
|
|
@ -10,7 +10,9 @@
|
||||||
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
|
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
|
||||||
|
|
||||||
### Server
|
### 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
|
## 2025.12.2
|
||||||
|
|
|
||||||
|
|
@ -123,41 +123,84 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str
|
||||||
return { name, logo };
|
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> {
|
async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise<ClientInformation> {
|
||||||
try {
|
try {
|
||||||
const res = await httpRequestService.send(id);
|
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');
|
const linkHeader = res.headers.get('link');
|
||||||
if (linkHeader) {
|
if (linkHeader) {
|
||||||
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
|
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await res.text();
|
if (res.headers.get('content-type')?.includes('application/json')) {
|
||||||
const doc = htmlParser.parse(`<div>${text}</div>`);
|
// 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;
|
// https://indieauth.spec.indieweb.org/#client-metadata-li-1
|
||||||
let logo: string | null = null;
|
// "The authorization server MUST verify that the client_id in the document matches the
|
||||||
if (text) {
|
// client_id of the URL where the document was retrieved."
|
||||||
const microformats = parseMicroformats(doc, res.url, id);
|
if (json.client_id !== id) {
|
||||||
if (typeof microformats.name === 'string') {
|
throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request');
|
||||||
name = microformats.name;
|
|
||||||
}
|
}
|
||||||
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 });
|
logger.error('Error while fetching client information', { err });
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
|
throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
|
||||||
|
} else if (err instanceof AuthorizationError) {
|
||||||
|
throw err;
|
||||||
} else {
|
} else {
|
||||||
throw new AuthorizationError('Failed to parse client information', 'server_error');
|
throw new AuthorizationError('Failed to parse client information', 'server_error');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const host = `http://127.0.0.1:${port}`;
|
||||||
|
|
||||||
const clientPort = port + 1;
|
const clientPort = port + 1;
|
||||||
const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`;
|
const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`;
|
||||||
|
const redirect_uri2 = `http://127.0.0.1:${clientPort}/redirect2`;
|
||||||
|
|
||||||
const basicAuthParams: AuthorizationParamsExtended = {
|
const basicAuthParams: AuthorizationParamsExtended = {
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
|
|
@ -807,65 +808,19 @@ describe('OAuth', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
|
||||||
describe('Client Information Discovery', () => {
|
describe('Client Information Discovery', () => {
|
||||||
describe('Redirection', () => {
|
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
||||||
const tests: Record<string, (reply: FastifyReply) => void> = {
|
describe('JSON client metadata (11 July 2024)', () => {
|
||||||
'Read HTTP header': reply => {
|
test('Read JSON document', async () => {
|
||||||
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
|
||||||
reply.send(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
'Mixed links': reply => {
|
|
||||||
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
|
||||||
reply.send(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<link rel="redirect_uri" href="/redirect2" />
|
|
||||||
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
'Multiple items in Link header': reply => {
|
|
||||||
reply.header('Link', '</redirect2>; rel="redirect_uri",</redirect>; rel="redirect_uri"');
|
|
||||||
reply.send(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
'Multiple items in HTML': reply => {
|
|
||||||
reply.send(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<link rel="redirect_uri" href="/redirect2" />
|
|
||||||
<link rel="redirect_uri" href="/redirect" />
|
|
||||||
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [title, replyFunc] of Object.entries(tests)) {
|
|
||||||
test(title, async () => {
|
|
||||||
sender = replyFunc;
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
test('No item', async () => {
|
|
||||||
sender = (reply): void => {
|
sender = (reply): void => {
|
||||||
reply.send(`
|
reply.header('content-type', 'application/json');
|
||||||
<!DOCTYPE html>
|
reply.send({
|
||||||
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
client_id: `http://127.0.0.1:${clientPort}/`,
|
||||||
`);
|
client_uri: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
client_name: 'Misklient JSON',
|
||||||
|
logo_uri: '/logo.png',
|
||||||
|
redirect_uris: ['/redirect'],
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const client = new AuthorizationCode(clientConfig);
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
@ -877,119 +832,294 @@ describe('OAuth', () => {
|
||||||
code_challenge: 'code',
|
code_challenge: 'code',
|
||||||
code_challenge_method: 'S256',
|
code_challenge_method: 'S256',
|
||||||
} as AuthorizationParamsExtended));
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
const meta = getMeta(await response.text());
|
||||||
|
assert.strictEqual(meta.clientName, 'Misklient JSON');
|
||||||
|
assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`);
|
||||||
|
});
|
||||||
|
|
||||||
// direct error because there's no redirect URI to ping
|
test('Merge Link header redirect_uri with JSON redirect_uris', async () => {
|
||||||
|
sender = (reply): void => {
|
||||||
|
reply.header('Link', '</redirect2>; rel="redirect_uri"');
|
||||||
|
reply.header('content-type', 'application/json');
|
||||||
|
reply.send({
|
||||||
|
client_id: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
client_uri: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
client_name: 'Misklient JSON',
|
||||||
|
redirect_uris: ['/redirect'],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const ok1 = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(ok1.status, 200);
|
||||||
|
|
||||||
|
const ok2 = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri: redirect_uri2,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(ok2.status, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reject when client_id does not match retrieved URL', async () => {
|
||||||
|
sender = (reply): void => {
|
||||||
|
reply.header('content-type', 'application/json');
|
||||||
|
reply.send({
|
||||||
|
client_id: `http://127.0.0.1:${clientPort}/mismatch`,
|
||||||
|
client_uri: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
redirect_uris: ['/redirect'],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reject when client_uri is not a prefix of client_id', async () => {
|
||||||
|
sender = (reply): void => {
|
||||||
|
reply.header('content-type', 'application/json');
|
||||||
|
reply.send({
|
||||||
|
client_id: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
client_uri: `http://127.0.0.1:${clientPort}/no-prefix/`,
|
||||||
|
redirect_uris: ['/redirect'],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reject when JSON metadata has no redirect_uris and no Link header', async () => {
|
||||||
|
sender = (reply): void => {
|
||||||
|
reply.header('content-type', 'application/json');
|
||||||
|
reply.send({
|
||||||
|
client_id: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
client_uri: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
client_name: 'Misklient JSON',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
await assertDirectError(response, 400, 'invalid_request');
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Disallow loopback', async () => {
|
// https://indieauth.spec.indieweb.org/20220212/#client-information-discovery
|
||||||
await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' });
|
describe('HTML link client metadata (12 Feb 2022)', () => {
|
||||||
|
describe('Redirection', () => {
|
||||||
|
const tests: Record<string, (reply: FastifyReply) => void> = {
|
||||||
|
'Read HTTP header': reply => {
|
||||||
|
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
'Mixed links': reply => {
|
||||||
|
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="redirect_uri" href="/redirect2" />
|
||||||
|
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
'Multiple items in Link header': reply => {
|
||||||
|
reply.header('Link', '</redirect2>; rel="redirect_uri",</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
'Multiple items in HTML': reply => {
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="redirect_uri" href="/redirect2" />
|
||||||
|
<link rel="redirect_uri" href="/redirect" />
|
||||||
|
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const client = new AuthorizationCode(clientConfig);
|
for (const [title, replyFunc] of Object.entries(tests)) {
|
||||||
const response = await fetch(client.authorizeURL({
|
test(title, async () => {
|
||||||
redirect_uri,
|
sender = replyFunc;
|
||||||
scope: 'write:notes',
|
|
||||||
state: 'state',
|
|
||||||
code_challenge: 'code',
|
|
||||||
code_challenge_method: 'S256',
|
|
||||||
} as AuthorizationParamsExtended));
|
|
||||||
await assertDirectError(response, 400, 'invalid_request');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Missing name', async () => {
|
const client = new AuthorizationCode(clientConfig);
|
||||||
sender = (reply): void => {
|
|
||||||
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
|
||||||
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 response = await fetch(client.authorizeURL({
|
test('No item', async () => {
|
||||||
redirect_uri,
|
sender = (reply): void => {
|
||||||
scope: 'write:notes',
|
reply.send(`
|
||||||
state: 'state',
|
<!DOCTYPE html>
|
||||||
code_challenge: 'code',
|
<div class="h-app"><a href="/" class="u-url p-name">Misklient
|
||||||
code_challenge_method: 'S256',
|
`);
|
||||||
} as AuthorizationParamsExtended));
|
};
|
||||||
assert.strictEqual(response.status, 200);
|
|
||||||
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('With Logo', async () => {
|
const client = new AuthorizationCode(clientConfig);
|
||||||
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));
|
||||||
|
|
||||||
const response = await fetch(client.authorizeURL({
|
// direct error because there's no redirect URI to ping
|
||||||
redirect_uri,
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
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);
|
test('Disallow loopback', async () => {
|
||||||
|
await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' });
|
||||||
|
|
||||||
const response = await fetch(client.authorizeURL({
|
const client = new AuthorizationCode(clientConfig);
|
||||||
redirect_uri,
|
const response = await fetch(client.authorizeURL({
|
||||||
scope: 'write:notes',
|
redirect_uri,
|
||||||
state: 'state',
|
scope: 'write:notes',
|
||||||
code_challenge: 'code',
|
state: 'state',
|
||||||
code_challenge_method: 'S256',
|
code_challenge: 'code',
|
||||||
} as AuthorizationParamsExtended));
|
code_challenge_method: 'S256',
|
||||||
assert.strictEqual(response.status, 200);
|
} as AuthorizationParamsExtended));
|
||||||
const meta = getMeta(await response.text());
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
assert.strictEqual(meta.clientName, 'Misklient');
|
});
|
||||||
assert.strictEqual(meta.clientLogo, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Mismatching URL in h-app', async () => {
|
test('Missing name', async () => {
|
||||||
sender = (reply): void => {
|
sender = (reply): void => {
|
||||||
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
||||||
reply.send(`
|
reply.send();
|
||||||
<!DOCTYPE html>
|
};
|
||||||
<div class="h-app"><a href="/foo" class="u-url p-name">Misklient
|
|
||||||
`);
|
|
||||||
reply.send();
|
|
||||||
};
|
|
||||||
|
|
||||||
const client = new AuthorizationCode(clientConfig);
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
const response = await fetch(client.authorizeURL({
|
const response = await fetch(client.authorizeURL({
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
scope: 'write:notes',
|
scope: 'write:notes',
|
||||||
state: 'state',
|
state: 'state',
|
||||||
code_challenge: 'code',
|
code_challenge: 'code',
|
||||||
code_challenge_method: 'S256',
|
code_challenge_method: 'S256',
|
||||||
} as AuthorizationParamsExtended));
|
} as AuthorizationParamsExtended));
|
||||||
assert.strictEqual(response.status, 200);
|
assert.strictEqual(response.status, 200);
|
||||||
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
|
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"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<div class="h-app"><a href="/foo" 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);
|
||||||
|
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue