22b2583f0b
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.
225 lines
8.0 KiB
Markdown
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.
|