Files
dezky/docs/AUTHENTIK-SETUP.md
T
Ronni Baslund 22b2583f0b chore(services): rename services/provisioning -> services/platform-api
O.0 prep from OPERATOR-PLAN.md. Mechanical refactor before adding partner
management and operator-specific endpoints. The service now owns more than
just provisioning orchestration (it'll soon own partners, tenant lifecycle
actions, multi-audience JWT validation), so the name 'platform-api' reflects
its scope better.

What changed:
- Directory: services/provisioning/ -> services/platform-api/
- Package: @dezky/provisioning -> @dezky/platform-api
- Docker: container_name dezky-provisioning -> dezky-platform-api;
  compose service key 'provisioning' -> 'platform-api'; volume
  provisioning_node_modules -> platform_api_node_modules
- Portal: PROVISIONING_INTERNAL_URL env var -> PLATFORM_API_INTERNAL_URL,
  default URL http://provisioning:3001 -> http://platform-api:3001 in all
  three proxy routes (me.get.ts, tenants/index.post.ts, tenants/[slug]/
  reconcile.post.ts), plus NUXT_API_BASE updated
- Health endpoint service identifier and main.ts log lines updated to
  'dezky-platform-api'
- Docs swept: README, CLAUDE.md, SERVICES.md, AUTHENTIK-SETUP.md,
  NEXT-STEPS.md, TROUBLESHOOTING.md, OPERATOR-PLAN.md, traefik/dynamic.yml

What deliberately stays:
- Internal module names ProvisioningService / ProvisioningModule (those
  describe an orchestration sub-concern, not the service's purpose)
- Tenant.provisioningStatus / provisioningErrors field names (state
  per integration, not service name)
- File services/platform-api/src/tenants/provisioning.service.ts
- 'Hetzner provisioning' references in production-prep docs (infrastructure
  provisioning, unrelated)

Verified end-to-end after rename: /api/me returns 200 with profile + 2
tenants + subscription, /api/tenants/dezky/reconcile returns 200 with
Authentik integration still ok.

OPERATOR-PLAN.md O.0 checkboxes ticked.
2026-05-24 00:35:01 +02:00

8.0 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 platform-api

platform-api 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 platform-api.

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. platform-api 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.