Ryan Malloy 21175c5b7a
Some checks are pending
Build / native (push) Waiting to run
Build / nix (push) Waiting to run
auth: stateless PKCE state, simplify OIDC handlers
- src/lib/auth/oidc-state.ts (new): pack {state, codeVerifier} into a
  short-lived signed JWT, store in an httpOnly cookie. Replaces the
  process-local Map that broke under multi-worker deployments where
  /api/auth/login and /api/auth/callback would land on different workers.
- src/pages/api/auth/{login,callback,logout,profile,status}.ts: drop the
  Map-based state-store calls; use oidc-state for set/get/clear.
- src/lib/auth/oidc-client.ts, session-manager.ts, config/authentik.ts:
  small adjustments to fit the new state surface.
- src/components/auth/AuthenticatedLayout.astro, src/layouts/Layout.astro:
  trim a lot of layout boilerplate (~125 lines each).
- astro.config.mjs, docker-compose.local.yml: minor cleanup.
- authentik-blueprints/heady-oidc.yaml (new): declarative provider +
  application blueprint to ship alongside Heady deployments.
2026-06-06 14:11:51 -06:00

83 lines
3.4 KiB
YAML

# Authentik blueprint: Heady OIDC application + provider + scope mappings.
#
# This blueprint declaratively creates everything Heady needs to authenticate
# users against Authentik. Drop it in /blueprints/local/ inside the Authentik
# container (the docker-compose mounts ../authentik-blueprints to that path)
# and Authentik will reconcile it on startup.
#
# Load order matters: scope mappings must exist before the provider can
# reference them; the provider must exist before the application can bind
# to it. Authentik's blueprint engine handles dependency ordering via the
# `!KeyOf` / `!Find` tags.
version: 1
metadata:
name: heady-oidc
labels:
blueprints.goauthentik.io/instantiate: 'true'
context:
# Override these in deployment by passing -e VAR=value to the Authentik
# container or by writing site-specific values directly.
heady_redirect_uri: http://localhost:3007/api/auth/callback
heady_app_slug: heady
heady_app_name: Heady
entries:
# ─── Scope mapping that exposes the user's group memberships ──────────
# Authentik's defaults include openid/email/profile but NOT groups; Heady's
# role mapper needs `groups` in the userinfo response to map Authentik
# groups → Heady roles (admin / network_admin / etc).
- model: authentik_providers_oauth2.scopemapping
id: heady-groups-mapping
identifiers:
managed: heady.scopemapping.groups
attrs:
name: 'Heady: groups'
scope_name: groups
description: List the groups a user belongs to.
expression: |
return {
"groups": [g.name for g in user.ak_groups.all()],
}
# ─── OAuth2 / OIDC provider ───────────────────────────────────────────
# Confidential client with PKCE. Uses the implicit-consent authorization
# flow shipped with Authentik so users aren't prompted on each login.
- model: authentik_providers_oauth2.oauth2provider
id: heady-oidc-provider
identifiers:
name: Heady OIDC
attrs:
client_type: confidential
authorization_flow:
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
invalidation_flow:
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
sub_mode: user_email
include_claims_in_id_token: true
access_token_validity: hours=1
refresh_token_validity: days=30
redirect_uris:
- matching_mode: strict
url: !Context heady_redirect_uri
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
- !KeyOf heady-groups-mapping
# ─── Application binding ──────────────────────────────────────────────
# The Application is what users see in the Authentik portal and what
# the OIDC issuer URL resolves to:
# https://<authentik>/application/o/<slug>/
- model: authentik_core.application
identifiers:
slug: !Context heady_app_slug
attrs:
name: !Context heady_app_name
provider: !KeyOf heady-oidc-provider
meta_description: Heady VPN management console
open_in_new_tab: false
policy_engine_mode: any