auth: sign in with OpenID Connect, connect and disconnect, sign-in modes (S2)
Spec 12 S2: openid-client 6.8 (decision 0004). Discovery, PKCE + state + nonce, ID token signatures checked, userinfo always read. Users connect their OpenID account from My account and sign in either way; no accounts are made; a user without a password is matched once by verified email. auth.mode mixed and oidc (admins keep a password way in), role from adminGroups, allowedGroups, signing out at the provider. Settings -> Sign-in with Test. Tested against a fake provider with RS256 and ES256 keys.
This commit is contained in:
1 parent
3a9423af1f
commit
06b3a73edc
32 files changed
+1747
-64
No files matched your search
@@ -42,7 +42,8 @@ export type AuthUser = {
|
||||
/** The ways a user can sign in (12 §2): their password, a connected OpenID account, the directory. */
|
||||
export type SignInWays = {
|
||||
password: boolean;
|
||||
oidc: { email: string | null } | null;
|
||||
/** `groups`: what the provider sent at the last sign-in (12 §3.4), to copy into Settings. */
|
||||
oidc: { email: string | null; groups: string[] } | null;
|
||||
ldap: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -118,6 +118,21 @@ export const SettingsSchemas = {
|
||||
.default(null),
|
||||
/** Sign-in (12 §1.1): passwords only, OpenID only (admins keep passwords), or both. */
|
||||
'auth.mode': z.enum(['password', 'mixed', 'oidc']).default('password'),
|
||||
/** OpenID Connect (12 §3.1). The client secret is encrypted; null = a public client. */
|
||||
'auth.oidc': z
|
||||
.object({
|
||||
label: z.string(),
|
||||
issuer: z.string(),
|
||||
clientId: z.string(),
|
||||
clientSecretEnc: z.string().nullable(),
|
||||
scopes: z.string(),
|
||||
groupsClaim: z.string(),
|
||||
adminGroups: z.array(z.string()),
|
||||
allowedGroups: z.array(z.string()),
|
||||
trustEmail: z.boolean(),
|
||||
})
|
||||
.nullable()
|
||||
.default(null),
|
||||
// Not in 02 §3's table but used by the generator (03 §3).
|
||||
'system.transportHash': z.string().nullable().default(null),
|
||||
};
|
||||
@@ -150,12 +165,56 @@ export const ContrackInputSchema = z.object({
|
||||
});
|
||||
export type ContrackInput = z.input<typeof ContrackInputSchema>;
|
||||
|
||||
const groupList = z
|
||||
.array(z.string().trim().min(1).max(200))
|
||||
.max(50)
|
||||
.transform((l) => [...new Set(l)]);
|
||||
|
||||
/** PATCH /api/settings `auth.oidc` and its Test: the client secret is write-only (empty keeps it). */
|
||||
export const OidcInputSchema = z.object({
|
||||
label: z.string().trim().min(1, 'Required').max(40),
|
||||
// Exactly as the provider writes it: the issuer is compared character for character
|
||||
// (Authentik's ends in "/").
|
||||
issuer: z.url({ protocol: /^https?$/ }).trim(),
|
||||
clientId: z.string().trim().min(1, 'Required').max(200),
|
||||
clientSecret: z.string().max(500).optional(),
|
||||
clearClientSecret: z.boolean().optional(),
|
||||
scopes: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(500)
|
||||
.refine((s) => s.split(/\s+/).includes('openid'), 'Must include openid'),
|
||||
groupsClaim: z.string().trim().min(1).max(200),
|
||||
adminGroups: groupList,
|
||||
allowedGroups: groupList,
|
||||
trustEmail: z.boolean(),
|
||||
});
|
||||
export type OidcInput = z.input<typeof OidcInputSchema>;
|
||||
|
||||
/** POST /api/settings/auth/oidc/test. */
|
||||
export type OidcTestResult =
|
||||
| {
|
||||
ok: true;
|
||||
issuer: string;
|
||||
algorithms: string[];
|
||||
pkce: boolean;
|
||||
refreshTokens: boolean;
|
||||
endSession: boolean;
|
||||
redirectUri: string;
|
||||
postLogoutRedirectUri: string;
|
||||
}
|
||||
| { ok: false; error: string };
|
||||
|
||||
/** GET /api/settings: every key, secrets replaced by hasPassword / hasCompanyToken. */
|
||||
export type SettingsDto = {
|
||||
[K in Exclude<SettingKey, 'smtp' | 'sms.webhookToken' | 'contacts.contrack'>]: SettingValue<K>;
|
||||
[
|
||||
K in Exclude<SettingKey, 'smtp' | 'sms.webhookToken' | 'contacts.contrack' | 'auth.oidc'>
|
||||
]: SettingValue<K>;
|
||||
} & {
|
||||
smtp: (Omit<SmtpInput, 'password'> & { hasPassword: boolean }) | null;
|
||||
'contacts.contrack': { url: string; publicUrl: string | null; hasCompanyToken: boolean } | null;
|
||||
'auth.oidc':
|
||||
(Omit<OidcInput, 'clientSecret' | 'clearClientSecret'> & { hasClientSecret: boolean }) | null;
|
||||
};
|
||||
|
||||
/** POST /api/ai/test: one endpoint tried with a tiny request. `audio`: the TTS test, a data: URL. */
|
||||
|
||||
Reference in new issue
Block a user