echo6-docs/runbooks/authentik-oidc-application.md
Matt Johnson e9231ac24a Migration: consolidate Echo6 docs to cortex with full infrastructure cleanup sync
- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup)
- Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing
- Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack
- Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure
- Removes 4 deprecated runbook duplicates (canonical versions live in projects/)
- Adds .gitignore for binary archives and editor temp files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 06:02:16 +00:00

11 KiB

Add Authentik OIDC to an Application

Fully automated via Authentik API. No web UI interaction required.

Prerequisite: DNS must already exist for the service (run expose-service-contabo.md or expose-service-home.md first).

Authentik instance: https://auth.echo6.co (Contabo, 100.64.0.6)


Inputs

Prompt the user for all of these before executing any steps:

SERVICE_NAME=               # Human-readable (e.g., "Vaultwarden", "Headplane")
SERVICE_SLUG=               # URL-safe, lowercase (e.g., "vaultwarden", "headplane")
SERVICE_URL=                # Base URL (e.g., "https://vault.echo6.co")
OIDC_CALLBACK_PATH=         # App's OIDC callback (e.g., "/oidc/callback")
NEEDS_OFFLINE_ACCESS=       # yes/no — does the app need refresh tokens?
CLIENT_TYPE=                # confidential (server-side) or public (SPA/mobile)

The redirect URI is ${SERVICE_URL}${OIDC_CALLBACK_PATH}.

When to set NEEDS_OFFLINE_ACCESS=yes

  • The app stores sessions that must survive service restarts (Headscale, Vaultwarden)
  • The app uses refresh tokens for long-lived sessions
  • Users shouldn't have to re-authenticate after every restart

Reserved slugs

These conflict with Authentik's internal OAuth2 endpoints and cannot be used: authorize, token, device, userinfo, introspect, revoke.


Step 1: Get API Token

Create an API token from the Authentik admin account. This only needs to happen once — reuse the token across all OIDC setups.

ssh root@100.64.0.6 "docker exec authentik-server \
  ak create_token --user akadmin --identifier oidc-automation --expiring 2>/dev/null \
  || echo 'Token may already exist — check credentials file'"

If the token already exists, retrieve it from /home/zvx/projects/.ref/credentials (AUTHENTIK_API_TOKEN).

Store it for use in subsequent steps:

AK_TOKEN="<token>"
AK_API="https://auth.echo6.co/api/v3"

Step 2: Look Up Authentik Internal IDs

The API requires UUIDs for flows, scope mappings, and signing keys. These are stable per Authentik instance but must be looked up once.

Authorization flow

ssh root@100.64.0.6 "curl -s \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/flows/instances/?slug=default-provider-authorization-implicit-consent' \
  | jq -r '.results[0].pk'"

Store as AUTH_FLOW_PK.

Scope mappings

# Get all scope mapping UUIDs at once
ssh root@100.64.0.6 "curl -s \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/propertymappings/provider/scope/?ordering=scope_name' \
  | jq -r '.results[] | select(.scope_name == \"openid\" or .scope_name == \"email\" or .scope_name == \"profile\" or .scope_name == \"offline_access\") | \"\(.scope_name): \(.pk)\"'"

Store each UUID: SCOPE_OPENID_PK, SCOPE_EMAIL_PK, SCOPE_PROFILE_PK, SCOPE_OFFLINE_PK.

Signing key

ssh root@100.64.0.6 "curl -s \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/crypto/certificatekeypairs/?name=authentik+Self-signed+Certificate&has_key=true' \
  | jq -r '.results[0].pk'"

Store as SIGNING_KEY_PK.

Gate

All five values must be non-null. If any are missing, Authentik's default objects may not have been created yet — check that the instance is healthy.


Step 3: Create the OAuth2 Provider

Build the scope mappings array based on whether offline_access is needed:

# Base scopes (always included)
SCOPES="[\"$SCOPE_OPENID_PK\", \"$SCOPE_EMAIL_PK\", \"$SCOPE_PROFILE_PK\"]"

