chore: initial scaffold with running local stack and portal auth
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
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
# 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 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`:
|
||||
|
||||
```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 the provisioning service.
|
||||
|
||||
### 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. The provisioning service 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.
|
||||
Reference in New Issue
Block a user