photoprism/internal/auth/oidc
Keith Martin b6687b6c29
Tests: Improve isolation and add fixes #5263
* Tests: fix random selection of deleted record causing test failure
* Tests: fix test folders being left after test
* Tests: ensure that required records are available
* Tests: make each package execute against it's own testdata folder to ensure no interpackage test failures
* Tests: add unit tests of CleanupTestFolder
* Tests:  Allow defer in TestMain
* Tests: rename testMain to runTestMain
2026-07-12 05:47:21 +02:00
..
client.go OIDC: Add configurable authorization prompt option #5698 2026-06-27 12:35:04 +00:00
client_test.go OIDC: Add configurable authorization prompt option #5698 2026-06-27 12:35:04 +00:00
groups.go Cluster: Collect OIDC groups at login for group-based admission 2026-06-12 16:08:38 +00:00
groups_test.go Cluster: Collect OIDC groups at login for group-based admission 2026-06-12 16:08:38 +00:00
http_client.go Logs: Add package pkg/log/status to provide generic outcome constants 2025-10-21 14:42:05 +02:00
http_client_test.go OIDC: Upgrade "zitadel/oidc" from v1 to v2 #782 2024-07-06 11:15:23 +02:00
logout.go Auth: Delegate cluster OIDC logout to the Portal end-session endpoint #5684 2026-06-24 15:22:04 +00:00
logout_test.go Auth: Add RP-initiated OIDC logout via PHOTOPRISM_OIDC_LOGOUT #5684 #5433 2026-06-24 17:21:00 +02:00
nonce.go OIDC: Send and validate a nonce on authorization requests #5695 2026-06-25 16:54:29 +02:00
nonce_test.go OIDC: Send and validate a nonce on authorization requests #5695 2026-06-25 16:54:29 +02:00
oidc.go Links: Use canonical trailing-slash form for website URLs 2026-06-24 17:20:30 +02:00
oidc_test.go Tests: Improve isolation and add fixes #5263 2026-07-12 05:47:21 +02:00
prompt.go OIDC: Add configurable authorization prompt option #5698 2026-06-27 12:35:04 +00:00
prompt_test.go OIDC: Add configurable authorization prompt option #5698 2026-06-27 12:35:04 +00:00
README.md OIDC: Send and validate a nonce on authorization requests #5695 2026-06-25 16:54:29 +02:00
redirect_url.go OIDC: Scope the RP state cookie to the instance OIDC base path 2026-06-09 15:36:35 +00:00
redirect_url_test.go OIDC: Scope the RP state cookie to the instance OIDC base path 2026-06-09 15:36:35 +00:00
register.go Auth: Add RP-initiated OIDC logout via PHOTOPRISM_OIDC_LOGOUT #5684 #5433 2026-06-24 17:21:00 +02:00
register_test.go Auth: Add RP-initiated OIDC logout via PHOTOPRISM_OIDC_LOGOUT #5684 #5433 2026-06-24 17:21:00 +02:00
username.go OIDC: Add "address" to the default userinfo request scopes #782 2024-07-09 06:55:06 +02:00
username_test.go Tests: Add unit tests #782 2024-07-16 10:34:15 +02:00

PhotoPrism — OIDC Integration

Last Updated: June 25, 2026

Overview

internal/auth/oidc implements PhotoPrisms OpenID Connect (OIDC) Relying Party (RP) flow so users can sign in with thirdparty identity providers. The package wraps the zitadel/oidc client to perform discovery, build the RP, redirect users to the provider, exchange codes for tokens, and retrieve profile claims in a predictable, testable way.

Constraints

  • Relies on the providers /.well-known/openid-configuration for discovery and enforces https unless explicitly allowed via insecure.
  • Uses random per-session cookie keys (16byte hash + encrypt) and the shared HTTP client defined in http_client.go.
  • PKCE is enabled automatically when the provider advertises S256.
  • Scopes default to authn.OidcRequiredScopes when none are supplied; scopes are cleaned via clean.Scopes.
  • Token exchange uses the providers userinfo endpoint by default; errors are surfaced via Gin response headers (oidc_error) and audit logs.

