Skip to content

Add OIDC auth support#279

Open
codexlynx wants to merge 2 commits intobrian7704:masterfrom
codexlynx:add-oidc-support
Open

Add OIDC auth support#279
codexlynx wants to merge 2 commits intobrian7704:masterfrom
codexlynx:add-oidc-support

Conversation

@codexlynx
Copy link
Copy Markdown

Summary

This PR adds the first OpenID Connect (OIDC) implementation to OpenTAKServer for browser-based single sign-on.

It introduces OIDC login/callback endpoints, local user synchronization, configurable claim mapping, PKCE support, and a conservative default configuration so existing deployments remain unaffected unless OIDC is explicitly
enabled.

What this adds

  • GET /api/oidc/login
  • GET /api/oidc/callback
  • OIDC provider configuration through either:
    • discovery metadata (OTS_OIDC_METADATA_URL), or
    • explicit authorization/token/userinfo endpoints
  • local user binding based on stable OIDC identity:
    • issuer + subject
  • configurable username, email, and role claim mapping
  • PKCE support
    • automatic S256 for public clients when no client secret is configured
    • optional S256 PKCE for confidential clients as well
  • hardened callback handling and non-cacheable callback responses
  • optional JSON callback mode for browser/API integrations

Security / identity behavior

OIDC users are linked to local accounts by issuer + subject, not by email.

This avoids relying on mutable claims for identity association and gives OpenTAKServer a stable external identity to bind to.

The callback flow also:

  • handles provider error responses explicitly
  • rejects invalid callbacks missing an authorization code
  • returns no-store response headers

Configuration / compatibility notes

  • OIDC is disabled by default
  • OpenTAKServer still starts and works normally without any OIDC configuration
  • existing local authentication continues to work
  • OIDC is opt-in and only active when OTS_ENABLE_OIDC is enabled

Implementation notes

The implementation uses flask-oidc internally while keeping OpenTAKServer-owned routes so OTS can continue to control:

  • local user creation/synchronization
  • local role mapping
  • callback response behavior

Testing

OIDC tests were unified into:

  • tests/test_oidc_api.py

Documentation

brian7704/OpenTAKServer-docs#18

@brian7704
Copy link
Copy Markdown
Owner

Thanks for this PR. What OIDC provider is good to test with? Also how can I control which users have the administrator role vs the user role?

@brian7704
Copy link
Copy Markdown
Owner

Another thing I noticed is that SECURITY_USER_IDENTITY_ATTRIBUTES isn't set to oidc when OIDC is enabled. The web UI uses SECURITY_USER_IDENTITY_ATTRIBUTES to know if email registration is enabled and if the server uses LDAP auth. If the server uses LDAP auth, the web UI will login using /api/ldap_login instead of /api/login. So SECURITY_USER_IDENTITY_ATTRIBUTES will have to be set to oidc in order for the web UI to know to use your /api/oidc/login endpoint.

See this line as an example of how LDAP does it.

The web UI should also be modified to use /api/oidc/login when OIDC is enabled on the server.

@codexlynx
Copy link
Copy Markdown
Author

Thanks for the detailed feedback @brian7704. it was very helpful.

I’ve now revalidated the flow end to end against a real Authelia setup, and that worked well, so I’d recommend Authelia as a good provider to test with. It was straightforward to run locally, supports discovery metadata and
PKCE, and exposes groups cleanly for role mapping.

For administrator vs user, the mapping is controlled through the OIDC claim settings:

  • OTS_OIDC_ROLE_CLAIM selects which claim OTS reads for roles/groups
    • default: groups
  • OTS_OIDC_ADMIN_ROLES is a comma-separated list of IdP roles/groups that should map to the local administrator role
  • OTS_OIDC_DEFAULT_ROLES is used when no role claim is present; default is user

For example:

  OTS_OIDC_ROLE_CLAIM=groups                                                                                                                                                                                                      
  OTS_OIDC_ADMIN_ROLES=ots-admin                                                                                                                                                                                                  
  OTS_OIDC_DEFAULT_ROLES=user                                                                                                                                                                                                      

With that setup:

  • a user with groups=["ots-admin"] gets the local administrator role
  • a user with no role claim falls back to the local user role

One nuance is that if the provider does send explicit groups/roles, those are synced on login. So for non-admin users you either want no role claim at all, so they fall back to OTS_OIDC_DEFAULT_ROLES=user, or you want user
explicitly included in the claim if that is the behavior you want.

I also made a few follow-up changes after the initial PR:

  • fixed the browser callback issue when SESSION_COOKIE_SAMESITE=strict by relaxing it to Lax when OIDC is enabled
  • removed provider-issued OIDC token storage from the browser session cookie
  • stopped forcing client_secret_post and added OTS_OIDC_TOKEN_ENDPOINT_AUTH_METHOD so the token endpoint auth method can be configured if needed
  • while validating against Authelia, I found that it returns only minimal claims in the ID token and exposes preferred_username, email, and groups via the userinfo endpoint, so I added a merge/fallback there and test coverage
    for that case
  • added {"oidc": {}} to SECURITY_USER_IDENTITY_ATTRIBUTES when OIDC is enabled
  • updated the web UI to use /api/oidc/login when OIDC is enabled

On your last point: you were right about that integration.

In our original setup, we were using the reverse proxy in front of OTS to send the user into the correct OIDC flow, so I didn’t run into the missing SECURITY_USER_IDENTITY_ATTRIBUTES / web UI integration right away. Now that
OTS is handling that flow directly, it makes sense for OIDC to be advertised the same way LDAP already is so the UI can make the right choice itself.

At the moment, if both LDAP and OIDC are enabled, the UI prefers OIDC when oidc is present. That matches the original reverse-proxy-driven use case on our side

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants