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
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:
- 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 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
- 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.