Phase 18 changes: OAuth/OIDC SSO (Authorization Code + PKCE): - alembic/versions/0015_add_oauth_fields.py: add oauth_sub UNIQUE column, drop NOT NULL on password_hash - src/fabledassistant/services/oauth.py: OIDC discovery (cached), build_auth_url, exchange_code, get_userinfo, find_or_create_oauth_user (sub→email auto-link→create) - src/fabledassistant/routes/auth.py: GET /api/auth/oauth/login and GET /api/auth/oauth/callback; LOCAL_AUTH_ENABLED guards on login/register; /api/auth/status now returns oauth_enabled + local_auth_enabled - src/fabledassistant/config.py: OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_SCOPES, LOCAL_AUTH_ENABLED, oidc_enabled() classmethod - src/fabledassistant/models/user.py: password_hash nullable, oauth_sub field, has_password bool in to_dict() - src/fabledassistant/services/auth.py: create_user accepts password=None + oauth_sub kwarg; authenticate returns None for OAuth-only users; add get_user_by_oauth_sub, link_oauth_sub, update_user_email - frontend: AuthStatus + User types updated; auth store exposes oauthEnabled + localAuthEnabled; LoginView shows SSO button / hides password form accordingly Email change: - PUT /api/auth/email: requires password confirmation for local-auth users, skips check for OAuth-only users; enforces email uniqueness - SettingsView.vue: new Email Address section pre-filled with current email, updates authStore.user in-place on success Docs: - docs/oauth-setup.md: step-by-step Authentik provider setup, example docker-compose env vars, account linking explanation, per-provider issuer URL table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.9 KiB
OAuth / OIDC SSO Setup (Authentik)
Fabled Assistant supports single sign-on via any OpenID Connect provider. This guide covers Authentik, but the same pattern works with Keycloak, Authelia, Zitadel, etc.
1. Create the provider in Authentik
- Log in to the Authentik admin UI.
- Go to Applications → Providers → Create → OAuth2/OpenID Provider.
- Fill in:
- Name:
Fabled Assistant(or whatever you like) - Authorization flow: your default authorization flow
- Client type:
Confidential - Redirect URIs:
https://your-fabled-domain/api/auth/oauth/callback(must matchBASE_URLexactly, including scheme and any path prefix)
- Name:
- Note the generated Client ID and Client Secret.
- Go to Applications → Create, give it a name, and bind it to the provider you just created.
- Note the Issuer URL from the provider detail page — it looks like:
https://auth.example.com/application/o/fabled-assistant/
2. Configure Fabled Assistant
Add the following environment variables to the app service in docker-compose.yml:
services:
app:
environment:
# --- Required ---
OIDC_ISSUER: "https://auth.example.com/application/o/fabled-assistant/"
OIDC_CLIENT_ID: "abc123xyz"
OIDC_CLIENT_SECRET: "supersecret"
# --- Optional ---
# Scopes to request (default: "openid profile email")
# OIDC_SCOPES: "openid profile email"
# Disable local username/password login once SSO is working
# LOCAL_AUTH_ENABLED: "false"
# Make sure BASE_URL matches the redirect URI you registered in Authentik
BASE_URL: "https://your-fabled-domain"
Docker Secrets alternative: Instead of
OIDC_CLIENT_SECRET, you can useOIDC_CLIENT_SECRET_FILEpointing to a Docker secret file.
Rebuild and restart:
docker compose up --build -d
3. Verify
- Open
/api/auth/status— it should return:{ "oauth_enabled": true, "local_auth_enabled": true, ... } - Go to the login page — you should see a "Login with Authentik" button.
- Click it → you are redirected to Authentik → authenticate → redirected back to Fabled → logged in.
- Check
/api/auth/meto confirm your user record.
4. Account linking
When a user logs in via OAuth for the first time, Fabled checks in this order:
- Existing OAuth sub — returns that user immediately.
- Matching email — if a local account already exists with the same email address, the OAuth identity is linked to it automatically. The user retains all their notes and tasks.
- New user — a fresh account is created. The username defaults to the
preferred_usernameclaim from the provider; if taken,_2,_3, etc. is appended.
5. Disable local login (optional)
Once everyone is using SSO you can hide the username/password form:
LOCAL_AUTH_ENABLED: "false"
The backend will reject any POST /api/auth/login or POST /api/auth/register request with a 403. The login page will only show the SSO button.
Warning: Make sure at least one account has been linked via OAuth before disabling local login, or you will be locked out.
6. Other providers
| Provider | Issuer URL format |
|---|---|
| Authentik | https://auth.example.com/application/o/<app-slug>/ |
| Keycloak | https://keycloak.example.com/realms/<realm> |
| Authelia | https://auth.example.com |
| Zitadel | https://your-instance.zitadel.cloud |
https://accounts.google.com |
The OIDC discovery endpoint (<issuer>/.well-known/openid-configuration) must be
publicly reachable from the Fabled container (server-to-server call).