# 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: ```bash mkcert -install ``` Then restart your browser. ## 2. Initial admin setup Authentik bootstraps with admin credentials from `.env`: - **URL:** https://auth.dezky.local/if/flow/initial-setup/ - **Email:** admin@dezky.local - **Password:** Value of `AUTHENTIK_BOOTSTRAP_PASSWORD` in `.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 Interface** → **Applications** → **Providers** 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 **Applications** → **Applications** 2. Click **Create** 3. Configure: - **Name:** `OCIS Files` - **Slug:** `ocis` - **Provider:** `ocis-provider` (just created) - **Launch URL:** https://files.dezky.local 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= 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: ```bash 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 < ``` **On a fresh environment, set `OPERATOR_OIDC_CLIENT_SECRET` in `.env` BEFORE first boot** (`.env.example` ships a placeholder with an `openssl rand -hex 64` note). This is the inverse of the portal flow: the portal's secret is *generated by Authentik* and copied out into `.env`, whereas the operator secret is *supplied by you* and consumed by the blueprint when it creates the provider. The operator container authenticates with the same `OPERATOR_OIDC_CLIENT_SECRET` (passed in as its `NUXT_OIDC_CLIENT_SECRET`), so the two only agree if the value is present before the blueprint runs — leave it empty and the blueprint provisions the provider with an empty secret and the OIDC handshake fails. On your existing environment these are already populated in `.env` (and the blueprint's `state: created` won't touch the live provider anyway), so nothing changes for you. The resulting issuer URL is `https://auth.dezky.local/application/o/dezky-operator/`. platform-api accepts both the portal and operator issuers/audiences — see `AUTHENTIK_ISSUER` / `AUTHENTIK_AUDIENCE` in `docker-compose.yml`. **Apply / verify:** ```bash # Worker picks up the mount + env and applies the blueprint docker compose -f infrastructure/docker-compose/docker-compose.yml up -d authentik-worker # Watch it apply (look for the blueprint name / any !Find that failed to resolve) docker compose -f infrastructure/docker-compose/docker-compose.yml logs authentik-worker | grep -i blueprint ``` Then in the UI: **Applications → dezky-operator → Bindings** should list `operator-require-platform-admin` (enabled). ## Operator portal isolation (three layers) The operator portal (`operator.dezky.local`) carries elevated, cross-tenant privilege. A partner or a tenant admin must **never** be able to log into it. That guarantee is defense-in-depth across three independent layers — a single misconfiguration in any one of them does not open the door: 1. **IdP (Authentik) — stops the login itself.** The `operator-require-platform-admin` policy bound to the `dezky-operator` application (§3.5) is evaluated *before* Authentik issues the authorization. A user who is not in `dezky-platform-admins` is denied during the OIDC flow and never reaches the app. This is the primary gate. 2. **Operator app — refuses to render for a non-admin.** The global middleware `apps/operator/middleware/require-platform-admin.global.ts` resolves the signed-in user's profile and requires `platformAdmin=true`. A non-admin who somehow holds a valid operator session is routed to `apps/operator/pages/not-authorized.vue`, which triggers a **full sign-out** (local nuxt-oidc-auth session **and** the Authentik IdP session) — never leaving a live operator session parked on a shared workstation. 3. **platform-api — refuses the data.** `OperatorGuard` (`services/platform-api/src/auth/operator.guard.ts`) requires **both** a `dezky-operator` token audience **and** `platformAdmin=true` resolved from the DB on every request. The DB check means revoking someone's admin status takes effect immediately, without waiting for their token to expire. `dezky-platform-admins` is the single source of truth tying these together: Authentik gates membership (layer 1), and platform-api maps that group to the `platformAdmin` flag it enforces (layers 2 and 3). **Partner ⇄ customer note.** The partner surface lives *inside* the shared `dezky-portal` OAuth client (tenant admins authenticate there legitimately), so it cannot have an IdP-level gate like the operator app. Its isolation is the portal's fail-closed `/partner/*` route middleware plus platform-api's per-endpoint `partnerId` checks. If the partner surface ever needs the same IdP-level isolation as operator, it must become its own OAuth client/application (at which point this same blueprint pattern applies to it). ## 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`: ```bash 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 ```bash 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.