mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
674bead922
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
11 KiB
11 KiB
WIP: Go → Rust proxy outpost rewrite
Porting the authentik proxy outpost from Go (internal/outpost/proxyv2) to Rust
(src/outpost/proxy). The Rust side has all infrastructure working, but every request handler is
todo!() and none of the auth machinery exists yet. We continue in very small steps — each
step below is meant to be one focused, compilable, testable commit.
What's already done (Rust)
- Config loading + watch (
packages/ak-common/src/config), TLS cert store + self-signed fallback (packages/ak-common/src/tls), generated API client (ak-client). OutpostControllerbase + WebSocket event loop (src/outpost/mod.rs,src/outpost/event.rs).ProxyOutpost: provider refresh, SNI cert resolution, host→app lookup, HTTP/HTTPS startup (src/outpost/proxy/mod.rs).- Per-provider
Applicationwith router wiring; query-signature dispatcherhandle()is done (src/outpost/proxy/application/mod.rs,.../handlers/mod.rs). - Stack: axum 0.8, tokio, hyper-util, reqwest + reqwest-middleware, rustls/aws-lc-rs (FIPS).
What's stubbed / missing
todo!():handle_auth_callback,handle_sign_out,proxy::handle,forward::{caddy,envoy,nginx,traefik}.ProxyOutpost::end_sessionjust logs. No/outpost.goauthentik.io/startroute.- No crates/code for: session store (fs + postgres), signed cookies, OAuth2/OIDC client,
state JWT,
Claims+ header injection, bearer introspection + basic/client-credentials auth, TTL auth cache, allowlist regex, reverse-proxy forwarding, error pages.
Locked-in decisions
- Session store: filesystem first (JSON files in tempdir). Postgres
(
authentik_providers_proxy_proxysession, sqlx) deferred to a later step. - JWT:
jsonwebtokenwith itsaws_lc_rsfeature (noring; keep single FIPS backend). Verify the feature exists at the pinned version when adding it; fall back to a hand-rolled HS256 + aws-lc-rs verifier only if not. - Cookies: clean break —
axum-extraSignedCookieJar, signing key derived fromprovider.cookie_secret. Cookie carries only the opaque session ID; not byte-compatible with Go's gorillasecurecookie(one-time re-login on cutover, acceptable). - OIDC client: hand-rolled with
reqwest— form-POSTs for code exchange / introspection / client-credentials + a JWKS GET, mirroring the Go code. Noopenidconnect/oauth2crate (API already provides resolved endpoints; avoids discovery + browser/backchannel URL-rewrite friction). - Other crates:
moka(async) for the 60s auth-header TTL cache;regex(already a workspace dep) for the allowlist.
Key Go references (for parity)
application/oauth_state.go— state JWT (HS256, issgoauthentik.io/outpost/{client_id}, sid/state/redirect; no exp/aud).application/endpoint.go— OIDC endpoint resolution incl. embedded browser vs backchannel host rewriting.application/oauth_callback.go,oauth.go— auth start + callback + redirect validation (checkRedirectParam).application/auth.go,auth_bearer.go,auth_basic.go—checkAuthorder: session → cache → bearer → basic.application/mode_common.go—getHeaders(X-authentik-*), basic-auth-from-attributes,IsAllowlisted.application/mode_forward.go,mode_proxy.go— the four forward handlers + reverse-proxy data path.application/session.go— session options, backend selection,Logout/LogoutSessions.
Incremental steps (each = one focused commit)
Phase A — pure types & crypto primitives (unit-testable, no axum)
- A1.
Claims+ProxyClaimsserde types (mirrortypes/claims.go;groups/entitlementsdefault-emptyVec<String>,raw_token,ak_proxy). Round-trip JSON test. Done insrc/outpost/proxy/claims.rs(container-level#[serde(default)]; 3 tests). - A2. Add
jsonwebtoken(aws_lc_rsfeature).OAuthStatetype + HS256 encode/decode signed withcookie_secret; disable exp/aud validation, enforce issuer. Tests: round-trip + issuer-mismatch rejection. Done insrc/outpost/proxy/oauth_state.rs. Note:jsonwebtokenis v10.4.0 (v10 redesign; HS256 encode/decode/ValidationAPI matches v9). Added to workspace deps +proxyfeature (dep:jsonwebtoken). Clearedrequired_spec_claimsso theexp-less token decodes. 3 tests. - A3.
OidcEndpointstruct fromOpenIdConnectConfigurationmirroringendpoint.go(embedded/browser URL rewriting). URL-rewrite tests. Done insrc/outpost/proxy/endpoint.rsas a pureOidcEndpoint::new(oidc, authentik_host, host_browser, embedded)(3 tests mirror the Goendpoint_test.gocases). NOTE: nohost_browserfield exists in the Rust config schema yet — add it when wiring intoApplication::new(B6). - A4. ID-token verification:
decode_header→kid, JWKS fetch (reqwest→jwk::JwkSet), RS256 verify; plus HS256-by-client-secret path keyed offid_token_signing_alg_values_supported. Fixture test. Done insrc/outpost/proxy/token.rs: pureverify_hs256/verify_rs256(issuer + audience- exp validated; sets
raw_tokenand defaultsak_proxy). RS256 works on theaws_lc_rsbackend with no extra feature. 4 fixture tests (HS256, RS256, wrong-issuer, unknown-kid). DEFERRED to B6/C10: the async JWKS fetch (reqwest GET), the HS256-vs-RS256 selection fromid_token_signing_alg_values_supported, and JWKS caching/refresh on unknownkid.
- exp validated; sets
Phase B — session + cookies (needs A1)
- B5.
SessionData+ asyncSessionStoretrait +FsSessionStore(JSON files,session_<id>, maxage→expiry). save/load/delete/expiry tests. Done insrc/outpost/proxy/session/{mod,filesystem}.rs. DESIGN CHANGE:SessionStoreis an enum (nativeasync fn, static dispatch), not adyntrait — native async-fn traits aren't dyn-compatible and we avoidasync-trait. Each file stores{expires, data}JSON and expiry is checked onload(no mtime/background-cleanup reliance). Addedtempfiledev-dep. 5 tests. DEFERRED: writable-path validation + periodic cleanup sweep (Go'sNewStore/CleanupManager) — not needed for correctness given load-time expiry. - B6. Extend
Applicationto hold theSessionStore(enum), cookie signing key,OidcEndpoint, backchannelreqwestclient; wire inApplication::new. Also add thehost_browserconfig field (needed byOidcEndpoint, see A3). Compiles, no behavior change. Done: addedhost_browser: Option<String>to config schema (AUTHENTIK_HOST_BROWSER);Applicationnow holdsendpoint: OidcEndpoint(built from outpostconfig["authentik_host"]host_browser+is_embedded) andsession_store: SessionStore(filesystem,temp_dir()). DEFERRED to B7: cookie signing key (needs axum-extra). DEFERRED to C10/C13: backchannelreqwestclient (needs Host-override) andsession_max_agefromaccess_token_validity(avoids an f64→intascast until it's actually used).
- B7. Add
axum-extracookie support; signed session-ID cookie read/issue helper with per-provider domain/secure/samesite/path/maxage (mirrorgetStoreoptions).
Phase C — auth-start + callback (shared flow; needs A2–A4, B)
- C8.
/outpost.goauthentik.io/startroute +handle_auth_start: ensure session ID, build state JWT, build authorize URL with?rd=, 302. (First user-visible behavior.) - C9.
redirect_to_starthelper: store redirect in session,InterceptHeaderAuth401 path, forward_domain redirect validation (checkRedirectParam). - C10.
handle_auth_callback: validate state JWT + session-ID match, code exchange, verify ID token, extract claims, session maxage fromexp, save, redirect to storedrd.
Phase D — non-session auth paths + caching (needs A4, C)
- D11. Add
mokaTTL cache;attempt_bearer_auth(introspection POST) + cache get/save (auth_bearer.go,auth.go). - D12.
attempt_basic_auth(goauthentik.io/tokenusername → bearer path; else client-credentials token POST + verify) (auth_basic.go). - D13. Unified
check_auth: session → cache → bearer → basic →Option<Claims>.
Phase E — header injection + allowlist (needs A1)
- E14.
get_headers/add_headers(allX-authentik-*, basic-auth from user attributes, additional headers, underscore-dedup). Unit test. - E15.
UnauthenticatedRegexallowlist (IsAllowlisted) — compile regexes inApplication::new; mode-dependent path-vs-URL matching. Unit test.
Phase F — modes (needs C, D, E)
- F16. Forward-auth URL helpers (
getTraefikForwardUrl,getNginxForwardUrl) +ReportMisconfiguration(events API). Parsing tests. - F17.
handle_traefik+handle_caddy(shared logic): callback/logout dispatch,check_auth→headers, allowlist, else auth-start. - F18.
handle_nginx(200+headers / redirect-flag session save / 401) andhandle_envoy(path-trim, host fixup). - F19. Reverse-proxy data path (
mode_proxy.go): hyper-util client →internal_host, request/response modification, backend-override/host-header, streaming,X-Powered-By,check_auth→headers orredirect_to_start.
Phase G — logout, postgres, error pages
- G20.
handle_sign_out: clear session, redirect toend_session_endpoint. - G21.
ProxyOutpost::end_session: per-app storelogout(sid == event.session_id)(session.go). - G22.
PgSessionStore(sqlx) + feature-flag decision (dep:sqlxunderproxyvs newproxy-postgres) +PgPoolwiring + backend selection in config schema. DB-gated test. - G23. Error-page rendering (templated 401/500) replacing bare status codes (
error.go).
Ordering risks / notes to carry forward
AppErroris always 502 — returnResponsedirectly for 302/401/200 control flow; reserveAppErrorfor genuine internal failures.- State JWT has no
exp— must disablevalidate_exp/audinjsonwebtoken::Validation. - Feature flags:
sqlxis currentlycore-only; decide its gating before G22 soApplicationfield types stay stable (filesystem-first keeps this off the critical path). end_sessionmapping: WS event carriessession_id; claim field issid. FS store must scan-and-match like Go'sLogout.- Embedded backchannel Host override: backchannel client rewrites Host while issuer/jwks use browser host — replicate in A3/A4.
- Tests: use
cargo t(project convention), notcargo test --lib.
Verification
- Per step:
cargo build+cargo t(the new unit tests for that step). Workspace lints are strict (clippy pedantic/nursery + many restriction lints;unwrap_used/todo/unimplemented= warn) — keep each step clean. - End-to-end milestone after Phase C: unauthenticated request to a proxy-mode app → 302 to the authorize endpoint; full login loop closes after C10. After Phase F: forward-auth (traefik/nginx) and reverse-proxy modes function against a running authentik core.