# Add offline_access if needed
if [ "$NEEDS_OFFLINE_ACCESS" = "yes" ]; then
  SCOPES="[\"$SCOPE_OPENID_PK\", \"$SCOPE_EMAIL_PK\", \"$SCOPE_PROFILE_PK\", \"$SCOPE_OFFLINE_PK\"]"
fi

Create the provider:

PROVIDER_RESPONSE=$(ssh root@100.64.0.6 "curl -s \
  -X POST '$AK_API/providers/oauth2/' \
  -H 'Authorization: Bearer $AK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    \"name\": \"$SERVICE_NAME\",
    \"authorization_flow\": \"$AUTH_FLOW_PK\",
    \"client_type\": \"$CLIENT_TYPE\",
    \"redirect_uris\": [{
      \"matching_mode\": \"strict\",
      \"url\": \"${SERVICE_URL}${OIDC_CALLBACK_PATH}\"
    }],
    \"signing_key\": \"$SIGNING_KEY_PK\",
    \"property_mappings\": $SCOPES,
    \"access_token_validity\": \"hours=1\",
    \"refresh_token_validity\": \"days=30\"
  }'")

# Extract the values we need
PROVIDER_PK=$(echo "$PROVIDER_RESPONSE" | jq -r '.pk')
CLIENT_ID=$(echo "$PROVIDER_RESPONSE" | jq -r '.client_id')
CLIENT_SECRET=$(echo "$PROVIDER_RESPONSE" | jq -r '.client_secret')

echo "Provider PK:    $PROVIDER_PK"
echo "Client ID:      $CLIENT_ID"
echo "Client Secret:  $CLIENT_SECRET"

Gate

PROVIDER_PK must be a number (not null or an error). If the API returns an error, common causes:

  • Duplicate name — a provider with this name already exists
  • Invalid flow PK — the authorization flow UUID is wrong
  • Invalid scope PK — one of the scope mapping UUIDs is wrong

Step 4: Create the Application

ssh root@100.64.0.6 "curl -s \
  -X POST '$AK_API/core/applications/' \
  -H 'Authorization: Bearer $AK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    \"name\": \"$SERVICE_NAME\",
    \"slug\": \"$SERVICE_SLUG\",
    \"provider\": $PROVIDER_PK,
    \"meta_launch_url\": \"$SERVICE_URL\"
  }' | jq '{name: .name, slug: .slug, provider: .provider}'"

Gate

Response must include the slug and provider PK. If it fails, the slug may already be in use.


Step 5: Verify Authentik Side

Discovery endpoint

curl -s "https://auth.echo6.co/application/o/$SERVICE_SLUG/.well-known/openid-configuration" | jq '{issuer, authorization_endpoint, token_endpoint, jwks_uri}'

Must return all four fields with valid URLs.

JWKS endpoint

curl -s "https://auth.echo6.co/application/o/$SERVICE_SLUG/jwks/" | jq '.keys | length'

Must return at least 1. If it returns 0, the signing key was not attached to the provider — go back and fix Step 3.


Step 6: Configure the Application

This step varies per application. Use the Client ID, Client Secret, and issuer URL from above.

OIDC endpoints (all derived from the slug)

Issuer:    https://auth.echo6.co/application/o/$SERVICE_SLUG/
Authorize: https://auth.echo6.co/application/o/authorize/
Token:     https://auth.echo6.co/application/o/token/
User Info: https://auth.echo6.co/application/o/userinfo/
JWKS:      https://auth.echo6.co/application/o/$SERVICE_SLUG/jwks/

Most apps only need the Issuer (or Discovery URL) plus Client ID and Client Secret. The app auto-discovers the rest.

Common config patterns

Environment variables (Docker):

OIDC_ISSUER=https://auth.echo6.co/application/o/$SERVICE_SLUG/
OIDC_CLIENT_ID=$CLIENT_ID
OIDC_CLIENT_SECRET=$CLIENT_SECRET
OIDC_SCOPES="openid email profile"          # add offline_access if needed
OIDC_REDIRECT_URI=${SERVICE_URL}${OIDC_CALLBACK_PATH}

