Files
dezky/docs/AUTHENTIK-SETUP.md
T
Ronni Baslund adfd9baafe chore: initial scaffold with running local stack and portal auth
Brings up Dezky's local development environment end-to-end:

Infrastructure (docker-compose):
- Traefik v3.7 reverse proxy with mkcert TLS (v3.2 couldn't speak Docker API 1.54)
- Postgres + Mongo + Redis with healthchecks and init script for per-service users
- Authentik 2025.10 (server + worker) as OIDC IdP
- Stalwart v0.16 mail server (image renamed from stalwartlabs/mail-server)
- OCIS 7.0 with PROXY_TLS=false and OCIS_CONFIG_DIR=/etc/ocis so init writes
  where the server reads
- Collabora office, plus the portal + provisioning service stubs
- Docker network aliases on Traefik so containers resolve auth.dezky.local etc.
  through the network (not host /etc/hosts)
- Docker socket mount parameterized for macOS Docker Desktop symlink path

Authentik provisioning (done via API after stack boot):
- ocis-provider (public client) + OCIS Files application
- dezky-portal provider (confidential) + Dezky Portal application
- Admin API token bound to akadmin manually since 2025.10's
  AUTHENTIK_BOOTSTRAP_TOKEN env var doesn't auto-materialize a token row

Portal (apps/portal):
- Nuxt 3 with nuxt-oidc-auth 1.0.0-beta.11 against generic 'oidc' preset
- Global auth middleware; login at /auth/oidc/login redirects to Authentik
- Visual implementation of Claude Design 'Auth' canvas: AuthShell, NodeMark,
  Auth* sub-components, design tokens as CSS custom properties
- Pages: auth/login, auth/expired, auth/disabled, index (post-login landing)
- mkcert root CA mounted into the portal so Node fetch trusts Authentik's
  self-signed cert (NODE_EXTRA_CA_CERTS) — dev only

Docs:
- AUTHENTIK-SETUP.md updated with manual token bind + portal provider scripted
  alternative
- NEXT-STEPS.md: Phase 1 and Phase 2 marked done with file locations and
  dev-mode caveats

Dev-mode shortcuts that need to be revisited before prod:
- skipAccessTokenParsing on the OIDC config
- NODE_EXTRA_CA_CERTS mkcert mount
- Bootstrap password still the generated value in .env
- Authentik admin token (dezky-bootstrap-token) is non-expiring
2026-05-23 21:25:11 +02:00

8.1 KiB

Authentik First-Time Setup

After the bootstrap script completes, Authentik is running but needs to be configured. This guide walks through the initial setup.

1. Access Authentik

Open https://auth.dezky.local in your browser.

If you see a TLS warning, mkcert root CA isn't trusted yet. Run:

mkcert -install

Then restart your browser.

2. Initial admin setup

Authentik bootstraps with admin credentials from .env:

On first login, change the password immediately.

3. Configure OIDC providers

Each Dezky service that uses SSO needs an OIDC provider configured in Authentik.

3.1 Create OCIS provider

  1. Go to Admin InterfaceApplicationsProviders
  2. Click Create
  3. Select OAuth2/OpenID Provider
  4. Configure:
    • Name: ocis-provider
    • Authorization flow: default-provider-authorization-implicit-consent
    • Client type: Public
    • Client ID: ocis-web
    • Redirect URIs:
      https://files.dezky.local/
      https://files.dezky.local/oidc-callback
      
    • Signing Key: authentik Self-signed Certificate
    • Scopes: openid, profile, email
  5. Save

3.2 Create OCIS application

  1. Go to ApplicationsApplications
  2. Click Create
  3. Configure:
  4. Save

3.3 Create portal provider

Same steps as OCIS, but with:

  • Provider name: dezky-portal
  • Client ID: dezky-portal
  • Redirect URIs: https://app.dezky.local/api/auth/callback
  • Client type: Confidential (Authentik will generate a Client Secret)

Then create the matching application:

  • Name: Dezky Portal → slug auto-generates as dezky-portal
  • Provider: dezky-portal (from above)
  • Launch URL: https://app.dezky.local

The resulting issuer URL is https://auth.dezky.local/application/o/dezky-portal/ — note the slug includes dezky-.

After creating, copy the generated client secret into .env:

PORTAL_OIDC_CLIENT_ID=dezky-portal
PORTAL_OIDC_CLIENT_SECRET=<paste from Authentik provider page>
PORTAL_OIDC_ISSUER=https://auth.dezky.local/application/o/dezky-portal/

docker-compose.yml passes these to the portal container as NUXT_OIDC_*, which nuxt-oidc-auth (added in Phase 2) consumes.

Scripted alternative

If you don't want to click through the UI, this one-shot uses the API token from section 4:

TOKEN=$(grep ^AUTHENTIK_BOOTSTRAP_TOKEN .env | cut -d= -f2)
BASE=https://auth.dezky.local/api/v3
AUTH="Authorization: Bearer $TOKEN"

# Pull the same authorization flow + signing key + scope mappings that OCIS uses
FLOW=$(curl -k -s -H "$AUTH" "$BASE/flows/instances/?slug=default-provider-authorization-implicit-consent" | jq -r '.results[0].pk')
KEY=$(curl -k -s -H "$AUTH" "$BASE/providers/oauth2/?search=ocis" | jq -r '.results[0].signing_key')
MAPS=$(curl -k -s -H "$AUTH" "$BASE/providers/oauth2/?search=ocis" | jq -c '.results[0].property_mappings')

# Create provider
PK=$(curl -k -s -X POST -H "$AUTH" -H "Content-Type: application/json" "$BASE/providers/oauth2/" -d "{
  \"name\": \"dezky-portal\",
  \"client_id\": \"dezky-portal\",
  \"client_type\": \"confidential\",
  \"authorization_flow\": \"$FLOW\",
  \"signing_key\": \"$KEY\",
  \"redirect_uris\": [{\"matching_mode\": \"strict\", \"url\": \"https://app.dezky.local/api/auth/callback\"}],
  \"property_mappings\": $MAPS,
  \"sub_mode\": \"hashed_user_id\",
  \"issuer_mode\": \"per_provider\"
}" | jq -r '.pk')

# Create app
curl -k -s -X POST -H "$AUTH" -H "Content-Type: application/json" "$BASE/core/applications/" -d "{
  \"name\": \"Dezky Portal\",
  \"slug\": \"dezky-portal\",
  \"provider\": $PK,
  \"meta_launch_url\": \"https://app.dezky.local\"
}" >/dev/null

