mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(oidc): OIDC_USERNAME_CLAIM picks the claim a new account's username comes from
Providers like Pocket ID send the full name in 'name', which became 'JaneDoe'. OIDC_USERNAME_CLAIM=preferred_username builds the username from that claim instead. Unset, or with the claim empty, the old order applies (name, preferred_username, the email's local part), and it is only read when an account is created. (#1677)
This commit is contained in:
@@ -64,6 +64,9 @@ data:
|
||||
{{- if .Values.env.OIDC_ADMIN_VALUE }}
|
||||
OIDC_ADMIN_VALUE: {{ .Values.env.OIDC_ADMIN_VALUE | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.env.OIDC_USERNAME_CLAIM }}
|
||||
OIDC_USERNAME_CLAIM: {{ .Values.env.OIDC_USERNAME_CLAIM | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.env.OIDC_SCOPE }}
|
||||
OIDC_SCOPE: {{ .Values.env.OIDC_SCOPE | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -71,6 +71,8 @@ env:
|
||||
# OIDC claim used to identify admin users.
|
||||
# OIDC_ADMIN_VALUE: ""
|
||||
# Value of the OIDC claim that grants admin role.
|
||||
# OIDC_USERNAME_CLAIM: ""
|
||||
# Claim a new account's username comes from, e.g. "preferred_username". Unset: name, then preferred_username.
|
||||
# OIDC_SCOPE: "openid email profile groups"
|
||||
# Space-separated OIDC scopes to request. Must include scopes for any claim used by OIDC_ADMIN_CLAIM.
|
||||
# OIDC_DISCOVERY_URL: ""
|
||||
|
||||
@@ -43,6 +43,7 @@ services:
|
||||
# - OIDC_ONLY=false # Set true to force SSO-only mode: disables password login and registration, overrides Admin > Settings toggles, cannot be changed at runtime
|
||||
# - OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users
|
||||
# - OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role
|
||||
# - OIDC_USERNAME_CLAIM=preferred_username # Claim a new account's username comes from (default: name, then preferred_username)
|
||||
# - OIDC_SCOPE=openid email profile # Fully overrides the default. Add extra scopes as needed (e.g. add groups if using OIDC_ADMIN_CLAIM)
|
||||
# - OIDC_DISCOVERY_URL= # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik)
|
||||
# - ADMIN_EMAIL=admin@trek.local # Initial admin e-mail — only used on first boot when no users exist
|
||||
|
||||
@@ -37,6 +37,7 @@ OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button
|
||||
OIDC_ONLY=true # Disable local password auth entirely (SSO only). Equivalent to setting password_login=false and password_registration=false in Admin > Settings.
|
||||
OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users
|
||||
OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role
|
||||
OIDC_USERNAME_CLAIM= # Claim a new account's username comes from, e.g. preferred_username. Unset: name, then preferred_username
|
||||
OIDC_DISCOVERY_URL= # Override the auto-constructed OIDC discovery endpoint. Useful for providers (e.g. Authentik) that expose it at a non-standard path. Example: https://auth.example.com/application/o/trek/.well-known/openid-configuration
|
||||
OIDC_SCOPE=openid email profile # Fully overrides the default. Add extra scopes as needed (e.g. add groups if using OIDC_ADMIN_CLAIM)
|
||||
|
||||
|
||||
@@ -206,6 +206,9 @@ export function deriveOidc(raw: RawEnv) {
|
||||
only: parseBool(raw.OIDC_ONLY) === true,
|
||||
adminClaim: raw.OIDC_ADMIN_CLAIM || 'groups',
|
||||
adminValue: raw.OIDC_ADMIN_VALUE,
|
||||
// Unset keeps the old order (name, then preferred_username), so an instance
|
||||
// that never sets it names new accounts exactly as before (#1677).
|
||||
usernameClaim: raw.OIDC_USERNAME_CLAIM?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ export const envSchema = z.object({
|
||||
OIDC_ONLY: boolStr,
|
||||
OIDC_ADMIN_CLAIM: anyString,
|
||||
OIDC_ADMIN_VALUE: anyString,
|
||||
OIDC_USERNAME_CLAIM: anyString,
|
||||
|
||||
// SMTP
|
||||
SMTP_HOST: anyString,
|
||||
|
||||
@@ -607,6 +607,20 @@ export class OidcService implements OnModuleDestroy {
|
||||
// Find or create user by OIDC sub / email
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The claim a new account's username comes from (#1677). OIDC_USERNAME_CLAIM
|
||||
* names one, typically `preferred_username` for providers whose `name` is the
|
||||
* full "Jane Doe"; when it is unset, or the provider leaves that claim empty,
|
||||
* the old order applies. Only read when an account is created: a username the
|
||||
* user has changed since is never overwritten on a later login.
|
||||
*/
|
||||
private usernameSource(userInfo: OidcUserInfo): string | undefined {
|
||||
const claimKey = readEnv().oidc.usernameClaim;
|
||||
const claimed = claimKey ? userInfo[claimKey] : undefined;
|
||||
if (typeof claimed === 'string' && claimed.trim()) return claimed.trim();
|
||||
return userInfo.name || userInfo.preferred_username;
|
||||
}
|
||||
|
||||
findOrCreateUser(
|
||||
userInfo: OidcUserInfo,
|
||||
config: OidcConfig,
|
||||
@@ -617,7 +631,7 @@ export class OidcService implements OnModuleDestroy {
|
||||
// `oidc_error=' + result.error` pass-through if reached here.
|
||||
if (!userInfo.email) return { error: 'no_email' };
|
||||
const email = userInfo.email.trim().toLowerCase();
|
||||
const name = userInfo.name || userInfo.preferred_username || email.split('@')[0];
|
||||
const name = this.usernameSource(userInfo) || email.split('@')[0];
|
||||
const sub = userInfo.sub;
|
||||
const picture = safeOidcPicture(userInfo.picture);
|
||||
|
||||
|
||||
@@ -146,6 +146,12 @@ describe('deriveOidc', () => {
|
||||
expect(deriveOidc({ OIDC_ADMIN_CLAIM: 'roles' }).adminClaim).toBe('roles');
|
||||
});
|
||||
|
||||
it('usernameClaim stays unset unless a claim is named (#1677)', () => {
|
||||
expect(deriveOidc({}).usernameClaim).toBeUndefined();
|
||||
expect(deriveOidc({ OIDC_USERNAME_CLAIM: ' ' }).usernameClaim).toBeUndefined();
|
||||
expect(deriveOidc({ OIDC_USERNAME_CLAIM: ' preferred_username ' }).usernameClaim).toBe('preferred_username');
|
||||
});
|
||||
|
||||
it('OIDC_ONLY coerces the boolean-like family', () => {
|
||||
expect(deriveOidc({ OIDC_ONLY: 'True' }).only).toBe(true);
|
||||
expect(deriveOidc({ OIDC_ONLY: '1' }).only).toBe(true);
|
||||
|
||||
@@ -145,6 +145,7 @@ beforeEach(() => {
|
||||
resetTestDb(testDb);
|
||||
delete process.env.OIDC_ADMIN_VALUE;
|
||||
delete process.env.OIDC_ADMIN_CLAIM;
|
||||
delete process.env.OIDC_USERNAME_CLAIM;
|
||||
delete process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
@@ -529,6 +530,48 @@ describe('findOrCreateUser', () => {
|
||||
expect((result as { user: any }).user.role).toBe('admin');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-022b: without OIDC_USERNAME_CLAIM the username comes from name, as before (#1677)', () => {
|
||||
createUser(testDb, { email: 'someone@example.com' });
|
||||
const result = svc.findOrCreateUser(
|
||||
{ sub: 'sub-jane-1', email: 'jane@example.com', name: 'Jane Doe', preferred_username: 'jane' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
expect((result as { user: { username: string } }).user.username).toBe('JaneDoe');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-022c: OIDC_USERNAME_CLAIM picks the claim the username is built from (#1677)', () => {
|
||||
createUser(testDb, { email: 'someone@example.com' });
|
||||
process.env.OIDC_USERNAME_CLAIM = 'preferred_username';
|
||||
const result = svc.findOrCreateUser(
|
||||
{ sub: 'sub-jane-2', email: 'jane2@example.com', name: 'Jane Doe', preferred_username: 'jane.d' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
expect((result as { user: { username: string } }).user.username).toBe('jane.d');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-022d: a named claim the provider leaves empty falls back to name, then the email (#1677)', () => {
|
||||
createUser(testDb, { email: 'someone@example.com' });
|
||||
process.env.OIDC_USERNAME_CLAIM = 'nickname';
|
||||
const withName = svc.findOrCreateUser(
|
||||
{ sub: 'sub-jane-3', email: 'jane3@example.com', name: 'Jane Three', nickname: ' ' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
expect((withName as { user: { username: string } }).user.username).toBe('JaneThree');
|
||||
const bare = svc.findOrCreateUser({ sub: 'sub-jane-4', email: 'j.four@example.com' }, MOCK_CONFIG);
|
||||
expect((bare as { user: { username: string } }).user.username).toBe('j.four');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-022e: a later login never renames the account to the claim', () => {
|
||||
const { user } = createUser(testDb, { email: 'kept@example.com', username: 'chosen' });
|
||||
testDb.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ? WHERE id = ?').run('sub-kept', MOCK_CONFIG.issuer, user.id);
|
||||
process.env.OIDC_USERNAME_CLAIM = 'preferred_username';
|
||||
const result = svc.findOrCreateUser(
|
||||
{ sub: 'sub-kept', email: 'kept@example.com', name: 'Kept', preferred_username: 'idp-name' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
expect((result as { user: { username: string } }).user.username).toBe('chosen');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-024: returns registration_disabled error when registration is off', () => {
|
||||
createUser(testDb, { email: 'existing@example.com' });
|
||||
testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('allow_registration', 'false')").run();
|
||||
|
||||
@@ -192,6 +192,7 @@ For setup instructions, see [OIDC-SSO](OIDC-SSO).
|
||||
| `OIDC_ONLY` | Force SSO-only mode: disables password login and registration, overrides Admin > Settings toggles, cannot be changed at runtime. First SSO login becomes admin on a fresh instance. Since the login page then has no "Remember me" switch, every SSO session gets the `SESSION_DURATION_REMEMBER` lifetime; shorten that variable for shorter sessions, `SESSION_DURATION` does not apply here. | `false` |
|
||||
| `OIDC_ADMIN_CLAIM` | OIDC claim inspected for the admin role. Only takes effect once `OIDC_ADMIN_VALUE` is set. | `groups` |
|
||||
| `OIDC_ADMIN_VALUE` | Value of the OIDC claim that grants admin role (e.g. `app-trek-admins`) | — |
|
||||
| `OIDC_USERNAME_CLAIM` | Claim a new account's username is built from, e.g. `preferred_username` when your provider sends the full name in `name`. Only used when the account is created; an empty claim falls back to the default order. | `name`, then `preferred_username` |
|
||||
| `OIDC_SCOPE` | Space-separated OIDC scopes to request. **Fully replaces** the default — always include `openid email profile` plus any extra scopes (e.g. add `groups` when using `OIDC_ADMIN_CLAIM`) | `openid email profile` |
|
||||
| `OIDC_DISCOVERY_URL` | Override the auto-constructed OIDC discovery endpoint. Required for providers with a non-standard path (e.g. Authentik) | — |
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ env:
|
||||
# OIDC_ONLY: "false" # force SSO-only mode; disables password login
|
||||
# OIDC_ADMIN_CLAIM: "" # OIDC claim used to identify admin users
|
||||
# OIDC_ADMIN_VALUE: "" # value of that claim that grants admin role
|
||||
# OIDC_USERNAME_CLAIM: "" # claim a new account's username comes from, e.g. preferred_username
|
||||
# OIDC_SCOPE: "openid email profile groups"
|
||||
# OIDC_DISCOVERY_URL: "" # override for providers with non-standard discovery paths (e.g. Authentik)
|
||||
# VAPID_PUBLIC_KEY: "" # Web Push: public half of your own key pair; leave unset to use the generated one
|
||||
|
||||
+2
-1
@@ -55,6 +55,7 @@ For example: `https://trek.example.com/api/auth/oidc/callback`
|
||||
| `OIDC_ONLY` | Set to `true` to disable local password login and password registration. SSO login and SSO registration remain governed by their own toggles. This is an environment-variable-only setting and cannot be toggled at runtime via the admin panel. |
|
||||
| `OIDC_ADMIN_CLAIM` | OIDC claim to inspect for admin role mapping. Defaults to `groups`. The claim value may be an array or a plain string. The claim only reaches TREK if one of the scopes in `OIDC_SCOPE` carries it — see *Admin role mapping* below. **Env var only — not configurable via the admin panel.** |
|
||||
| `OIDC_ADMIN_VALUE` | Value that must be present in `OIDC_ADMIN_CLAIM` to grant the admin role. If unset, claim-based role mapping is disabled. When set, the role is re-evaluated on every login. **Env var only — not configurable via the admin panel.** |
|
||||
| `OIDC_USERNAME_CLAIM` | Claim the username of a new account is built from. Defaults to `name`, then `preferred_username`, then the part of the email before the `@`. Set it to `preferred_username` for providers like Pocket ID whose `name` is the full "Jane Doe". Only read when the account is created, so a username changed later in TREK stays; when the provider leaves the claim empty, the default order applies. **Env var only, not configurable via the admin panel.** |
|
||||
| `OIDC_SCOPE` | Overrides the default scope list sent to the provider. Defaults to `openid email profile`. Ensure `openid` and `email` are always included, plus whichever scope carries your `OIDC_ADMIN_CLAIM`. **Env var only — not configurable via the admin panel.** |
|
||||
| `OIDC_DISCOVERY_URL` | Full URL to the OIDC discovery document. Use this for providers with non-standard discovery paths (e.g. Authentik tenants). If unset, discovery is attempted at `<OIDC_ISSUER>/.well-known/openid-configuration`. The discovery document is cached for 1 hour. |
|
||||
|
||||
@@ -109,7 +110,7 @@ OIDC can also be configured without environment variables via **Admin → Settin
|
||||
|
||||
Environment variables take priority over database settings when both are present.
|
||||
|
||||
The following variables are **env var only** and have no admin panel equivalent: `OIDC_ONLY`, `OIDC_SCOPE`, `OIDC_ADMIN_CLAIM`, `OIDC_ADMIN_VALUE`.
|
||||
The following variables are **env var only** and have no admin panel equivalent: `OIDC_ONLY`, `OIDC_SCOPE`, `OIDC_ADMIN_CLAIM`, `OIDC_ADMIN_VALUE`, `OIDC_USERNAME_CLAIM`.
|
||||
|
||||
The `OIDC_ONLY` env var always overrides the panel's login-method toggles. To disable password login at runtime without `OIDC_ONLY`, use the **password_login** and **password_registration** toggles in Admin → Settings instead.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user