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

225 lines
8.0 KiB
Markdown

# 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=<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:
```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 <<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`:
```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.