Repository navigation
feat: GitHub OAuth authentication for the dashboard - #153
Open
sebykrueger wants to merge 2 commits into
Open
sebykrueger wants to merge 2 commits into
sebykrueger wants to merge 2 commits into
Conversation
Adds opt-in dashboard authentication via GitHub OAuth. When the auth block
is absent or auth.enabled is false, the dashboard runs unauthenticated as
before. When enabled, /api/* and / require a valid signed session cookie;
/metrics stays open for Prometheus scrapers.
Implementation
- internal/auth package (~470 LOC):
- session.go: HMAC-SHA256 signed cookie format
base64url(payload).base64url(hmac). Carries sub/email/iat/exp; no
server-side store. State token uses the same key with 10-min TTL for
CSRF protection on the OAuth callback.
- github.go: authorization code flow against GitHub OAuth, then
/user and /user/emails calls to extract login + verified primary
email.
- middleware.go: Allowlist (case-insensitive email and login matching)
plus an HTTP middleware that returns 401 application/json for /api/*
and 303 to /auth/login for everything else.
- handlers.go: /auth/login, /auth/callback, /auth/logout. Secure
cookie flag inferred from the redirect URL scheme.
- AuthConfig in internal/config/config.go with env-var support
(AUTH_ENABLED, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET, AUTH_REDIRECT_URL,
AUTH_SESSION_SECRET, AUTH_ALLOWED_USERS, AUTH_ALLOWED_EMAILS,
AUTH_SESSION_TTL_DAYS). ValidateAuth() refuses to start when auth is
enabled but the allowlist is empty.
- internal/api/server.go gains WithAuth(handlers, signer). Internal split
into protectedMux (gated routes: /api/*, /) and the outer mux that
carries /metrics and /auth/*.
- main.go --gen-session-secret flag prints a base64-encoded 32-byte
random secret suitable for AuthConfig.SessionSecret.
Tests
- Session sign/verify round-trip, tamper detection, expiry enforcement,
state-token round-trip and expiry, secret-too-short rejection.
- Allowlist case-insensitive matching for emails and logins, empty
allowlist denies all.
- Middleware decision matrix: missing cookie → 303 (browser) or 401 JSON
(api), invalid cookie → same, valid cookie → pass through.
- Config validation matrix: every required field, allowlist non-empty
invariant, user-only allowlist accepted.
Smoke test verified locally: --gen-session-secret produces 44-char
secret, /api/statistics returns 401 JSON, / redirects to /auth/login,
/auth/login redirects to github.com with signed state, /metrics stays
open, garbage cookies are rejected, state cookie is HttpOnly + SameSite=Lax.
…out page
Backend
- New /api/auth/me endpoint returns the current user as
{login, email, logout_url}. Behind the same auth middleware so
unauthenticated requests get the standard 401 JSON.
- Auth middleware now stores an *Identity (constructed from the
verified session) in the request context. UserFromContext() retrieves
it for handlers like /api/auth/me.
- Logout no longer redirects to /auth/login. That path immediately
bounced the user through GitHub and (because GitHub remembers prior
consent) silently re-authenticated them — making Logout look broken.
Logout now lands on a dedicated /auth/logged-out page with an
explicit "Sign in again" button, so the user has to actively re-auth.
Frontend
- src/lib/api.js: getCurrentUser() helper that calls /api/auth/me and
returns null on 401 so callers can render gracefully when auth is
disabled or the session expired.
- src/components/settings/SettingsModal.vue: an "Account" section in
the settings modal showing the GitHub @username and email, plus a
Logout button. Section is hidden when getCurrentUser returns null.
Tests
- Regression: TestMiddleware_PopulatesUserContext asserts that a valid
session causes UserFromContext to return a usable Identity. The
earlier "valid session passes through" test only checked that the
next handler was invoked, which silently masked a bug where
*Session was being stored in the context but UserFromContext
type-asserted to *Identity (always returning nil and breaking
/api/auth/me).
Collaborator
|
auth is not the focus of this repo the self-hosted setup in parse-dmarc allows team to quickly spin up the server without hassle nginx and other proxies handle auth properly and quite better |
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.
Summary
Adds opt-in GitHub OAuth login for the dashboard. When the
authblock is absent orauth.enabled=false, the dashboard runs unauthenticated as today (no breaking change). When enabled,/api/*and/require a signed session cookie obtained via GitHub OAuth;/metricsstays open so Prometheus scrapers keep working.internal/authpackage: HMAC-SHA256 signed cookie sessions (no server-side store), GitHub OAuth client with/user+/user/emailscalls to extract login + verified primary email, allowlist matching (case-insensitive emails OR usernames), and middleware that returns 303→/auth/loginfor browsers and 401 JSON for/api/*callers./auth/login,/auth/callback,/auth/logged-out,/auth/logouthandlers. Logout lands on a dedicated static page rather than redirecting back through GitHub — otherwise the user gets silently re-authenticated and Logout looks broken./api/auth/meendpoint returns{login, email, logout_url}for the current session, used by the SPA.--gen-session-secretCLI flag prints a 32-byte base64 secret.Why GitHub OAuth (not OIDC)?
GitHub doesn't implement OIDC (no discovery doc, no ID tokens, separate
/user/emailsAPI call to get the verified email). Treating it as a first-class provider rather than forcing it through a generic OIDC abstraction kept the code simpler and removes the need for users to stand up a federating IDP just to log in with their GitHub account. A future PR can add a generic OIDC provider alongside if there's demand for Google/Microsoft/Authentik/Keycloak.Config shape
ValidateAuth()refuses to start whenauth.enabled=trueand the allowlist is empty (otherwise nobody could log in). All fields also available as env vars (AUTH_ENABLED,AUTH_CLIENT_ID,AUTH_ALLOWED_USERS=a,b,c, etc.) for Docker/k8s secrets.Security notes
HttpOnly; SameSite=Lax;Secureflag inferred fromhttps://redirect URL). No server-side session store.stateis a separate signed token, validated against a short-lived state cookie scoped to/auth/callbackfor CSRF protection on the callback./metricsis intentionally not auth-gated so existing Prometheus scrapers don't break.session_ttl_days). To force-evict immediately, rotateauth.session_secret. Documented as a known gotcha in the README.Test plan
go test ./...— session sign/verify round-trip + tamper detection + expiry, state token round-trip + expiry, secret-too-short rejection, allowlist case-insensitive matching for emails and logins, empty allowlist denies all, middleware decision matrix (no cookie → 303 browser / 401 api, garbage cookie → same, valid cookie → pass through with populated context for/api/auth/me), full config validation matrix.go vet ./...clean.go build ./...clean for both pure-Go and CGO modes.http://localhost:8080: login flow, allowlist denial, logout flow with the dedicated landing page (verified you don't get silently re-authenticated through GitHub)./metricsstays open with auth enabled.authblock continue to run unauthenticated (AuthConfig{Enabled: false}is the zero value).