Goals

  • Provide a consistent RP client that can be reused by CLI, server routes, and tests.
  • Keep redirect and codeexchange handlers minimal while ensuring audit visibility and secure defaults.
  • Allow editions (CE/Pro) to extend claim processing (e.g., groups, roles) without duplicating RP wiring.

Non-Goals

  • Managing upstream identity provider configuration or enrollment.
  • Implementing a full OIDC Provider; PhotoPrism acts only as a Relying Party.
  • Handling every custom claim set; extension hooks should live beside claim parsing code.

Package Layout (Code Map)

  • oidc.go — package doc + logger.
  • client.go — RP construction (NewClient), PKCE detection, auth redirect, code exchange + userinfo retrieval.
  • nonce.go — per-request Nonce generation and tolerant CheckNonce ID-token validation; tests in nonce_test.go.
  • logout.go(*Client).EndSessionURL builds the RP-initiated logout URL (id_token_hint, post_logout_redirect_uri, client_id, state) for a browser redirect; tests in logout_test.go.
  • http_client.go — shared HTTP client with TLS toggle and timeouts; helpers for tests in http_client_test.go.
  • redirect_url.go — builds the redirect/callback URL from site config.
  • register.go — provider registration glue; tests in register_test.go.
  • username.go — derives usernames from claims; tests in username_test.go.
  • client_test.go, oidc_test.go — happy-path and error-path coverage for discovery, auth URL, and code exchange.

Related Packages & Entry Points

  • internal/server/routes.go registers the OIDC auth and callback endpoints.
  • pkg/authn defines required scopes and shared auth helpers.
  • internal/auth/acl and private extension LDAP packages (pro/internal/auth/ldap, portal/internal/auth/ldap) handle role/group mapping; the planned OIDC group parsing will mirror this logic.
  • internal/config provides OIDC options/flags (issuer, client ID/secret, scopes, insecure).
  • internal/event supplies the logger used for audit and error reporting.

Configuration & Safety

  • Enforce https for issuers unless insecure is explicitly set (intended for dev/test).
  • Cookie handler is created per client with fresh random keys to avoid reuse across restarts.
  • Audit every provider/redirect/token error with sanitized messages; avoid logging secrets.
  • Prefer explicit scopes from configuration; defaults request only the minimal set.

Nonce Handling

  • AuthURLHandler generates a unique nonce per authorization request, stores it in the signed and encrypted RP cookie (same scope as state/PKCE), and sends it on the authorization request so the provider reflects it back in the ID token.
  • CodeExchangeUserInfo validates the ID token's nonce claim against the cookie value via CheckNonce. Validation is tolerant: a token that echoes the nonce must match, but a provider that omits the nonce on a session-resumed token (e.g. AWS Cognito) is accepted so logins do not regress.
  • The library's strict nonce verifier is disabled with rp.WithNonce(nil) because its default expects an empty nonce and would reject every echoed value; PhotoPrism owns the check instead.
  • Without an explicit nonce, Cognito auto-generates one on the first interactive login from a fresh browser, which the default verifier rejects with nonce does not match; sending our own nonce makes the round-trip spec-clean.

