From 80d94247d31ca58004b97c9ea9fb7bd7be3c1e16 Mon Sep 17 00:00:00 2001 From: Problemsolver0070 Date: Wed, 25 Mar 2026 04:42:04 +0530 Subject: [PATCH] feat(sso): complete Authentik SSO stack with all OIDC integrations - Add Nextcloud OIDC setup script (scripts/nextcloud-oidc-setup.sh) - Add Open WebUI OIDC env vars to AI stack - Add Portainer OAuth env vars to base stack - Add config/grafana/grafana.ini with OIDC config - Mount grafana.ini in monitoring stack compose - Enhance setup-authentik.sh with --dry-run, user groups (homelab-admins, homelab-users, media-users), Nextcloud and Open WebUI providers - Update .env.example with all OAuth client vars - Update SSO README with integration tutorial for new services - Fix Grafana role mapping to use project group names - Fix Outline redirect URI to use docs.DOMAIN Co-Authored-By: Claude Opus 4.6 --- .env.example | 9 +- config/grafana/grafana.ini | 50 ++++++ scripts/nextcloud-oidc-setup.sh | 104 +++++++++++++ scripts/setup-authentik.sh | 219 +++++++++++++++++++++++---- stacks/ai/docker-compose.yml | 7 + stacks/base/docker-compose.yml | 11 ++ stacks/monitoring/docker-compose.yml | 3 +- stacks/sso/.env.example | 9 ++ stacks/sso/README.md | 143 +++++++++++++++-- 9 files changed, 511 insertions(+), 44 deletions(-) create mode 100644 config/grafana/grafana.ini create mode 100755 scripts/nextcloud-oidc-setup.sh diff --git a/.env.example b/.env.example index ab86b655..07cd3a6c 100644 --- a/.env.example +++ b/.env.example @@ -31,8 +31,9 @@ TRAEFIK_DASHBOARD_PASSWORD_HASH= AUTHENTIK_SECRET_KEY= # REQUIRED: openssl rand -base64 32 AUTHENTIK_POSTGRES_PASSWORD= # REQUIRED: strong random password AUTHENTIK_REDIS_PASSWORD= # REQUIRED: strong random password -AUTHENTIK_ADMIN_EMAIL= -AUTHENTIK_ADMIN_PASSWORD= +AUTHENTIK_BOOTSTRAP_EMAIL=admin@yourdomain.com +AUTHENTIK_BOOTSTRAP_PASSWORD= # REQUIRED: initial admin password +AUTHENTIK_BOOTSTRAP_TOKEN= # Auto-generated or: openssl rand -hex 32 AUTHENTIK_DOMAIN=auth.${DOMAIN} # OAuth2 clients — auto-filled by scripts/setup-authentik.sh @@ -44,6 +45,10 @@ OUTLINE_OAUTH_CLIENT_ID= OUTLINE_OAUTH_CLIENT_SECRET= PORTAINER_OAUTH_CLIENT_ID= PORTAINER_OAUTH_CLIENT_SECRET= +NEXTCLOUD_OAUTH_CLIENT_ID= +NEXTCLOUD_OAUTH_CLIENT_SECRET= +OPENWEBUI_OAUTH_CLIENT_ID= +OPENWEBUI_OAUTH_CLIENT_SECRET= # ----------------------------------------------------------------------------- # DATABASES (shared stack) diff --git a/config/grafana/grafana.ini b/config/grafana/grafana.ini new file mode 100644 index 00000000..48db0bfe --- /dev/null +++ b/config/grafana/grafana.ini @@ -0,0 +1,50 @@ +# ============================================================================= +# Grafana Configuration — Authentik OIDC Integration +# Docs: https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ +# +# NOTE: Most values here reference environment variables set in +# stacks/monitoring/docker-compose.yml. This file provides defaults +# and additional settings not available via env vars. +# ============================================================================= + +[server] +domain = ${GF_SERVER_DOMAIN} +root_url = ${GF_SERVER_ROOT_URL} + +[security] +admin_user = ${GF_SECURITY_ADMIN_USER} +admin_password = ${GF_SECURITY_ADMIN_PASSWORD} + +# ----------------------------------------------------------------------------- +# Authentik OIDC (Generic OAuth) +# Client ID/Secret are created by scripts/setup-authentik.sh +# and stored in the root .env file. +# ----------------------------------------------------------------------------- +[auth.generic_oauth] +enabled = true +name = Authentik +icon = signin +allow_sign_up = true +auto_login = false + +client_id = ${GF_AUTH_GENERIC_OAUTH_CLIENT_ID} +client_secret = ${GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET} +scopes = openid profile email +auth_url = ${GF_AUTH_GENERIC_OAUTH_AUTH_URL} +token_url = ${GF_AUTH_GENERIC_OAUTH_TOKEN_URL} +api_url = ${GF_AUTH_GENERIC_OAUTH_API_URL} + +# Map Authentik groups to Grafana roles +role_attribute_path = contains(groups, 'homelab-admins') && 'Admin' || contains(groups, 'homelab-users') && 'Editor' || 'Viewer' +role_attribute_strict = false + +# Signout redirect to Authentik +signout_redirect_url = ${GF_AUTH_SIGNOUT_REDIRECT_URL} + +[users] +auto_assign_org = true +auto_assign_org_role = Viewer + +[analytics] +reporting_enabled = false +check_for_updates = false diff --git a/scripts/nextcloud-oidc-setup.sh b/scripts/nextcloud-oidc-setup.sh new file mode 100755 index 00000000..d874e89d --- /dev/null +++ b/scripts/nextcloud-oidc-setup.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# ============================================================================= +# Nextcloud OIDC Setup — Authentik Integration +# Configures the Social Login app in Nextcloud to use Authentik as OIDC provider. +# +# Prerequisites: +# - Nextcloud container running (stacks/storage) +# - Authentik running with Nextcloud provider created (scripts/setup-authentik.sh) +# - NEXTCLOUD_OAUTH_CLIENT_ID and NEXTCLOUD_OAUTH_CLIENT_SECRET set in .env +# +# Usage: ./scripts/nextcloud-oidc-setup.sh +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +ROOT_DIR=$(dirname "$SCRIPT_DIR") + +# Load .env +if [ -f "$ROOT_DIR/.env" ]; then + set -a; source "$ROOT_DIR/.env"; set +a +fi + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' +log_info() { echo -e "${GREEN}[INFO]${RESET} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } +log_error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } +log_step() { echo; echo -e "${BOLD}${CYAN}==> $*${RESET}"; } + +CONTAINER_NAME="nextcloud" +AUTHENTIK_DOMAIN="${AUTHENTIK_DOMAIN:-auth.${DOMAIN}}" +CLIENT_ID="${NEXTCLOUD_OAUTH_CLIENT_ID:-}" +CLIENT_SECRET="${NEXTCLOUD_OAUTH_CLIENT_SECRET:-}" +NEXTCLOUD_DOMAIN="nextcloud.${DOMAIN}" + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- +if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then + log_error "NEXTCLOUD_OAUTH_CLIENT_ID and NEXTCLOUD_OAUTH_CLIENT_SECRET must be set in .env" + log_error "Run scripts/setup-authentik.sh first to create the Nextcloud provider." + exit 1 +fi + +if ! docker inspect "$CONTAINER_NAME" > /dev/null 2>&1; then + log_error "Nextcloud container '$CONTAINER_NAME' is not running." + exit 1 +fi + +occ() { + docker exec -u www-data "$CONTAINER_NAME" php occ "$@" +} + +# --------------------------------------------------------------------------- +# Step 1: Install Social Login app +# --------------------------------------------------------------------------- +log_step "Installing Social Login app..." +if occ app:list --output=json 2>/dev/null | grep -q '"sociallogin"'; then + log_info "Social Login app already installed" +else + occ app:install sociallogin || log_warn "sociallogin may already be installed" +fi +occ app:enable sociallogin + +# --------------------------------------------------------------------------- +# Step 2: Configure OpenID Connect provider +# --------------------------------------------------------------------------- +log_step "Configuring Authentik as OIDC provider..." + +# Social Login stores its config as a JSON blob in the Nextcloud config +occ config:app:set sociallogin custom_oidc --value="$(cat <&2; } log_step() { echo; echo -e "${BOLD}${CYAN}==> $*${RESET}"; } +log_dry() { echo -e "${YELLOW}[DRY-RUN]${RESET} $*"; } +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- AUTHENTIK_URL="https://${AUTHENTIK_DOMAIN:-auth.${DOMAIN}}" API_URL="$AUTHENTIK_URL/api/v3" TOKEN="${AUTHENTIK_BOOTSTRAP_TOKEN:-}" @@ -33,6 +74,9 @@ fi AUTH_HEADER="Authorization: Bearer $TOKEN" +# --------------------------------------------------------------------------- +# API Helpers +# --------------------------------------------------------------------------- get_default_flow() { local designation="$1" curl -sf "$API_URL/flows/instances/?designation=${designation}&ordering=slug" \ @@ -44,6 +88,45 @@ get_signing_key() { -H "$AUTH_HEADER" | jq -r '.results[0].pk' } +# --------------------------------------------------------------------------- +# Create a user group +# --------------------------------------------------------------------------- +create_group() { + local name="$1" + local is_superuser="${2:-false}" + + if $DRY_RUN; then + log_dry "Would create group: $name (superuser=$is_superuser)" + return + fi + + # Check if group already exists + local existing + existing=$(curl -sf "$API_URL/core/groups/?name=$name" \ + -H "$AUTH_HEADER" | jq -r '.results | length') + + if [ "$existing" -gt 0 ]; then + log_info "Group already exists: $name" + return + fi + + local payload + payload=$(jq -n \ + --arg name "$name" \ + --argjson su "$is_superuser" \ + '{name: $name, is_superuser: $su}') + + curl -sf -X POST "$API_URL/core/groups/" \ + -H "$AUTH_HEADER" \ + -H "Content-Type: application/json" \ + -d "$payload" > /dev/null + + log_info "Created group: $name" +} + +# --------------------------------------------------------------------------- +# Create OIDC provider + application +# --------------------------------------------------------------------------- create_oidc_provider() { local name="$1" local redirect_uri="$2" @@ -52,15 +135,25 @@ create_oidc_provider() { log_step "Creating OIDC provider: $name" + local slug + slug=$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') + + if $DRY_RUN; then + log_dry "Provider: $name" + log_dry "Slug: $slug" + log_dry "Redirect URI: $redirect_uri" + log_dry "Env vars: $client_id_var / $client_secret_var" + log_dry "Issuer URL: $AUTHENTIK_URL/application/o/$slug/" + return + fi + local flow_pk signing_key - flow_pk=$(get_default_flow authorize) + flow_pk=$(get_default_flow authorization) signing_key=$(get_signing_key) - local slug - slug=$(echo "$name" | tr '[:upper:]' '[:lower:]') local payload payload=$(jq -n \ - --arg name "${name} Provider" \ + --arg name "$name" \ --arg flow "$flow_pk" \ --arg uri "$redirect_uri" \ --arg key "$signing_key" \ @@ -87,10 +180,13 @@ create_oidc_provider() { log_info " Provider PK: $provider_pk" log_info " Client ID: $client_id" + log_info " Redirect URI: $redirect_uri" + # Write credentials to .env sed -i "s|^${client_id_var}=.*|${client_id_var}=${client_id}|" "$ROOT_DIR/.env" sed -i "s|^${client_secret_var}=.*|${client_secret_var}=${client_secret}|" "$ROOT_DIR/.env" + # Create the corresponding application local app_payload app_payload=$(jq -n \ --arg name "$name" \ @@ -106,26 +202,48 @@ create_oidc_provider() { log_info " Application created: $name" } +# =========================================================================== +# Main +# =========================================================================== + +if $DRY_RUN; then + echo + echo -e "${BOLD}${YELLOW}=== DRY RUN MODE — no changes will be made ===${RESET}" +fi + # ------------------------------------------------------------------ -# Wait for Authentik to be ready +# Wait for Authentik to be ready (skip in dry-run) # ------------------------------------------------------------------ -log_step "Waiting for Authentik API..." -for i in $(seq 1 30); do - if curl -sf "$AUTHENTIK_URL/-/health/ready/" -o /dev/null; then - log_info "Authentik is ready" - break - fi - if [ "$i" -eq 30 ]; then - log_error "Authentik did not become ready in 150s" - exit 1 - fi - echo -n "." - sleep 5 -done +if ! $DRY_RUN; then + log_step "Waiting for Authentik API..." + for i in $(seq 1 30); do + if curl -sf "$AUTHENTIK_URL/-/health/ready/" -o /dev/null; then + log_info "Authentik is ready" + break + fi + if [ "$i" -eq 30 ]; then + log_error "Authentik did not become ready in 150s" + exit 1 + fi + echo -n "." + sleep 5 + done +fi # ------------------------------------------------------------------ -# Create providers +# Step 1: Create user groups # ------------------------------------------------------------------ +log_step "Creating user groups..." + +create_group "homelab-admins" "true" +create_group "homelab-users" "false" +create_group "media-users" "false" + +# ------------------------------------------------------------------ +# Step 2: Create OIDC providers for each service +# ------------------------------------------------------------------ +log_step "Creating OIDC providers..." + create_oidc_provider \ "Grafana" \ "https://grafana.${DOMAIN}/login/generic_oauth" \ @@ -140,15 +258,62 @@ create_oidc_provider \ create_oidc_provider \ "Outline" \ - "https://outline.${DOMAIN}/auth/oidc.callback" \ + "https://docs.${DOMAIN}/auth/oidc.callback" \ "OUTLINE_OAUTH_CLIENT_ID" \ "OUTLINE_OAUTH_CLIENT_SECRET" create_oidc_provider \ "Portainer" \ - "https://portainer.${DOMAIN}/" \ + "https://portainer.${DOMAIN}" \ "PORTAINER_OAUTH_CLIENT_ID" \ "PORTAINER_OAUTH_CLIENT_SECRET" -log_step "All providers created. Credentials written to .env" -log_info "Authentik OIDC issuer: $AUTHENTIK_URL/application/o//" +create_oidc_provider \ + "Nextcloud" \ + "https://nextcloud.${DOMAIN}/apps/sociallogin/custom_oidc/authentik" \ + "NEXTCLOUD_OAUTH_CLIENT_ID" \ + "NEXTCLOUD_OAUTH_CLIENT_SECRET" + +create_oidc_provider \ + "Open-WebUI" \ + "https://ai.${DOMAIN}/oauth/oidc/callback" \ + "OPENWEBUI_OAUTH_CLIENT_ID" \ + "OPENWEBUI_OAUTH_CLIENT_SECRET" + +# ------------------------------------------------------------------ +# Summary +# ------------------------------------------------------------------ +log_step "Setup complete!" + +if $DRY_RUN; then + echo + echo -e "${BOLD}Groups that would be created:${RESET}" + echo " - homelab-admins (superuser)" + echo " - homelab-users" + echo " - media-users" + echo + echo -e "${BOLD}Providers that would be created:${RESET}" + echo " - Grafana → https://grafana.\${DOMAIN}/login/generic_oauth" + echo " - Gitea → https://git.\${DOMAIN}/user/oauth2/Authentik/callback" + echo " - Outline → https://docs.\${DOMAIN}/auth/oidc.callback" + echo " - Portainer → https://portainer.\${DOMAIN}" + echo " - Nextcloud → https://nextcloud.\${DOMAIN}/apps/sociallogin/custom_oidc/authentik" + echo " - Open-WebUI → https://ai.\${DOMAIN}/oauth/oidc/callback" + echo + echo -e "${BOLD}Run without --dry-run to apply changes.${RESET}" +else + echo + log_info "All providers created. Credentials written to .env" + log_info "Authentik OIDC issuer: $AUTHENTIK_URL/application/o//" + echo + echo -e "${BOLD}Next steps:${RESET}" + echo " 1. Restart services to pick up new OAuth credentials:" + echo " cd stacks/monitoring && docker compose up -d" + echo " cd stacks/productivity && docker compose up -d" + echo " cd stacks/ai && docker compose up -d" + echo " cd stacks/base && docker compose up -d" + echo " 2. Configure Nextcloud OIDC:" + echo " ./scripts/nextcloud-oidc-setup.sh" + echo " 3. Assign users to groups in Authentik admin UI:" + echo " https://${AUTHENTIK_DOMAIN}/if/admin/#/identity/groups" +fi diff --git a/stacks/ai/docker-compose.yml b/stacks/ai/docker-compose.yml index 1ef0e1c4..65b92aba 100644 --- a/stacks/ai/docker-compose.yml +++ b/stacks/ai/docker-compose.yml @@ -34,6 +34,13 @@ services: - OLLAMA_BASE_URL=http://ollama:11434 - WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY:-changeme-secret-32chars} - DEFAULT_LOCALE=zh-CN + # Authentik OIDC — credentials created by scripts/setup-authentik.sh + - ENABLE_OAUTH_SIGNUP=true + - OAUTH_PROVIDER_NAME=Authentik + - OAUTH_CLIENT_ID=${OPENWEBUI_OAUTH_CLIENT_ID} + - OAUTH_CLIENT_SECRET=${OPENWEBUI_OAUTH_CLIENT_SECRET} + - OPENID_PROVIDER_URL=https://${AUTHENTIK_DOMAIN}/application/o/open-webui/.well-known/openid-configuration + - OAUTH_SCOPES=openid profile email depends_on: ollama: condition: service_healthy diff --git a/stacks/base/docker-compose.yml b/stacks/base/docker-compose.yml index 7185e836..61972776 100644 --- a/stacks/base/docker-compose.yml +++ b/stacks/base/docker-compose.yml @@ -66,6 +66,17 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - portainer-data:/data + environment: + # Authentik OAuth — credentials created by scripts/setup-authentik.sh + - PORTAINER_OAUTH_CLIENT_ID=${PORTAINER_OAUTH_CLIENT_ID} + - PORTAINER_OAUTH_CLIENT_SECRET=${PORTAINER_OAUTH_CLIENT_SECRET} + - PORTAINER_OAUTH_AUTHORIZATION_URI=https://${AUTHENTIK_DOMAIN}/application/o/authorize/ + - PORTAINER_OAUTH_ACCESS_TOKEN_URI=https://${AUTHENTIK_DOMAIN}/application/o/token/ + - PORTAINER_OAUTH_RESOURCE_URI=https://${AUTHENTIK_DOMAIN}/application/o/userinfo/ + - PORTAINER_OAUTH_REDIRECT_URI=https://portainer.${DOMAIN} + - PORTAINER_OAUTH_USER_IDENTIFIER=email + - PORTAINER_OAUTH_SCOPES=openid profile email + - PORTAINER_OAUTH_SSO=true labels: - "traefik.enable=true" - "traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN}`)" diff --git a/stacks/monitoring/docker-compose.yml b/stacks/monitoring/docker-compose.yml index ea1a2718..786e9e54 100644 --- a/stacks/monitoring/docker-compose.yml +++ b/stacks/monitoring/docker-compose.yml @@ -48,7 +48,7 @@ services: - GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://${AUTHENTIK_DOMAIN}/application/o/token/ - GF_AUTH_GENERIC_OAUTH_API_URL=https://${AUTHENTIK_DOMAIN}/application/o/userinfo/ - GF_AUTH_SIGNOUT_REDIRECT_URL=https://${AUTHENTIK_DOMAIN}/application/o/grafana/end-session/ - - GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups, 'Grafana Admins') && 'Admin' || contains(groups, 'Grafana Editors') && 'Editor' || 'Viewer' + - GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups, 'homelab-admins') && 'Admin' || contains(groups, 'homelab-users') && 'Editor' || 'Viewer' - GF_USERS_AUTO_ASSIGN_ORG=true - GF_USERS_AUTO_ASSIGN_ORG_ROLE=Viewer - GF_ANALYTICS_REPORTING_ENABLED=false @@ -56,6 +56,7 @@ services: volumes: - grafana_data:/var/lib/grafana - ../../config/grafana/provisioning:/etc/grafana/provisioning:ro + - ../../config/grafana/grafana.ini:/etc/grafana/grafana.ini:ro healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"] interval: 30s diff --git a/stacks/sso/.env.example b/stacks/sso/.env.example index 3d4a315f..d3b05a21 100644 --- a/stacks/sso/.env.example +++ b/stacks/sso/.env.example @@ -21,6 +21,9 @@ AUTHENTIK_REDIS_PASSWORD= AUTHENTIK_BOOTSTRAP_EMAIL=admin@yourdomain.com AUTHENTIK_BOOTSTRAP_PASSWORD= +# API token for setup script — generate with: openssl rand -hex 32 +AUTHENTIK_BOOTSTRAP_TOKEN= + # OAuth2 client credentials — filled by scripts/setup-authentik.sh GRAFANA_OAUTH_CLIENT_ID= GRAFANA_OAUTH_CLIENT_SECRET= @@ -28,3 +31,9 @@ GITEA_OAUTH_CLIENT_ID= GITEA_OAUTH_CLIENT_SECRET= OUTLINE_OAUTH_CLIENT_ID= OUTLINE_OAUTH_CLIENT_SECRET= +PORTAINER_OAUTH_CLIENT_ID= +PORTAINER_OAUTH_CLIENT_SECRET= +NEXTCLOUD_OAUTH_CLIENT_ID= +NEXTCLOUD_OAUTH_CLIENT_SECRET= +OPENWEBUI_OAUTH_CLIENT_ID= +OPENWEBUI_OAUTH_CLIENT_SECRET= diff --git a/stacks/sso/README.md b/stacks/sso/README.md index ffa79c77..06063be0 100644 --- a/stacks/sso/README.md +++ b/stacks/sso/README.md @@ -11,11 +11,13 @@ Browser Traefik (443) │ ForwardAuth middleware → authentik-server:9000 │ - ├── auth.DOMAIN → Authentik UI (login, admin, user portal) - ├── grafana.DOMAIN → Grafana (OIDC) - ├── git.DOMAIN → Gitea (OIDC) - ├── outline.DOMAIN → Outline (OIDC) - └── portainer.DOMAIN → Portainer (OIDC) + ├── auth.DOMAIN → Authentik UI (login, admin, user portal) + ├── grafana.DOMAIN → Grafana (OIDC) + ├── git.DOMAIN → Gitea (OIDC) + ├── docs.DOMAIN → Outline (OIDC) + ├── portainer.DOMAIN → Portainer (OAuth) + ├── nextcloud.DOMAIN → Nextcloud (OIDC via Social Login) + └── ai.DOMAIN → Open WebUI (OIDC) Internal: authentik-server ─┐ @@ -64,8 +66,14 @@ docker compose up -d # 4. Wait for healthy (takes ~60s on first run) docker compose ps -# 5. Create OIDC providers for all services +# 5. Preview what setup will do +../../scripts/setup-authentik.sh --dry-run + +# 6. Create OIDC providers + user groups for all services ../../scripts/setup-authentik.sh + +# 7. Configure Nextcloud OIDC (after Nextcloud is running) +../../scripts/nextcloud-oidc-setup.sh ``` ## Environment Variables @@ -80,23 +88,127 @@ docker compose ps | `AUTHENTIK_BOOTSTRAP_TOKEN` | YES | API token for setup script | | `AUTHENTIK_DOMAIN` | YES | e.g. `auth.yourdomain.com` | -## Integrating Other Services +## User Groups + +The setup script creates three groups with different access levels: + +| Group | Role | Access | +|-------|------|--------| +| `homelab-admins` | Superuser | All services — admin dashboards, full management | +| `homelab-users` | Standard | Grafana (Editor), Gitea, Outline, Nextcloud, Open WebUI | +| `media-users` | Limited | Jellyfin, Jellyseerr only | + +Assign users to groups in the Authentik admin UI: `https://auth.DOMAIN/if/admin/#/identity/groups` + +## OIDC Integration Status -### Option A: OIDC (recommended for services with native OAuth2 support) +| Service | Method | Config Location | Status | +|---------|--------|-----------------|--------| +| Grafana | OIDC (env vars) | `stacks/monitoring/docker-compose.yml` + `config/grafana/grafana.ini` | Auto-configured | +| Gitea | OIDC (env vars) | `stacks/productivity/docker-compose.yml` | Auto-configured | +| Outline | OIDC (env vars) | `stacks/productivity/docker-compose.yml` | Auto-configured | +| Portainer | OAuth (env vars) | `stacks/base/docker-compose.yml` | Auto-configured | +| Nextcloud | OIDC (Social Login) | `scripts/nextcloud-oidc-setup.sh` | Run script after Nextcloud starts | +| Open WebUI | OIDC (env vars) | `stacks/ai/docker-compose.yml` | Auto-configured | -Run `../../scripts/setup-authentik.sh` — it automatically creates providers and writes credentials to `.env`. +## Integrating a New Service with Authentik + +Follow this guide to add SSO to any new service. + +### Option A: Native OIDC (for services with built-in OAuth2 support) + +**Step 1:** Add the provider to `scripts/setup-authentik.sh`: + +```bash +create_oidc_provider \ + "MyService" \ + "https://myservice.${DOMAIN}/auth/callback" \ + "MYSERVICE_OAUTH_CLIENT_ID" \ + "MYSERVICE_OAUTH_CLIENT_SECRET" +``` -Services with native OIDC support: Grafana, Gitea, Outline, Nextcloud, Portainer. +**Step 2:** Add env var placeholders to `.env.example` and `.env`: -### Option B: ForwardAuth (for services without OAuth2) +```bash +MYSERVICE_OAUTH_CLIENT_ID= +MYSERVICE_OAUTH_CLIENT_SECRET= +``` -Add to any service's Traefik labels: +**Step 3:** Add OIDC environment variables to the service's `docker-compose.yml`: ```yaml -traefik.http.routers..middlewares: authentik@file +environment: + - OAUTH_CLIENT_ID=${MYSERVICE_OAUTH_CLIENT_ID} + - OAUTH_CLIENT_SECRET=${MYSERVICE_OAUTH_CLIENT_SECRET} + - OAUTH_AUTHORIZE_URL=https://${AUTHENTIK_DOMAIN}/application/o/authorize/ + - OAUTH_TOKEN_URL=https://${AUTHENTIK_DOMAIN}/application/o/token/ + - OAUTH_USERINFO_URL=https://${AUTHENTIK_DOMAIN}/application/o/userinfo/ ``` -Authentik will intercept unauthenticated requests and redirect to the login page at `https://auth.DOMAIN`. +**Step 4:** Run the setup script to create the provider: + +```bash +../../scripts/setup-authentik.sh +``` + +**Step 5:** Restart the service to pick up the new credentials: + +```bash +docker compose up -d +``` + +### Option B: Traefik ForwardAuth (for services without OAuth2 support) + +ForwardAuth protects any service at the reverse-proxy level. No code changes needed. + +**Step 1:** Create an Application + Provider in Authentik admin UI: +- Go to `https://auth.DOMAIN/if/admin/` +- Create a new **Proxy Provider** (Forward auth mode) +- Set External Host to `https://myservice.DOMAIN` +- Create an Application linked to this provider + +**Step 2:** Add the middleware to the service's Traefik labels: + +```yaml +labels: + - "traefik.http.routers.myservice.middlewares=authentik@file" +``` + +The `authentik@file` middleware is defined in `config/traefik/dynamic/authentik.yml` and will: +- Redirect unauthenticated users to the Authentik login page +- Pass `X-authentik-username`, `X-authentik-groups`, `X-authentik-email` headers to the backend + +**Step 3:** Restart the service. Any unauthenticated request will now redirect to `https://auth.DOMAIN`. + +### Option C: ForwardAuth (API mode, no browser redirect) + +For APIs that need auth but should return 401 instead of redirecting: + +```yaml +labels: + - "traefik.http.routers.myapi.middlewares=authentik-basic@file" +``` + +This uses the `authentik-basic@file` middleware defined in `config/traefik/dynamic/authentik.yml`. + +## Traefik ForwardAuth Middleware + +The middleware is defined in `config/traefik/dynamic/authentik.yml`: + +```yaml +http: + middlewares: + authentik: + forwardAuth: + address: "http://authentik-server:9000/outpost.goauthentik.io/auth/traefik" + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + - X-authentik-groups + - X-authentik-email + - X-authentik-name + - X-authentik-uid +``` ## Health Check @@ -128,3 +240,6 @@ If `ghcr.io` is inaccessible, edit `docker-compose.yml` and uncomment the CN mir | OIDC redirect mismatch | Ensure `redirect_uris` in Authentik provider matches exact callback URL | | ForwardAuth loop | Ensure authentik outpost URL uses internal hostname `authentik-server:9000` not public domain | | `ghcr.io` pull timeout | Switch to CN mirror in docker-compose.yml | +| Groups not synced | Ensure `openid profile email` scopes are requested; check group claim mapping in Authentik | +| Nextcloud OIDC broken | Re-run `scripts/nextcloud-oidc-setup.sh`; check Social Login app is enabled | +| Open WebUI no login button | Verify `OPENID_PROVIDER_URL` is reachable from the container |