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.
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:
- URL: https://auth.dezky.local/if/flow/initial-setup/
- Email: admin@dezky.local
- Password: Value of
AUTHENTIK_BOOTSTRAP_PASSWORDin.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
- Go to Admin Interface → Applications → Providers
- Click Create
- Select OAuth2/OpenID Provider
- 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
- Name:
- Save
3.2 Create OCIS application
- Go to Applications → Applications
- Click Create
- Configure:
- Name:
OCIS Files - Slug:
ocis - Provider:
ocis-provider(just created) - Launch URL: https://files.dezky.local
- Name:
- 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 asdezky-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
- Open incognito browser to https://files.dezky.local
- OCIS should redirect to https://auth.dezky.local for login
- Log in with admin@dezky.local
- 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_ISSUERindocker-compose.ymlto 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.