internal/auth/oidc/README.md
Last Updated: February 22, 2026
internal/auth/oidc implements PhotoPrism’s OpenID Connect (OIDC) Relying Party (RP) flow so users can sign in with third‑party 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.
/.well-known/openid-configuration for discovery and enforces https unless explicitly allowed via insecure.http_client.go.S256.authn.OidcRequiredScopes when none are supplied; scopes are cleaned via clean.Scopes.oidc_error) and audit logs.oidc.go — package doc + logger.client.go — RP construction (NewClient), PKCE detection, auth redirect, code exchange + userinfo retrieval.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.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.https for issuers unless insecure is explicitly set (intended for dev/test).The following features are supported by the current implementation:
groups claim in ID or access tokens; accepts GUIDs or names (case-insensitive, sanitized via NormalizeGroupID).--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.--oidc-group-role (GROUP=ROLE, repeatable) assigns the first matching role; falls back to --oidc-role (default guest) when no mapping matches.roles, wids) separate from security groups to avoid accidental privilege escalation.--oidc-group-claim (default groups).--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).https://{hostname}/api/v1/oidc/redirect.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.openid profile email, plus offline_access if you need refresh tokens)..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"
Please note:
_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.)oidc-group / oidc-group-role; all entries are normalized and deduplicated.As an alternative to security groups, we may use Microsoft/Entra App Roles to provide a more business-friendly option if needed:
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).admin or viewer) and assign them to users or groups in Entra.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:
--oidc-group / --oidc-group-role can use human-friendly group names while still matching IDs._claim_names.groups signals overage or when the token only carries IDs.Implementation outline:
oidc-graph-lookup (enable), oidc-graph-timeout (default ~3–5s), 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.Group.Read.All.Group.Read.All consent./v1.0/me/transitiveMemberOf?$select=id,displayName to retrieve security groups; filter to @odata.type that ends with group./v1.0/groups/{id}?$select=id,displayName when only a few IDs need resolution.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.Impact:
RedirectURL(siteUrl) to build callbacks that respect reverse proxies and base URIs.HttpClient(insecure) so timeouts and TLS settings stay consistent.go test ./internal/auth/oidc -count=1compose.local.yaml, start the dev server, and exercise /auth/oidc + callback.