feat(auth): generic OIDC (Entra ID) with group-based RBAC - #14
Open
tanguyfalconnet wants to merge 4 commits into
Open
tanguyfalconnet wants to merge 4 commits into
tanguyfalconnet wants to merge 4 commits into
Conversation
The PR branch was cut before teams, countries and events landed. Merging main in compiles and passes, but nothing covered the place where the two halves actually meet: group-based roles deciding the new write paths. - add cmd/server/rbac_test.go — a fake OIDC provider (discovery + JWKS) and real signed tokens, exercising rbacMiddleware end to end: events writable by any authorized member, teams and holidays still admin-only (now by group), and an identity outside AUTH_ALLOWED_GROUPS kept read-only - SSO-README: the roles table predated events; state that /events is the one write open to every authorized account, and why - README: untouched by the PR, so it still documented the Dex-only flow — document the provider-agnostic login and the six new AUTH_* variables - CLAUDE.md: same, plus the default-deny ordering the events rule depends on Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
staticcheck QF1008: rsa.PrivateKey embeds rsa.PublicKey, so key.N and key.E name the same fields as key.PublicKey.N / key.PublicKey.E. The report quoted only the modulus line; the exponent one line below had the same defect and is fixed too, otherwise CI would have gone red again on the next run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The built-in SSO only worked with the bundled Dex setup running on localhost, which made Offly impossible to deploy behind a real identity provider. This PR turns it into a generic OIDC integration, validated against Microsoft Entra ID, and adds group-based RBAC. It also fixes a few security issues in the token verification and login flow.
Why it did not work outside local Dex
redirect_uri(frontend andcallback.go) and the post-login redirect were hardcoded tohttp://localhost:8080/http://localhost:3000.<issuer>/auth,/token,/keys) instead of using OIDC discovery. Entra ID uses/oauth2/v2.0/authorize,/oauth2/v2.0/tokenand/discovery/v2.0/keys.groupsscope was always requested, and Entra ID rejects it (AADSTS70011).groupsclaim was extracted but never used.Changes
OIDC, provider-agnostic
<issuer>/.well-known/openid-configuration, with optional overrides (AUTH_AUTHORIZATION_URL,AUTH_TOKEN_URL,AUTH_JWKS_URL). If discovery is unavailable, the Dex layout is used as before.AUTH_REDIRECT_URL,AUTH_POST_LOGIN_REDIRECT_URL,AUTH_SCOPES,AUTH_GROUPS_CLAIM. Defaults match the current local Dex setup.email, then frompreferred_username/upn. By default, Entra ID only emitsemailwhen the optional claim is configured.Group-based RBAC
AUTH_ADMIN_GROUPSgrants the admin role (Entra ID: group object IDs).AUTH_ADMIN_GROUP, as documented in the previous README, is accepted as an alias.AUTH_ADMIN_EMAILSstill works.AUTH_ALLOWED_GROUPSrestricts who can log in: users in none of these groups get a 403. Admins are always allowed.AUTH_GROUPS_CLAIM=rolesuses Entra ID app roles instead of groups._claim_names, more than 200 groups) is detected and logged.Security fixes
aud. Entra ID sendsaudas a string, and the code only checked the array form, then fell back toazp, which is absent from v2 ID tokens. As a result, an ID token issued for any other application of the same tenant was accepted. Verification now usesjwt.WithAudienceand covers both forms.state,nonceand PKCE (S256). The login is now started by the backend (GET /api/v1/auth/login), so the frontend no longer builds provider URLs.noneconfusion.expis now required, with a 30s leeway./api/v1/auth/configis now JSON-encoded instead of built by string concatenation.Helm chart
auth:section. The client secret comes fromexistingSecret, or from an inline value for which the chart creates the Secret.extraEnv.auth.enabled: false, so existing installs are unaffected.Docs
SSO-README.mdis rewritten. It now describes the actual confidential flow; the previous version documented a browser-side PKCE flow that the code did not implement.emailclaim, group assignment), the full variable reference, and a troubleshooting table.Type of change
Behaviour changes to be aware of (not breaking for a standard setup):
auddoes not containAUTH_CLIENT_IDare now rejected. This was the intended behaviour, and Dex already emits the right audience.expare rejected.AUTH_CLIENT_IDis unset, its default is nowofflyinstead ofwirety, which matches the bundled Dex config.How Has This Been Tested?
backend/internal/auth:aud, arrayaud, foreign audience rejected (regression test for the bug above), wrong issuer, expired or missingexp, HS256 rejected, nonce check;AUTH_ADMIN_GROUPalias, allowed groups, admin bypass.httptest) issuing Entra-style ID tokens: the authorization request carries state, nonce and an S256 challenge; the callback checks the code exchange (client secret, PKCE verifier, redirect URI), sets the session cookie, provisions the user and redirects. The test also covers rejection of a forgedstate, a replayednonceand a user outside the allowed groups.go test ./...,go vetandgolangci-lint run(0 issues).tsc --noEmitandeslint.helm lint. With auth disabled, noAUTH_*variable is rendered. With Entra values (existingSecret), the env vars are correct, and the trailing/ofpublicUrlis handled. The Secret is created only for an inlineclientSecret, and rendering fails with a clear message when neither secret option is set.To reproduce with Entra ID, follow the Microsoft Entra ID section of
SSO-README.mdand set the variables shown there.Checklist:
🤖 Generated with Claude Code