|
|
||
|---|---|---|
| .. | ||
| client.go | ||
| client_test.go | ||
| groups.go | ||
| groups_test.go | ||
| http_client.go | ||
| http_client_test.go | ||
| oidc.go | ||
| oidc_test.go | ||
| README.md | ||
| redirect_url.go | ||
| register.go | ||
| register_test.go | ||
| username.go | ||
| username_test.go | ||
PhotoPrism — OIDC Integration
Last Updated: November 27, 2025
Overview
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.
Context & Constraints
- Relies on the provider’s
/.well-known/openid-configurationfor discovery and enforceshttpsunless explicitly allowed viainsecure. - Uses random per-session cookie keys (16‑byte 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.OidcRequiredScopeswhen none are supplied; scopes are cleaned viaclean.Scopes. - Token exchange uses the provider’s 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 code‑exchange 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.http_client.go— shared HTTP client with TLS toggle and timeouts; helpers for tests inhttp_client_test.go.redirect_url.go— builds the redirect/callback URL from site config.register.go— provider registration glue; tests inregister_test.go.username.go— derives usernames from claims; tests inusername_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.goregisters the OIDC auth and callback endpoints.pkg/authndefines required scopes and shared auth helpers.internal/auth/acland (Pro)pro/internal/auth/ldaphandle role/group mapping; the planned OIDC group parsing will mirror this logic.internal/configprovides OIDC options/flags (issuer, client ID/secret, scopes, insecure).internal/eventsupplies the logger used for audit and error reporting.
Configuration & Safety
- Enforce
httpsfor issuers unlessinsecureis 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.
Security Group Extension for Entra ID
The following features are supported by the current implementation:
- Reads security groups from the
groupsclaim in ID or access tokens; accepts GUIDs or names (case-insensitive, sanitized viaNormalizeGroupID). - Optional required membership:
--oidc-group(orPHOTOPRISM_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.groupsand 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(defaultguest) 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(defaultgroups).
Configuration Options
--oidc-group-claim/PHOTOPRISM_OIDC_GROUP_CLAIM: claim to read (defaultgroups).--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: mappingGROUP=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 toguest).
Integration Guide for Entra ID
- Register an app in Microsoft Entra ID (v2) or reuse your existing PhotoPrism registration. Note the tenant ID and the application (client) ID.
- Redirect URI: add
https://{hostname}/api/v1/oidc/redirect. - Token configuration → Add optional claim → Token type = ID (and Access if you prefer) → Groups → choose Security groups.
- 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.
- If you keep Group ID, leave PhotoPrism config in GUID mode; if you emit names, set
PHOTOPRISM_OIDC_GROUP/PHOTOPRISM_OIDC_GROUP_ROLEto those names (lowercase in config for consistency). When Microsoft signals group overage (too many groups to fit in the token), it sets_claim_names.groupsand may omit groups entirely; PhotoPrism will currently block login if required groups are configured and no groups are present. - Grant admin consent for the chosen scopes (at minimum
openid profile email, plusoffline_accessif you need refresh tokens). - Configure PhotoPrism (example
.env-oidcwith 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" - 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 tenant for deterministic mapping.
- Overage: when the
_claim_names.groupsmarker is present and no groups are in the token, PhotoPrism cannot validate membership and will block login ifoidc-groupis 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
rolesclaim, normalize it as with groups, and allow mapping by adding a new flag (e.g.,--oidc-role-claim=rolesor--oidc-app-role=ROLE=photoprismRole). - This would require an estimated 80–150 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.,
adminorviewer) 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-rolecan use human-friendly group names while still matching IDs. - Fetch memberships via Microsoft Graph when
_claim_names.groupssignals 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 ~3–5s),oidc-graph-mode(clientfor Client Credentials,obofor On-Behalf-Of), and optional scope override (defaulthttps://graph.microsoft.com/.default). Surface in flags, reports, andoptions.yml. - Token acquisition:
- Client Credentials flow using the existing OIDC client ID/secret against the tenant 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.Allconsent.
- Client Credentials flow using the existing OIDC client ID/secret against the tenant token endpoint with Graph scope; requires admin-consented Application permission
- Graph calls:
- Prefer a single batch or
/v1.0/me/transitiveMemberOf?$select=id,displayNameto retrieve security groups; filter to@odata.typethat ends withgroup. - Optionally fall back to
/v1.0/groups/{id}?$select=id,displayNamewhen only a few IDs need resolution.
- Prefer a single batch or
- Processing: normalize both
idanddisplayName, 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
- Microsoft Entra ID: https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id
- Entra group claims: https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference#groups-claim
- Entra app roles: https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-app-roles-in-apps
- Group overage handling: https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-app-roles-in-azure-ad-apps#group-overage-and-_claim_names
- Token customization guidance: https://learn.microsoft.com/en-us/entra/architecture/customize-tokens
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 sign‑in 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 code‑exchange 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.