RP-Initiated Logout

  • The provider's end_session_endpoint is captured automatically from discovery by the wrapped zitadel/oidc RP and read via GetEndSessionEndpoint(); no separate config is needed.
  • (*Client).EndSessionURL constructs the redirect URL for the browser (it does not call the endpoint server-side, unlike rp.EndSession, because only the browser carries the provider's SSO cookie). It returns an empty string when the provider advertises no end-session endpoint, so callers fall back to a local-only logout.
  • The behavior is gated by PHOTOPRISM_OIDC_LOGOUT (default off) and enforced at sign-out in internal/api/session_delete.go.

Security Group Extension for Entra ID

The following features are supported by the current implementation:

  • Reads security groups from the groups claim in ID or access tokens; accepts GUIDs or names (case-insensitive, sanitized via NormalizeGroupID).
  • Optional required membership: --oidc-group (or PHOTOPRISM_OIDC_GROUP) lists one or more groups that must be present; login is rejected if none match. If the token signals overage via _claim_names.groups and contains no groups, login is denied with an audit entry explaining that membership could not be validated.
  • Group-to-role mapping: --oidc-group-role (GROUP=ROLE, repeatable) assigns the first matching role; falls back to --oidc-role (default guest) when no mapping matches.
  • Keeps app/directory roles (roles, wids) separate from security groups to avoid accidental privilege escalation.
  • Claim name is configurable via --oidc-group-claim (default groups).
  • On Portal builds, persists the merged, normalized group set from the ID token and userinfo on the login session (entity.SessionData.Groups), so the Portal can evaluate group-based cluster access at authorize time without another IdP round-trip. Instance and CE sessions never store groups, and the session API responses redact the field (SessionData.Redacted), so group membership is not exposed to clients.

Configuration Options

  • --oidc-group-claim / PHOTOPRISM_OIDC_GROUP_CLAIM: claim to read (default groups).
  • --oidc-group / PHOTOPRISM_OIDC_GROUP: comma- or multi-flag list of groups required for login (IDs or names accepted, normalized to lowercase alphanumerics/hyphen/underscore).
  • --oidc-group-role / PHOTOPRISM_OIDC_GROUP_ROLE: mapping GROUP=ROLE (roles: admin|manager|user|contributor|viewer|guest|none). First match wins.
  • --oidc-role / PHOTOPRISM_OIDC_ROLE: fallback role if no group mapping matches (defaults to guest).

Integration Guide for Entra ID

  1. Register an app in Microsoft Entra ID (v2) or reuse your existing PhotoPrism registration. Note the instance ID and the application (client) ID.
  2. Redirect URI: add https://{hostname}/api/v1/oidc/redirect.
  3. Token configuration → Add optional claimToken type = ID (and Access if you prefer) → Groups → choose Security groups.
  4. Under “Emit groups as”, pick Group name (cloud-only) or sAMAccountName / DNSDomainName\sAMAccountName for synced AD; this makes tokens carry human-friendly names instead of GUIDs.
  5. If you keep Group ID, leave PhotoPrism config in GUID mode; if you emit names, set PHOTOPRISM_OIDC_GROUP / PHOTOPRISM_OIDC_GROUP_ROLE to those names (lowercase in config for consistency). When Microsoft signals group overage (too many groups to fit in the token), it sets _claim_names.groups and may omit groups entirely; PhotoPrism will currently block login if required groups are configured and no groups are present.
  6. Grant admin consent for the chosen scopes (at minimum openid profile email, plus offline_access if you need refresh tokens).
  7. Configure PhotoPrism (example .env-oidc with placeholder secrets):
    PHOTOPRISM_OIDC_URI="https://login.microsoftonline.com/f8b10857-a7f2-49ba-b73c-6f619715f574/v2.0"
    PHOTOPRISM_OIDC_CLIENT="11111111-2222-3333-4444-555555555555"
    PHOTOPRISM_OIDC_SECRET="asecure-random-oidc-client-secret"
    PHOTOPRISM_OIDC_GROUP_CLAIM="groups"
    PHOTOPRISM_OIDC_GROUP="photoprism-admins, photoprism-users"        # names or GUIDs
    PHOTOPRISM_OIDC_GROUP_ROLE="photoprism-admins=admin, photoprism-users=user"
    
  8. Restart PhotoPrism; on login the service will:
    • Read groups from ID token, then fall back to userinfo if absent.
    • Deny login if required groups are configured but none are present (and overage is signaled).
    • Apply the first matching group→role mapping; otherwise assign the fallback role.

Please note:

  • Entra ID security groups are only supported in PhotoPrism® Pro.
  • If tokens still contain GUIDs, revisit Token configuration → Groups and change “Emit groups as” to a name format; reissue tokens by signing out/in. Names must be unique in your instance for deterministic mapping.
  • Overage: when the _claim_names.groups marker is present and no groups are in the token, PhotoPrism cannot validate membership and will block login if oidc-group is set. (Graph-based resolution is described in the next section but is not yet implemented.)
  • For mixed environments, you can supply both names and GUIDs in oidc-group / oidc-group-role; all entries are normalized and deduplicated.

Entra App Roles

As an alternative to security groups, we may use Microsoft/Entra App Roles to provide a more business-friendly option if needed:

  • To implement this, PhotoPrism must read the roles claim, normalize it as with groups, and allow mapping by adding a new flag (e.g., --oidc-role-claim=roles or --oidc-app-role=ROLE=photoprismRole).
  • This would require an estimated 80150 lines of code (LOC), including wiring and tests, without introducing new dependencies.
  • Once this feature is available, Entra admins can create app roles (e.g., admin or viewer) and assign them to users or groups in Entra.
  • PhotoPrism would then receive readable role strings in tokens, eliminating the need to rely on security group names or GUIDs.

Microsoft Graph API

Support for the Microsoft Graph API is required to translate Entra security group GUIDs to display names and to fetch full membership lists when tokens omit groups:

  • Resolve GUID → display name so --oidc-group / --oidc-group-role can use human-friendly group names while still matching IDs.
  • Fetch memberships via Microsoft Graph when _claim_names.groups signals overage or when the token only carries IDs.
  • Deduplicate and merge token groups with Graph results; continue to fall back gracefully if Graph is unavailable.

Implementation outline:

  • Config: add flags/options such as oidc-graph-lookup (enable), oidc-graph-timeout (default ~35s), oidc-graph-mode (client for Client Credentials, obo for On-Behalf-Of), and optional scope override (default https://graph.microsoft.com/.default). Surface in flags, reports, and options.yml.
  • Token acquisition:
    • Client Credentials flow using the existing OIDC client ID/secret against the instance token endpoint with Graph scope; requires admin-consented Application permission Group.Read.All.
    • On-Behalf-Of flow exchanging the user access token plus the same secret; requires Delegated Group.Read.All consent.
  • Graph calls:
    • Prefer a single batch or /v1.0/me/transitiveMemberOf?$select=id,displayName to retrieve security groups; filter to @odata.type that ends with group.
    • Optionally fall back to /v1.0/groups/{id}?$select=id,displayName when only a few IDs need resolution.
  • Processing: normalize both id and displayName, cache GUID→name mappings with a short TTL, merge into the existing group set, then apply required-group and group→role mapping logic.
  • Testing: add httptest fixtures for token exchange and Graph responses, covering timeouts, 401/403, and partial data.

Impact:

  • Allows administrators to configure PhotoPrism with recognizable group names instead of GUIDs.
  • Makes log/debug output more readable and reduces reliance on Azure portal lookups for GUIDs.
  • Provides a path to honor group-based access when tokens exceed size limits and omit groups by default.

Documentation & References

Operational Tips

  • Always call RedirectURL(siteUrl) to build callbacks that respect reverse proxies and base URIs.
  • Reuse HttpClient(insecure) so timeouts and TLS settings stay consistent.
  • When adding claims processing, keep parsing isolated (e.g., new helper) and ensure failures do not block signin unless required.

Test Guidelines

  • Unit tests: go test ./internal/auth/oidc -count=1
  • Tests cover discovery failures, PKCE detection, redirect URL construction, username extraction, and codeexchange error handling.
  • For integration testing with a real IdP, set OIDC env vars in compose.local.yaml, start the dev server, and exercise /auth/oidc + callback.