b37e15d59a
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>
110 lines
3.9 KiB
Markdown
110 lines
3.9 KiB
Markdown
# 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
|
|
|
|
1. Log in to the Authentik admin UI.
|
|
2. Go to **Applications → Providers → Create → OAuth2/OpenID Provider**.
|
|
3. 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 match `BASE_URL` exactly, including scheme and any path prefix)*
|
|
4. Note the generated **Client ID** and **Client Secret**.
|
|
5. Go to **Applications → Create**, give it a name, and bind it to the provider you just created.
|
|
6. 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`:
|
|
|
|
```yaml
|
|
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 use
|
|
> `OIDC_CLIENT_SECRET_FILE` pointing to a Docker secret file.
|
|
|
|
Rebuild and restart:
|
|
|
|
```bash
|
|
docker compose up --build -d
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Verify
|
|
|
|
1. Open `/api/auth/status` — it should return:
|
|
```json
|
|
{ "oauth_enabled": true, "local_auth_enabled": true, ... }
|
|
```
|
|
2. Go to the login page — you should see a **"Login with Authentik"** button.
|
|
3. Click it → you are redirected to Authentik → authenticate → redirected back to Fabled → logged in.
|
|
4. Check `/api/auth/me` to confirm your user record.
|
|
|
|
---
|
|
|
|
## 4. Account linking
|
|
|
|
When a user logs in via OAuth for the first time, Fabled checks in this order:
|
|
|
|
1. **Existing OAuth sub** — returns that user immediately.
|
|
2. **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.
|
|
3. **New user** — a fresh account is created. The username defaults to the `preferred_username` claim 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:
|
|
|
|
```yaml
|
|
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` |
|
|
| Google | `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).
|