SSO with Authelia — Setup Guide
HenKaiPan supports single sign-on (SSO) via any OpenID Connect (OIDC) identity provider. This guide covers the reference setup using Authelia as the identity provider (IdP). Keycloak, Google Workspace, LLDAP (behind Authelia), and other OIDC-compliant providers work the same way.
How it works
User → HenKaiPan login page → "Sign in with SSO"
→ redirect to Authelia (auth.example.com)
→ user authenticates at Authelia
→ redirect back to HenKaiPan /api/auth/sso/callback
→ HenKaiPan verifies ID token, creates/links user, issues JWT
→ redirect to /dashboardImportant: HenKaiPan reads claims only from the signed ID token (no UserInfo fallback). The IdP must include
groupswhen using group-based role mapping.
Prerequisites
- HenKaiPan running behind a reverse proxy with HTTPS (Authelia requires HTTPS for OIDC)
- Authelia v4.38+ (OIDC provider support)
- A domain or subdomain for Authelia (e.g.
auth.example.com) - HenKaiPan accessible at a domain (e.g.
henkaipan.example.com)
Step 1 — Add Authelia to Docker Compose
The self-hosted repository ships an Authelia override file at docker-compose.authelia.yml. Run it alongside the base stack:
docker compose -f docker-compose.yml -f docker-compose.authelia.yml up -dThe override adds the Authelia service on the existing henkaipan-net network:
services:
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
volumes:
- ./authelia:/config
ports:
- "9091:9091" # keep internal only when behind a reverse proxy
environment:
- AUTHELIA_TELEMETRY_METRICS_ENABLED=false
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9091/api/health"]
interval: 10s
timeout: 5s
retries: 3
networks:
- henkaipan-netCreate the config directory (if it does not already exist):
mkdir -p autheliaStep 2 — Configure Authelia
The self-hosted repository includes authelia/configuration.yml and authelia/users_database.yml as starting points. Review them against your domain, then adjust the identity_providers.oidc block.
Key sections of authelia/configuration.yml:
server:
address: 'tcp://0.0.0.0:9091'
log:
level: info
identity_validation:
reset_password:
jwt_lifespan: '5 minutes'
authentication:
file:
path: '/config/users_database.yml'
access_control:
default_policy: deny
rules:
- domain: 'henkaipan.example.com'
policy: one_factor # or two_factor for stricter auth
session:
cookies:
- domain: 'example.com'
authelia_url: 'auth.example.com'
default_redirection_url: 'henkaipan.example.com'
storage:
local:
path: '/config/db.sqlite3'
notifier:
filesystem:
filename: '/config/notification.txt'
identity_providers:
oidc:
hmac_secret: '...' # generate a random string
jwks:
- key: |
-----BEGIN PRIVATE KEY-----
...PKCS#8 key content, indented 10 spaces...
-----END PRIVATE KEY-----
lifespans:
access_token: '1h'
id_token: '1h'
refresh_token: '90d'
# IMPORTANT: HenKaiPan reads claims ONLY from the signed ID token (no
# UserInfo fallback). This policy copies email/groups/preferred_username
# into the ID token for the henkaipan client. Without it, login fails with
# "SSO provider did not return an email address".
claims_policies:
henkaipan:
id_token:
- email
- groups
- preferred_username
clients:
- client_id: 'henkaipan'
client_name: 'HenKaiPan'
client_secret: '$argon2id$v=19$m=65536,t=3,p=4$...' # see Step 3
public: false
authorization_policy: 'one_factor'
consent_mode: 'implicit' # skip consent screen
claims_policy: 'henkaipan' # hydrate the ID token with email/groups
redirect_uris:
- 'https://henkaipan.example.com/api/auth/sso/callback'
scopes:
- 'openid'
- 'email'
- 'profile'
- 'groups'
response_types:
- 'code'
grant_types:
- 'authorization_code'Replace
example.comwith your actual domain throughout.
Step 3 — Generate the Client Secret Hash
Authelia stores client secrets as Argon2id hashes. Generate one:
docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'your-secure-client-secret'Copy the full $argon2id$... output and paste it as client_secret in configuration.yml.
Step 4 — Create Users + Groups in Authelia
authelia/users_database.yml:
users:
admin:
displayname: 'Admin User'
password: '$argon2id$...' # generate with: authelia crypto hash generate argon2 --password 'password'
email: admin@example.com
groups:
- admins # ← maps to HenKaiPan admin role
- henkaipan-users
viewer:
displayname: 'Viewer User'
password: '$argon2id$...'
email: viewer@example.com
groups:
- henkaipan-users # ← not in 'admins' → gets viewer roleGenerate password hashes:
docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'user-password'Step 5 — Configure HenKaiPan
Add these to your HenKaiPan .env file:
# ── SSO / OIDC ──────────────────────────────────────────────────
SSO_ENABLED=true
SSO_ISSUER_URL=https://auth.example.com
SSO_CLIENT_ID=henkaipan
SSO_CLIENT_SECRET=your-secure-client-secret
SSO_REDIRECT_URI=https://henkaipan.example.com/api/auth/sso/callback
SSO_GROUP_CLAIM=groups
SSO_ADMIN_GROUP=admins| Env var | Value | Notes |
|---|---|---|
SSO_ENABLED |
true |
Enables SSO routes + shows button on login page |
SSO_ISSUER_URL |
https://auth.example.com |
Must match Authelia's issuer |
SSO_CLIENT_ID |
henkaipan |
Must match client_id in Authelia |
SSO_CLIENT_SECRET |
your-secure-client-secret |
Plaintext secret (not the Argon2 hash) |
SSO_REDIRECT_URI |
https://henkaipan.example.com/api/auth/sso/callback |
Must match redirect_uris in Authelia |
SSO_GROUP_CLAIM |
groups |
Claim carrying group membership (default groups) |
SSO_ADMIN_GROUP |
admins |
Users in this group get HenKaiPan admin role |
Step 6 — Reverse Proxy Configuration
Both Authelia and HenKaiPan must be behind HTTPS. Example nginx config:
# Authelia
server {
listen 443 ssl http2;
server_name auth.example.com;
location / {
proxy_pass http://authelia:9091;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
# HenKaiPan
server {
listen 443 ssl http2;
server_name henkaipan.example.com;
location / {
proxy_pass http://api:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
}
}Ensure TRUSTED_PROXIES is set in HenKaiPan's .env when running behind nginx:
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16Step 7 — Restart and Test
# Restart both services
docker compose -f docker-compose.yml -f docker-compose.authelia.yml restart authelia api
# Verify config status shows SSO enabled
curl -s https://henkaipan.example.com/api/config/status | python3 -m json.tool
# Expected: "features": {"sso": true, "risk_acceptance": true}
# Open the login page — SSO button should appear
open https://henkaipan.example.com/loginTest the flow:
- Click "Sign in with SSO"
- You should be redirected to
auth.example.com - Log in with an Authelia user (e.g.
admin/ password) - You should be redirected back to HenKaiPan
/dashboard - Verify the user was created in HenKaiPan with the correct role:
docker compose exec -T postgres psql -U aspm -d aspm \
-c "SELECT username, email, role, sso_provider, sso_subject FROM users WHERE sso_subject IS NOT NULL;"How Role Mapping Works
HenKaiPan maps IdP groups to local roles:
| IdP group | HenKaiPan role | Behavior |
|---|---|---|
admins (or whatever SSO_ADMIN_GROUP is set to) |
admin |
Full read+write access |
| Any other group, or no group | viewer |
Read-only access |
If a user is in both the admin group and another group, they get admin (admin takes priority).
The
groupsscope MUST be requested. HenKaiPan requestsopenid,profile, andgroupsscopes from the IdP. If the IdP does not returngroups, role mapping will not work. This requires the client to includegroupsin its allowed scopes (HenKaiPan requests it by default).
Role sync: On every SSO login, HenKaiPan re-evaluates the user's role from the IdP groups claim and updates it if changed. Moving a user into/out of SSO_ADMIN_GROUP takes effect on their next SSO login.
User Provisioning Behavior
First SSO login for an email that already exists in HenKaiPan:
- The SSO identity (
sso_provider+sso_subject) is linked to the existing user - The existing user's role and team assignments are preserved
- This allows pre-provisioning users before enabling SSO
First SSO login for a new email:
- A new user is created automatically
- Username comes from the IdP's
preferred_usernameclaim (falls back to email) - Role is determined by group-claim mapping (see above)
- Password is empty (SSO-only user — cannot log in with password)
Troubleshooting
"SSO not configured" on the login page
The API started but the OIDC provider failed to initialize. Check logs:
docker compose logs api | grep -i ssoCommon causes:
SSO_ISSUER_URLunreachable from the API container (check DNS/networking)SSO_CLIENT_IDorSSO_CLIENT_SECRETempty- Authelia OIDC not configured (check Authelia logs)
"invalid or expired SSO state" on callback
The state cookie expired (5-minute window). This happens if the user takes too long at the Authelia login page. Try again.
"sso_failed" redirect after callback
The OIDC token exchange or ID token verification failed. Check API logs:
docker compose logs api | grep -i "sso.*failed"Common causes:
- Redirect URI mismatch between Authelia and
SSO_REDIRECT_URI(must match exactly, including scheme/host/path) - Client secret mismatch (HenKaiPan uses the plaintext secret, Authelia stores the Argon2 hash)
- Authelia not configured for the
authorization_codegrant type
"SSO provider did not return an email address" (sso_no_email)
HenKaiPan reads claims only from the signed ID token (by design — no UserInfo fallback). Authelia by default puts email/groups in the UserInfo endpoint, not the ID token.
Fix: add a claims_policy that hydrates the ID token for the henkaipan client (see the claims_policies section in Step 2). Verify it took effect by decoding an ID token — it must contain email and groups claims.
If using a different IdP (Keycloak, Google), configure the equivalent — the ID token must carry email and groups.
SSO button not appearing on login page
Verify the config status endpoint returns sso: true:
curl -s https://henkaipan.example.com/api/config/statusIf it returns sso: false, the API did not detect SSO config. Check that SSO_ENABLED=true is in the .env file and the API container was restarted after the change.
User gets viewer instead of admin
Check that the Authelia user is in the group specified by SSO_ADMIN_GROUP:
# authelia/users_database.yml
users:
myadmin:
groups:
- admins # must match SSO_ADMIN_GROUP exactlyExisting user can't log in after SSO is enabled
If an existing HenKaiPan user's email does not match their Authelia email, the SSO identity will not be linked. Fix by either:
- Updating the user's email in HenKaiPan to match their Authelia email
- Manually linking the SSO identity in the database:
UPDATE users
SET sso_provider = 'https://auth.example.com',
sso_subject = '<authelia-user-subject>'
WHERE email = 'existing@example.com';