Skip to content

feat(auth): add generic OAuth2 / OIDC (Authentik) dashboard SSO - #1

Open
thewillwilson wants to merge 1 commit into
developervariety:mainfrom
thewillwilson:feat/oauth2-authentik
Open

feat(auth): add generic OAuth2 / OIDC (Authentik) dashboard SSO#1
thewillwilson wants to merge 1 commit into
developervariety:mainfrom
thewillwilson:feat/oauth2-authentik

Conversation

@thewillwilson

Copy link
Copy Markdown

Summary

Adds generic OAuth2 / OIDC single sign-on for the Armada web dashboard, so users can log in via Authentik (or any OAuth2/OIDC provider) instead of only email/password or a pasted API key.

Because Armada's REST layer runs on Watson Webserver, not ASP.NET Core, turnkey OIDC middleware doesn't apply — this is a hand-rolled Authorization-Code + PKCE flow implemented as Watson routes. On success it mints the existing AES-encrypted session token (SessionTokenService), so the entire downstream auth stack (AuthenticationService, whoami, the SPA's X-Token flow) is untouched.

Flow

  1. Dashboard calls GET /api/v1/auth/oauth/config → shows a "Sign in with <DisplayName>" button when SSO is enabled.
  2. Button navigates to GET /api/v1/auth/oauth/authorize → 302 to the provider (state + PKCE S256).
  3. Provider redirects to GET /api/v1/auth/oauth/callback → code→token→userinfo exchange, resolve/provision user, 302 to /dashboard#oauth_token=<session-token>.
  4. The SPA consumes the token from the URL fragment and strips it from history.

What's included

Backend (Armada.Core / Armada.Server)

  • OAuth2Settings (nested in ArmadaSettings): endpoints, client id/secret, scopes, PKCE, claim mapping, RequireVerifiedEmail, AllowAutoProvision, DefaultTenantId.
  • OAuth2Service + IOAuth2Service: PKCE (RFC 7636), single-use CSRF state store, token/userinfo exchange, email-based user resolve/provision, session-token minting.
  • OAuthRoutes: config / authorize / callback (all NoAuthRequired); wired into ArmadaServer.

Dashboard (React)

  • SSO button + config discovery in LoginFlow.tsx; #oauth_token / #oauth_error fragment handling in AuthContext.tsx (reuses the existing login() path).

Config (Authentik example) — see docs/REST_API.md:

"OAuth2": {
  "Enabled": true,
  "DisplayName": "Authentik",
  "AuthorizationEndpoint": "https://authentik.example.com/application/o/authorize/",
  "TokenEndpoint": "https://authentik.example.com/application/o/token/",
  "UserInfoEndpoint": "https://authentik.example.com/application/o/userinfo/",
  "ClientId": "armada",
  "ClientSecret": "<client-secret>",
  "RedirectUri": "https://armada.example.com/api/v1/auth/oauth/callback"
}

Security review (self-reviewed; fixes applied in this PR)

  • Account-takeover via unverified email → fixed: the provider's email_verified claim must be true before the email is trusted for identity mapping (RequireVerifiedEmail, default on).
  • Session token in audit store → fixed: the callback's token is never echoed into the response body, and the callback route is excluded from request-history capture (redaction is key-name based and would otherwise persist the Location fragment verbatim).
  • Login-CSRF (state not bound to the initiating browser): documented as a known limitation with a follow-up to add a SameSite/HttpOnly state cookie. State is still single-use, 256-bit, and expiring.

Tests

  • Unit: OAuth2ServiceTests — PKCE RFC vector, state single-use/unknown, settings gating, authorize-URL params, user resolve + auto-provision + rejection paths.
  • Automated HTTP: OAuthApiTestsconfig returns disabled by default; authorize 302s with an error when disabled.

Scope / notes

  • Dashboard login only. The pre-existing no-auth gaps on the MCP server and WebSocket endpoint are out of scope for this PR.
  • No DB schema change — SSO users are matched/provisioned by email; provisioned users get a random, unusable password.
  • ⚠️ Build not run in CI here: the authoring environment had no .NET/Node SDK, so dotnet build/test suites and the dashboard tsc build were not executed locally. Please run the standard build + test/Armada.Test.Unit / Armada.Test.Automated before merge.

🤖 Generated with Claude Code

Adds a redirect-based Authorization-Code + PKCE single sign-on flow for
the web dashboard. Because the REST layer is Watson (not ASP.NET Core),
the flow is implemented by hand and, on success, mints the existing
AES-encrypted session token so the rest of the auth stack is unchanged.

Backend:
- OAuth2Settings (nested in ArmadaSettings): endpoints, client id/secret,
  scopes, PKCE, claim mapping, auto-provision, tenant, verified-email gate.
- OAuth2Service + IOAuth2Service: PKCE (RFC 7636), single-use CSRF state
  store, code->token->userinfo exchange, email-based user resolve/provision
  into DefaultTenantId, session-token minting.
- OAuthRoutes: GET /api/v1/auth/oauth/{config,authorize,callback}
  (NoAuthRequired); callback redirects to /dashboard#oauth_token=...
- Wired into ArmadaServer; AuthorizationConfig marks the routes public.

Dashboard:
- "Sign in with <provider>" button, config discovery, and #oauth_token /
  #oauth_error fragment handling (token stripped from history on consume).

Security hardening (from self-review):
- Require the provider's email_verified claim before trusting email for
  identity mapping (prevents takeover via unverified email).
- Never persist the callback's session token: empty redirect body and the
  callback route is excluded from request-history capture.
- State store documents the residual login-CSRF (browser-binding) follow-up.

Tests: OAuth2Service unit suite (PKCE vector, state single-use, settings
gating, authorize URL, user resolve/provision) and an automated HTTP suite
for the config/authorize endpoints.

Docs: docs/REST_API.md SSO section, settings table, Authentik example.

Note: .NET/Node toolchains are unavailable in the authoring environment,
so the solution build and test suites were not executed locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewillwilson

Copy link
Copy Markdown
Author

✅ Built & tested (Docker, mcr.microsoft.com/dotnet/sdk:10.0)

Updating the earlier "not built locally" caveat — I ran the build and tests in the project's own SDK image:

  • dotnet build src/Armada.sln -c Debug0 errors (130 pre-existing warnings, none in the new OAuth2 files).
  • Unit suite (Armada.Test.Unit, net10.0)3109/3112 pass. The new OAuth2Service suite is 13/13 green (PKCE RFC-7636 vector, state single-use, settings gating, authorize-URL params, user resolve + auto-provision + rejection paths). The 3 unrelated failures are environmental from running as root in a container (broken-pipe on process spawn; root bypasses chmod read-only; one timing-sensitive dispatch test) — none touch OAuth or the request-history path.
  • Automated HTTP suite (Armada.Test.Automated) → the new OAuth API Tests are 2/2 green, including OAuthAuthorize_WhenDisabled_RedirectsWithError, which confirms the Watson ApiRequest 302-redirect (status + Location) behaves correctly end-to-end.

The dashboard tsc/Vite build was not run (no Node toolchain), but the TS changes are small and follow existing patterns.

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