98ad5674a8
Toward MSC3861/MSC2965 next-gen-auth login (P4-6), client-only.
- cs-api.ts: type the stable `m.authentication` well-known key + getOidcIssuer()
(stable preferred over the unstable msc2965 key; {} for non-OIDC servers).
- useParsedLoginFlows.ts: getOidcCompatibilityFlag() (MSC3824 oauth_aware_preferred
/ delegated_oidc_compatibility) as a secondary OIDC hint.
- New pages/auth/oidc/oidcConfig.ts: dynamic-registration client metadata + the
non-hash callback URL (redirect_uris can't contain a fragment).
- paths.ts: OIDC_CALLBACK_PATH.
- 8 unit tests for the pure helpers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
import to from 'await-to-js';
|
|
import { trimTrailingSlash } from './utils/common';
|
|
|
|
export enum AutoDiscoveryAction {
|
|
PROMPT = 'PROMPT',
|
|
IGNORE = 'IGNORE',
|
|
FAIL_PROMPT = 'FAIL_PROMPT',
|
|
FAIL_ERROR = 'FAIL_ERROR',
|
|
}
|
|
|
|
export type AutoDiscoveryError = {
|
|
host: string;
|
|
action: AutoDiscoveryAction;
|
|
};
|
|
|
|
export type AutoDiscoveryInfo = Record<string, unknown> & {
|
|
'm.homeserver': {
|
|
base_url: string;
|
|
};
|
|
'm.identity_server'?: {
|
|
base_url: string;
|
|
};
|
|
// v1.15 stable next-gen-auth (MSC2965) discovery key — emitted by servers that
|
|
// delegate to a Matrix Authentication Service (e.g. mozilla.org). The
|
|
// `org.matrix.msc2965.authentication` key below is the unstable predecessor.
|
|
'm.authentication'?: {
|
|
issuer?: string;
|
|
account?: string;
|
|
};
|
|
'org.matrix.msc2965.authentication'?: {
|
|
account?: string;
|
|
issuer?: string;
|
|
};
|
|
'org.matrix.msc4143.rtc_foci'?: [
|
|
{
|
|
livekit_service_url: string;
|
|
type: 'livekit';
|
|
},
|
|
];
|
|
};
|
|
|
|
/**
|
|
* Resolve the OIDC issuer (and account-management URL) advertised by a homeserver
|
|
* in its `.well-known/matrix/client`, preferring the v1.15 stable
|
|
* `m.authentication` key over the unstable `org.matrix.msc2965.authentication`.
|
|
* Returns `{}` when the server is not OIDC-native (e.g. matrix.lotusguild.org).
|
|
*/
|
|
export const getOidcIssuer = (info: AutoDiscoveryInfo): { issuer?: string; account?: string } => {
|
|
const stable = info['m.authentication'];
|
|
if (stable && typeof stable.issuer === 'string') {
|
|
return { issuer: stable.issuer, account: stable.account };
|
|
}
|
|
const unstable = info['org.matrix.msc2965.authentication'];
|
|
if (unstable && typeof unstable.issuer === 'string') {
|
|
return { issuer: unstable.issuer, account: unstable.account };
|
|
}
|
|
return {};
|
|
};
|
|
|
|
export const autoDiscovery = async (
|
|
request: typeof fetch,
|
|
server: string,
|
|
): Promise<[AutoDiscoveryError, undefined] | [undefined, AutoDiscoveryInfo]> => {
|
|
const host = /^https?:\/\//.test(server) ? trimTrailingSlash(server) : `https://${server}`;
|
|
const autoDiscoveryUrl = `${host}/.well-known/matrix/client`;
|
|
|
|
const [err, response] = await to(request(autoDiscoveryUrl, { method: 'GET' }));
|
|
|
|
if (err || response.status === 404) {
|
|
// AutoDiscoveryAction.IGNORE
|
|
// We will use default value for IGNORE action
|
|
return [
|
|
undefined,
|
|
{
|
|
'm.homeserver': {
|
|
base_url: host,
|
|
},
|
|
},
|
|
];
|
|
}
|
|
if (response.status !== 200) {
|
|
return [
|
|
{
|
|
host,
|
|
action: AutoDiscoveryAction.FAIL_PROMPT,
|
|
},
|
|
undefined,
|
|
];
|
|
}
|
|
|
|
const [contentErr, content] = await to<AutoDiscoveryInfo>(response.json());
|
|
|
|
if (contentErr || typeof content !== 'object') {
|
|
return [
|
|
{
|
|
host,
|
|
action: AutoDiscoveryAction.FAIL_PROMPT,
|
|
},
|
|
undefined,
|
|
];
|
|
}
|
|
|
|
const baseUrl = content['m.homeserver']?.base_url;
|
|
if (typeof baseUrl !== 'string') {
|
|
return [
|
|
{
|
|
host,
|
|
action: AutoDiscoveryAction.FAIL_PROMPT,
|
|
},
|
|
undefined,
|
|
];
|
|
}
|
|
|
|
if (/^https?:\/\//.test(baseUrl) === false) {
|
|
return [
|
|
{
|
|
host,
|
|
action: AutoDiscoveryAction.FAIL_ERROR,
|
|
},
|
|
undefined,
|
|
];
|
|
}
|
|
|
|
content['m.homeserver'].base_url = trimTrailingSlash(baseUrl);
|
|
if (content['m.identity_server']) {
|
|
content['m.identity_server'].base_url = trimTrailingSlash(
|
|
content['m.identity_server'].base_url,
|
|
);
|
|
}
|
|
|
|
return [undefined, content];
|
|
};
|
|
|
|
export type SpecVersions = {
|
|
versions: string[];
|
|
unstable_features?: Record<string, boolean>;
|
|
};
|
|
export const specVersions = async (
|
|
request: typeof fetch,
|
|
baseUrl: string,
|
|
): Promise<SpecVersions> => {
|
|
const res = await request(`${trimTrailingSlash(baseUrl)}/_matrix/client/versions`);
|
|
|
|
const data = (await res.json()) as unknown;
|
|
|
|
if (data && typeof data === 'object' && 'versions' in data && Array.isArray(data.versions)) {
|
|
return data as SpecVersions;
|
|
}
|
|
throw new Error('Homeserver URL does not appear to be a valid Matrix homeserver');
|
|
};
|