feat(auth): OIDC phase 0+1 — discovery, flow detection, client config

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>
This commit is contained in:
2026-06-30 15:51:23 -04:00
parent 30d0331174
commit 98ad5674a8
6 changed files with 173 additions and 1 deletions
+45
View File
@@ -0,0 +1,45 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { LoginFlow } from 'matrix-js-sdk/lib/@types/auth';
import {
getOidcCompatibilityFlag,
getSSOFlow,
getPasswordFlow,
getTokenFlow,
} from './useParsedLoginFlows';
const flows = (...f: Record<string, unknown>[]): LoginFlow[] => f as unknown as LoginFlow[];
test('flow getters pick the right flow', () => {
const f = flows({ type: 'm.login.password' }, { type: 'm.login.sso' }, { type: 'm.login.token' });
assert.equal(getPasswordFlow(f)?.type, 'm.login.password');
assert.equal(getSSOFlow(f)?.type, 'm.login.sso');
assert.equal(getTokenFlow(f)?.type, 'm.login.token');
assert.equal(getSSOFlow(flows({ type: 'm.login.cas' }))?.type, 'm.login.cas');
});
test('getOidcCompatibilityFlag detects the stable flag name', () => {
assert.equal(
getOidcCompatibilityFlag(flows({ type: 'm.login.sso', oauth_aware_preferred: true })),
true,
);
});
test('getOidcCompatibilityFlag detects the msc3824 alt name', () => {
assert.equal(
getOidcCompatibilityFlag(
flows({ type: 'm.login.sso', 'org.matrix.msc3824.delegated_oidc_compatibility': true }),
),
true,
);
});
test('getOidcCompatibilityFlag is false without sso or without the flag', () => {
assert.equal(getOidcCompatibilityFlag(flows({ type: 'm.login.password' })), false);
assert.equal(getOidcCompatibilityFlag(flows({ type: 'm.login.sso' })), false);
// non-true values do not count
assert.equal(
getOidcCompatibilityFlag(flows({ type: 'm.login.sso', oauth_aware_preferred: 'yes' })),
false,
);
});
+23 -1
View File
@@ -1,5 +1,11 @@
import { useMemo } from 'react';
import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth';
import {
ILoginFlow,
IPasswordFlow,
ISSOFlow,
LoginFlow,
OAUTH_AWARE_PREFERRED_FLOW_FIELD,
} from 'matrix-js-sdk/lib/@types/auth';
export const getSSOFlow = (loginFlows: LoginFlow[]): ISSOFlow | undefined =>
loginFlows.find((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas') as
@@ -13,6 +19,22 @@ export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
type: 'm.login.token';
};
/**
* MSC3824: a server may advertise `m.login.sso` while signalling (via the
* `oauth_aware_preferred` / `org.matrix.msc3824.delegated_oidc_compatibility`
* flow field) that it actually prefers native OIDC. This is a *secondary* hint —
* the authoritative signal for next-gen auth is an issuer in `.well-known`
* discovery (see `getOidcIssuer` in cs-api.ts).
*/
export const getOidcCompatibilityFlag = (loginFlows: LoginFlow[]): boolean => {
const sso = getSSOFlow(loginFlows) as (ISSOFlow & Record<string, unknown>) | undefined;
if (!sso) return false;
return (
sso[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name] === true ||
sso[OAUTH_AWARE_PREFERRED_FLOW_FIELD.altName] === true
);
};
export type ParsedLoginFlows = {
password?: LoginFlow;
token?: LoginFlow;