Wire Authelia SSO into Immich, Audiobookshelf, and Beszel
_authelia_provision_oidc_client gains an optional PKCE flag (new 5th positional arg; every existing caller updated to pass "n", producing an identical client block to before) — Audiobookshelf and Beszel's own Authelia integration docs both require require_pkce/pkce_challenge_method, which Authelia doesn't turn on by default. immich.sh: _immich_offer_authelia_oidc() is real server-side automation, not just paste-in instructions — confirmed the exact system-config "oauth" JSON field names against Immich's own config-file.md and source (not guessed, closing out the "needs one more verification pass" note this repo's own CLAUDE.md already had on file). GET/PUT exchange the whole config object, so it round-trips everything else unchanged. Needs an admin API key that doesn't exist until first web-UI visit, so it's wired into both the fresh-install path and the "update" rerun path. audiobookshelf.sh, beszel.sh: both apps' OIDC config is UI-only (checked against audiobookshelf.org and beszel.dev directly — no config API or env var for the provider fields), so their new offers automate the Authelia side and print exact paste-in values. Beszel also gets a real, separate DISABLE_PASSWORD_AUTH/USER_CREATION toggle to fully replace its login, gated behind a warning to register a working account first. Also adds Audiobookshelf and Beszel as presets in authelia.sh's own generic "Register another app" menu, and updates CLAUDE.md's OIDC verification table to match reality (Immich now wired, Audiobookshelf was wrongly listed as "high-confidence no", Beszel added).
This commit is contained in:
@@ -201,6 +201,129 @@ fi
|
||||
|
||||
register_service immich media "Self-hosted photo & video backup — like Google Photos (Immich)" 2283
|
||||
|
||||
# Offers to wire Immich's own native OAuth support to Authelia — real
|
||||
# server-side automation, not just paste-in instructions, unlike
|
||||
# Audiobookshelf/Beszel below (neither exposes a config API; Immich does).
|
||||
# Confirmed against docs.mealie.io's sibling page for Immich
|
||||
# (docs.immich.app/administration/oauth) and, since that page doesn't
|
||||
# document the underlying API, against Immich's own config-file.md and
|
||||
# GitHub source directly for the exact JSON field names under the "oauth"
|
||||
# key — not guessed. GET/PUT /api/system-config exchanges the WHOLE config
|
||||
# object (there's no partial-patch endpoint), so this only ever touches the
|
||||
# "oauth" sub-object and round-trips everything else completely unchanged
|
||||
# — the same GET-modify-PUT shape already proven in this file for the
|
||||
# storage-template step in import-photos.sh (search CURRENT_CONFIG above).
|
||||
#
|
||||
# Unlike Mealie/ActualBudget/Gitea, Immich's admin account isn't created by
|
||||
# this installer — the user creates it themselves on first web visit (see
|
||||
# "First launch" in the generated README) — so there's no API key to call
|
||||
# with at the moment a FRESH install finishes. This is deliberately called
|
||||
# from both the fresh-install path (where it'll usually just tell you to
|
||||
# come back later) and the "update" rerun path (the realistic way most
|
||||
# people actually complete this, once they have an account), same as
|
||||
# _mealie_offer_authelia_oidc's own "works from either" design.
|
||||
#
|
||||
# Args: IMMICH_DIR WEB_PORT
|
||||
_immich_offer_authelia_oidc() {
|
||||
local DIR="$1" WEB_PORT="$2"
|
||||
|
||||
[ -d "$DOCKER_DIR/authelia" ] || return 0
|
||||
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
|
||||
|
||||
echo ""
|
||||
local USE_SSO=""
|
||||
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Immich? (y/n):" "n" USE_SSO
|
||||
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
|
||||
|
||||
echo " This writes Immich's OAuth settings for you via its own API — needs an"
|
||||
echo " admin API key: Administration -> Settings -> API Keys -> New API Key"
|
||||
echo " (Admin scope). Leave blank to skip for now — safe to come back to this"
|
||||
echo " later by re-running 'sudo ./setup.sh immich' once you have one."
|
||||
local IMMICH_API_KEY=""
|
||||
prompt_text " Immich admin API key:" "" IMMICH_API_KEY
|
||||
if [ -z "$IMMICH_API_KEY" ]; then
|
||||
log_info "Skipped — no account/API key yet. Come back to this by re-running"
|
||||
log_info "'sudo ./setup.sh immich' (choose \"Manage that install\" -> update)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local IMMICH_LOCAL_URL="http://localhost:${WEB_PORT}"
|
||||
local VERIFY_CODE
|
||||
VERIFY_CODE="$(curl -s -o /dev/null -w '%{http_code}' -H "x-api-key: $IMMICH_API_KEY" "$IMMICH_LOCAL_URL/api/users/me" 2>/dev/null)"
|
||||
if [ "$VERIFY_CODE" != "200" ]; then
|
||||
log_warning "Couldn't verify that API key against Immich (HTTP $VERIFY_CODE) — skipping SSO setup."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local APP_DOMAIN
|
||||
APP_DOMAIN="$(_authelia_pick_domain "Domain Immich is reachable at (number or domain)")"
|
||||
if [ -z "$APP_DOMAIN" ]; then
|
||||
log_warning "No domain entered — skipping SSO setup."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local _2fa="" AUTH_POLICY="two_factor"
|
||||
prompt_yn " Require two-factor for Immich logins via Authelia too? (y/n):" "y" _2fa
|
||||
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
|
||||
|
||||
# Same three redirect URIs as the "Immich" preset in authelia.sh's own
|
||||
# generic OIDC menu (web login, account-linking, mobile app callback) —
|
||||
# kept identical on purpose so either path produces the same client.
|
||||
if ! _authelia_provision_oidc_client "Immich" "immich" "$AUTH_POLICY" "y" "n" \
|
||||
"https://${APP_DOMAIN}/auth/login" "https://${APP_DOMAIN}/user-settings" "app.immich:///oauth-callback"; then
|
||||
log_warning "Couldn't register Immich as an OIDC client in Authelia — skipping SSO setup."
|
||||
return 0
|
||||
fi
|
||||
local _client_secret="$OIDC_CLIENT_SECRET_PLAIN" _portal_url="$OIDC_AUTHELIA_PORTAL_URL"
|
||||
|
||||
local CURRENT_CONFIG
|
||||
CURRENT_CONFIG="$(curl -s -H "x-api-key: $IMMICH_API_KEY" "$IMMICH_LOCAL_URL/api/system-config" 2>/dev/null)"
|
||||
if [ -z "$CURRENT_CONFIG" ] || ! command -v python3 &>/dev/null; then
|
||||
log_warning "Couldn't read Immich's system config — set OAuth manually instead:"
|
||||
echo " Administration -> Settings -> OAuth Authentication"
|
||||
echo " Issuer URL: ${_portal_url}"
|
||||
echo " Client ID: immich"
|
||||
echo " Client Secret: ${_client_secret}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Secret/issuer are passed via env vars, not interpolated into the
|
||||
# python source as string literals — Authelia's generated secret uses
|
||||
# an rfc3986 charset that isn't guaranteed free of characters (a stray
|
||||
# quote, say) that would otherwise break out of a quoted Python literal.
|
||||
local UPDATED_CONFIG
|
||||
UPDATED_CONFIG="$(echo "$CURRENT_CONFIG" | OIDC_SECRET="$_client_secret" OIDC_ISSUER="$_portal_url" python3 -c "
|
||||
import sys, json, os
|
||||
config = json.load(sys.stdin)
|
||||
config['oauth']['enabled'] = True
|
||||
config['oauth']['issuerUrl'] = os.environ['OIDC_ISSUER']
|
||||
config['oauth']['clientId'] = 'immich'
|
||||
config['oauth']['clientSecret'] = os.environ['OIDC_SECRET']
|
||||
config['oauth']['scope'] = 'openid email profile'
|
||||
config['oauth']['buttonText'] = 'Login with Authelia'
|
||||
json.dump(config, sys.stdout)
|
||||
" 2>/dev/null)"
|
||||
if [ -z "$UPDATED_CONFIG" ]; then
|
||||
log_warning "Couldn't parse Immich's config — set OAuth manually: Administration -> Settings -> OAuth Authentication"
|
||||
echo " Issuer URL: ${_portal_url} Client ID: immich Client Secret: ${_client_secret}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local RESULT
|
||||
RESULT="$(curl -s -o /dev/null -w '%{http_code}' -X PUT \
|
||||
-H "x-api-key: $IMMICH_API_KEY" -H "Content-Type: application/json" \
|
||||
"$IMMICH_LOCAL_URL/api/system-config" -d "$UPDATED_CONFIG" 2>/dev/null)"
|
||||
if [ "$RESULT" = "200" ]; then
|
||||
log_success "\"Sign in with Authelia\" enabled in Immich — local login still works too."
|
||||
else
|
||||
log_warning "Couldn't set Immich's OAuth config (HTTP $RESULT) — set it manually instead:"
|
||||
echo " Administration -> Settings -> OAuth Authentication"
|
||||
echo " Issuer URL: ${_portal_url} Client ID: immich Client Secret: ${_client_secret}"
|
||||
fi
|
||||
|
||||
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "immich" "$APP_DOMAIN"
|
||||
}
|
||||
|
||||
install_immich() {
|
||||
require_docker || return 1
|
||||
|
||||
@@ -279,6 +402,15 @@ install_immich() {
|
||||
( cd "$IMMICH_DIR" && docker compose pull && docker compose up -d ) \
|
||||
&& log_success "Immich image refreshed" \
|
||||
|| log_warning "Refresh failed — check: docker compose -f $IMMICH_DIR/docker-compose.yml logs"
|
||||
# WEB_PORT isn't persisted anywhere but the compose
|
||||
# file's own port mapping — re-derive it here rather
|
||||
# than assuming the "2283" default this local started
|
||||
# with, which may not match if it was shifted at
|
||||
# install time (collision avoidance / another instance).
|
||||
local _EXISTING_PORT
|
||||
_EXISTING_PORT="$(grep -oP '^\s+- "?\K[0-9]+(?=:2283)' "$IMMICH_DIR/docker-compose.yml" 2>/dev/null | head -1)"
|
||||
[ -n "$_EXISTING_PORT" ] && WEB_PORT="$_EXISTING_PORT"
|
||||
_immich_offer_authelia_oidc "$IMMICH_DIR" "$WEB_PORT"
|
||||
return 0
|
||||
;;
|
||||
cancel)
|
||||
@@ -886,6 +1018,14 @@ IMPORT_BODY
|
||||
|
||||
configure_caddy_for_service "Immich${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${C_SERVER}:2283" "immich${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
|
||||
|
||||
# Almost always a no-op on a truly fresh install — the admin account
|
||||
# (and thus an API key) doesn't exist until the user visits the web UI
|
||||
# for the first time, which hasn't happened yet at this point in the
|
||||
# script. Still offered here for the rare case an instance is being
|
||||
# reconfigured with credentials already in hand; the update rerun path
|
||||
# above is the realistic way most people complete this.
|
||||
_immich_offer_authelia_oidc "$IMMICH_DIR" "$WEB_PORT"
|
||||
|
||||
write_readme "$IMMICH_DIR" << MD
|
||||
# Immich${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user