Config file (YAML):

oidc:
  issuer: "https://auth.echo6.co/application/o/$SERVICE_SLUG/"
  client_id: "$CLIENT_ID"
  client_secret: "$CLIENT_SECRET"
  scope: ["openid", "profile", "email"]     # add "offline_access" if needed

Common alternate names for these values

Concept Names you'll see
Issuer authority, issuer_url, sso_authority, provider_url
Client ID client_id, oidc_client_id, sso_client_id
Client Secret client_secret, oidc_client_secret, sso_client_secret
Redirect URI redirect_uri, callback_url, oidc_redirect_url
Scopes scope, scopes, oidc_scopes, sso_scopes

Step 7: Test Login

  1. Open $SERVICE_URL in a browser
  2. Click SSO / OIDC login
  3. Should redirect to auth.echo6.co → authenticate → redirect back to the app
  4. Verify user info is correct (email, display name)

Step 8: Store Credentials

cat >> /home/zvx/projects/.ref/credentials << EOF

# $SERVICE_NAME OIDC
${SERVICE_SLUG^^}_OIDC_CLIENT_ID=$CLIENT_ID
${SERVICE_SLUG^^}_OIDC_CLIENT_SECRET=$CLIENT_SECRET
${SERVICE_SLUG^^}_OIDC_ISSUER=https://auth.echo6.co/application/o/$SERVICE_SLUG/
EOF

Troubleshooting

SSO login redirects back to login page (loop)

Check in order:

  1. Access token validity too short — increase to at least hours=1
  2. Missing offline_access scope — app can't refresh tokens, session expires immediately
  3. Missing signing key — JWKS endpoint returns empty, app can't verify tokens

Debug via API:

ssh root@100.64.0.6 "curl -s \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/providers/oauth2/?search=$SERVICE_NAME' \
  | jq '.results[0] | {name, client_id, signing_key, access_token_validity, refresh_token_validity, property_mappings}'"

Or via ak shell:

ssh root@100.64.0.6 "docker exec authentik-server ak shell -c \"
from authentik.providers.oauth2.models import OAuth2Provider
p = OAuth2Provider.objects.get(name='$SERVICE_NAME')
print(f'Access Token: {p.access_token_validity}')
print(f'Refresh Token: {p.refresh_token_validity}')
print(f'Signing Key: {p.signing_key}')
print(f'Scopes: {list(p.property_mappings.values_list(\\\"scope_name\\\", flat=True))}')
\""

"Failed to discover OpenID provider" / discovery error

  1. JWKS endpoint is empty → signing key missing from provider
  2. Authentik unreachable from the app → test with curl from the app's host
  3. Wrong issuer URL → must include trailing slash, must match the slug exactly

"Invalid redirect URI"

The redirect URI in the app config must exactly match what's in Authentik — scheme, trailing slashes, path, everything.

User authenticated but gets "access denied"

User isn't authorized for the application. By default all authenticated users have access. If you've added group restrictions via policy bindings, verify the user is in the correct group:

ssh root@100.64.0.6 "curl -s \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/core/applications/$SERVICE_SLUG/' \
  | jq '{name, slug, policy_engine_mode}'"

Token/session breaks after service restart

Missing offline_access scope. Without refresh tokens, sessions only last as long as the access token validity.

Delete and recreate (nuclear option)

# Delete application first (it references the provider)
ssh root@100.64.0.6 "curl -s -X DELETE \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/core/applications/$SERVICE_SLUG/'"

# Then delete provider
ssh root@100.64.0.6 "curl -s -X DELETE \
  -H 'Authorization: Bearer $AK_TOKEN' \
  '$AK_API/providers/oauth2/$PROVIDER_PK/'"

Then re-run from Step 3.


Quick Reference: Existing OIDC Applications

Application Slug Redirect URI offline_access
Headscale headscale https://vpn.echo6.co/oidc/callback Yes
Headplane headplane https://vpn.echo6.co/admin/oidc/callback No
Vaultwarden vaultwarden https://vault.echo6.co/identity/connect/oidc-signin Yes