# Read the generated secret and write to .env
SECRET=$(curl -k -s -H "$AUTH" "$BASE/providers/oauth2/$PK/" | jq -r '.client_secret')
cat >> .env <<EOF

PORTAL_OIDC_CLIENT_ID=dezky-portal
PORTAL_OIDC_CLIENT_SECRET=$SECRET
PORTAL_OIDC_ISSUER=https://auth.dezky.local/application/o/dezky-portal/
EOF

3.4 Create Stalwart provider (when wiring mail SSO later)

Note: Stalwart's OIDC integration is configured in infrastructure/docker-compose/configs/stalwart/config.toml. For local dev with internal users, OIDC is optional.

4. Get the API token for provisioning service

The provisioning service needs to call Authentik's API to create tenants, users, and applications. .env holds a pre-generated value in AUTHENTIK_BOOTSTRAP_TOKEN, but Authentik 2025.10 does not materialize that env var into a usable API token on first boot. You need to create the token once and bind it to akadmin.

One-time setup

Run this from the project root — it reads the value from .env and inserts it as a non-expiring API token for akadmin:

TOKEN=$(grep ^AUTHENTIK_BOOTSTRAP_TOKEN .env | cut -d= -f2)
docker compose -f infrastructure/docker-compose/docker-compose.yml exec -T \
  -e BOOTSTRAP_TOKEN="$TOKEN" authentik-server ak shell -c "
import os
from authentik.core.models import User, Token, TokenIntents
admin = User.objects.get(username='akadmin')
Token.objects.update_or_create(
    identifier='dezky-bootstrap-token',
    defaults={
        'user': admin,
        'intent': TokenIntents.INTENT_API,
        'expiring': False,
        'key': os.environ['BOOTSTRAP_TOKEN'],
    },
)
print('Token bound to akadmin')
"

Alternative: create the token through the UI — Directory → Tokens & App passwords → Create, set Intent: API, User: akadmin, then copy the key into .env and restart the provisioning service.

Verify it works

curl -k -H "Authorization: Bearer $(grep ^AUTHENTIK_BOOTSTRAP_TOKEN .env | cut -d= -f2)" \
  https://auth.dezky.local/api/v3/core/users/

A 200 response with the user list (including akadmin) means the token is live. A 403 {"detail":"Token invalid/expired"} means the one-time setup above hasn't been run yet.

When you need to re-run this

After docker compose down -v (or any reset that wipes the postgres volume), the token row is gone and you'll need to recreate it. The value in .env doesn't need to change — re-running the shell snippet above re-binds it.

5. Multi-tenancy strategy

For local dev, you can either:

Option A: Single Authentik tenant, multiple groups

  • All Dezky test users in one Authentik instance
  • Tenants modeled as Authentik groups
  • Simpler for dev, less realistic

Option B: Authentik tenants (production-mode)

  • Each Dezky customer = Authentik tenant
  • Tenant subdomain pattern: {tenant}.auth.dezky.local
  • More realistic but more setup overhead

For dev, start with Option A. The provisioning service should be built to support Option B from day one (data model includes tenantId).

6. Test SSO flow end-to-end

  1. Open incognito browser to https://files.dezky.local
  2. OCIS should redirect to https://auth.dezky.local for login
  3. Log in with admin@dezky.local
  4. Should redirect back to OCIS, logged in

If this works, OIDC integration is solid.

Common issues

"Issuer URL does not match"

OCIS expects exact match between OCIS_OIDC_ISSUER and the iss claim in the JWT.

Check the issuer in Authentik:

  • Admin Interface → Providers → ocis-provider → Configure
  • Note the Issuer URL at the bottom
  • Update OCIS_OIDC_ISSUER in docker-compose.yml to match exactly

"Client authentication failed"

Public clients (Client Type: Public) don't need a client secret. Confidential clients need the secret added to the consumer's config.

For OCIS, use Public type. For the portal (which has server-side auth), use Confidential.

TLS verification fails between containers

Inside the Docker network, services use Docker-internal hostnames (e.g. authentik-server). TLS verification can fail because the cert is for auth.dezky.local, not authentik-server.

For service-to-service auth, use the issuer URL with OCIS_INSECURE=true only for dev. Production will use proper certs.