Merge pull request #393 from outis1one/claude/frigate-authelia-openid-0l1htj

Claude/frigate authelia openid 0l1htj
This commit is contained in:
Outis
2026-08-25 06:55:30 -04:00
committed by GitHub
8 changed files with 452 additions and 11 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ _actualbudget_offer_authelia_oidc() {
prompt_yn " Require two-factor for Actual Budget logins via Authelia too? (y/n):" "y" _2fa
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
if ! _authelia_provision_oidc_client "ActualBudget" "actualbudget" "$AUTH_POLICY" "y" \
if ! _authelia_provision_oidc_client "ActualBudget" "actualbudget" "$AUTH_POLICY" "y" "n" \
"https://${AB_OIDC_DOMAIN}/openid/callback"; then
log_warning "Couldn't register Actual Budget as an OIDC client in Authelia — skipping SSO setup."
return 0
+56
View File
@@ -198,6 +198,59 @@ fi
register_service audiobookshelf media "Audiobook & podcast server (Audiobookshelf)" 13378
# Offers to register Audiobookshelf as an Authelia OIDC client and prints
# exactly what to paste into its own settings — checked against
# audiobookshelf.org's own OIDC docs directly: config lives entirely in
# Settings -> Authentication in the app's UI, no env var or config API to
# automate the app side with (unlike Mealie/ActualBudget/Immich), so this
# only automates the Authelia half. Its own Authelia integration doc
# (authelia.com) requires PKCE — the fifth arg to
# _authelia_provision_oidc_client below.
_audiobookshelf_offer_authelia_oidc() {
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
[ -d "$DOCKER_DIR/authelia" ] || return 0
echo ""
local USE_SSO=""
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Audiobookshelf? (y/n):" "n" USE_SSO
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
local APP_DOMAIN
APP_DOMAIN="$(_authelia_pick_domain "Domain Audiobookshelf 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 Audiobookshelf logins via Authelia too? (y/n):" "y" _2fa
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
if ! _authelia_provision_oidc_client "Audiobookshelf" "audiobookshelf" "$AUTH_POLICY" "y" "y" \
"https://${APP_DOMAIN}/auth/openid/callback" "https://${APP_DOMAIN}/auth/openid/mobile-redirect" "audiobookshelf://oauth"; then
log_warning "Couldn't register Audiobookshelf as an OIDC client in Authelia — skipping SSO setup."
return 0
fi
echo ""
echo " Audiobookshelf -> Settings -> Authentication -> enable OpenID Connect"
echo " Authentication, then fill in (it wants individual endpoints, not a"
echo " discovery URL):"
echo " Issuer URL: ${OIDC_AUTHELIA_PORTAL_URL}"
echo " Authorize URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/authorization"
echo " Token URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/token"
echo " Userinfo URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/userinfo"
echo " JWKS URL: ${OIDC_AUTHELIA_PORTAL_URL}/jwks.json"
echo " Client ID: audiobookshelf"
echo " Client Secret: $OIDC_CLIENT_SECRET_PLAIN"
echo " Signing Algorithm: RS256"
echo " Allowed Mobile Redirect URIs: audiobookshelf://oauth"
echo ""
log_warning "The Client Secret above is shown once — save it now."
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "audiobookshelf" "$APP_DOMAIN"
}
install_audiobookshelf() {
require_docker || return 1
@@ -261,6 +314,7 @@ install_audiobookshelf() {
( cd "$ABS_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Audiobookshelf image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $ABS_DIR/docker-compose.yml logs"
_audiobookshelf_offer_authelia_oidc
return 0
;;
cancel)
@@ -358,6 +412,8 @@ ABS_ENV
configure_caddy_for_service "Audiobookshelf${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:80" "audiobooks${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
_audiobookshelf_offer_authelia_oidc
write_readme "$ABS_DIR" << MD
# Audiobookshelf${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
+172 -6
View File
@@ -248,10 +248,12 @@ install_authelia() {
echo " safe to re-run any time)"
echo " 14) Rename an outside-access group (e.g. \"customer1\" -> \"acme-corp\")"
echo " 15) Show every group's sites and users (site groups + user groups overview)"
echo " 16) Add/remove users from a group (pick the group, then toggle members —"
echo " the reverse of option 4's per-user group toggle)"
echo " 0) Leave as-is / exit"
echo ""
local EXISTING_CHOICE=""
prompt_text " Choice [1-15, 0 to exit]:" "0" EXISTING_CHOICE
prompt_text " Choice [1-16, 0 to exit]:" "0" EXISTING_CHOICE
case "$EXISTING_CHOICE" in
1)
add_authelia_domain
@@ -312,6 +314,10 @@ install_authelia() {
_authelia_report_groups
return 0
;;
16)
_authelia_manage_group_membership
return 0
;;
0|*)
echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)"
return 0
@@ -1967,6 +1973,108 @@ _authelia_report_groups() {
done
}
# Group-first complement to _authelia_manage_one_user()'s option 6 (which is
# user-first: pick a user, then toggle which groups they're in). This is the
# other direction — pick a group, then toggle which users are in it — for
# when you know which group you want to populate and don't want to visit
# each user one at a time. Same _authelia_toggle_group() underneath either
# way; this is purely a different entry point onto the same membership data.
#
# A group only exists here once it's been attached to at least one site
# (via site protection's "Outside access" choice) — Authelia has no notion
# of a group that isn't referenced by an access rule or a user's
# membership, so there's no separate "create an empty group" step; naming
# a new group during site protection is what creates it.
_authelia_manage_group_membership() {
local users_file="$DOCKER_DIR/authelia/config/users.yml"
local config_file="$DOCKER_DIR/authelia/config/configuration.yml"
[ -f "$users_file" ] || { log_warning "No users.yml found — install Authelia first."; return 1; }
local -a groups
mapfile -t groups < <(_authelia_list_scoped_groups "$users_file")
if [ "${#groups[@]}" -eq 0 ]; then
log_info "No outside-access groups exist yet. A group is created the first time you"
log_info "protect a site (option 10, or a service's own \"Add Sign in with Authelia\""
log_info "offer) and choose \"Outside access\" instead of \"Native\" — name it there"
log_info "(e.g. \"customer1\"), and it'll show up here afterward to manage its members."
return 0
fi
echo ""
echo " Outside-access groups:"
local gi
for gi in "${!groups[@]}"; do
echo " $((gi + 1))) ${groups[$gi]%-only}"
done
echo " 0) Cancel"
local GROUP_CHOICE=""
prompt_text " Which group? [0]:" "0" GROUP_CHOICE
if ! [[ "$GROUP_CHOICE" =~ ^[0-9]+$ ]] || [ "$GROUP_CHOICE" -lt 1 ] || [ "$GROUP_CHOICE" -gt "${#groups[@]}" ]; then
log_info "Cancelled — nothing changed."
return 0
fi
local GROUP="${groups[$((GROUP_CHOICE - 1))]}"
local -a all_users
mapfile -t all_users < <(_authelia_list_usernames "$users_file")
if [ "${#all_users[@]}" -eq 0 ]; then
log_info "No users exist yet — add one first (this menu's \"Add a new user\")."
return 0
fi
echo ""
echo " Members of '${GROUP%-only}' (* = currently a member):"
local ui u start_end start end member
for ui in "${!all_users[@]}"; do
u="${all_users[$ui]}"
start_end="$(_authelia_user_line_range "$users_file" "$u")"
start="${start_end% *}"; end="${start_end#* }"
member=" "
sed -n "${start},${end}p" "$users_file" | grep -qF " - ${GROUP}" && member="*"
echo " $((ui + 1))) [${member}] ${u}"
done
echo ""
echo " Pick by number (space-separated) to toggle — a member gets removed, a"
echo " non-member gets added. 0 (or blank) to leave unchanged."
local TOGGLE_SEL=""
prompt_text " Numbers [0]:" "0" TOGGLE_SEL
local -a TOGGLE_TOKENS
read -ra TOGGLE_TOKENS <<< "$TOGGLE_SEL"
local tk tidx tu t_range t_start t_end is_member CHANGED=0
for tk in "${TOGGLE_TOKENS[@]}"; do
[[ "$tk" =~ ^[0-9]+$ ]] || continue
[ "$tk" -ge 1 ] && [ "$tk" -le "${#all_users[@]}" ] || continue
tidx=$((tk - 1))
tu="${all_users[$tidx]}"
# Re-resolve line range before every toggle — a prior toggle in this
# same loop shifts every line after it (see the equivalent comment
# in _authelia_manage_one_user's own group-toggle option).
t_range="$(_authelia_user_line_range "$users_file" "$tu")"
t_start="${t_range% *}"; t_end="${t_range#* }"
is_member="false"
sed -n "${t_start},${t_end}p" "$users_file" | grep -qF " - ${GROUP}" && is_member="true"
if [ "$is_member" = "true" ]; then
_authelia_toggle_group "$users_file" "$t_start" "$t_end" "$GROUP" "false"
log_success "Removed ${tu} from '${GROUP%-only}'"
else
_authelia_toggle_group "$users_file" "$t_start" "$t_end" "$GROUP" "true"
log_success "Added ${tu} to '${GROUP%-only}'"
fi
CHANGED=1
done
if [ "$CHANGED" = "1" ]; then
chown 1000:1000 "$users_file" 2>/dev/null || true
local RESTART_AUTH=""
prompt_yn " Restart Authelia to apply? (y/n):" "y" RESTART_AUTH
if [[ "$RESTART_AUTH" =~ ^[Yy]$ ]]; then
(cd "$DOCKER_DIR/authelia" && docker compose restart authelia 2>/dev/null) \
&& log_success "Authelia restarted" \
|| log_warning "Restart failed — check: docker compose logs authelia"
fi
fi
}
# Menu-driven, idempotent bulk version of _authelia_ensure_admin_bypass —
# backfills the admin-bypass rule for every apex domain currently on this
# instance in one pass. install_authelia and add_authelia_domain bake the
@@ -2746,7 +2854,7 @@ _authelia_remove_oidc_client() {
# that's already registered is NOT a failure — it gets replaced (see the
# comment at that check below).
_authelia_provision_oidc_client() {
local APP_NAME="$1" CLIENT_ID="$2" AUTH_POLICY="$3" RESTART_AUTH="$4"; shift 4
local APP_NAME="$1" CLIENT_ID="$2" AUTH_POLICY="$3" RESTART_AUTH="$4" REQUIRE_PKCE="$5"; shift 5
local -a REDIRECT_URIS=("$@")
OIDC_CLIENT_SECRET_PLAIN=""
@@ -2841,11 +2949,27 @@ _authelia_provision_oidc_client() {
REDIRECT_URIS_YAML="$(printf " - '%s'\n" "${REDIRECT_URIS[@]}")"
REDIRECT_URIS_YAML="${REDIRECT_URIS_YAML%$'\n'}"
# PKCE lines are opt-in, not default — Authelia's own defaults for every
# other field here (client_secret_basic auth method for a confidential
# client, access_token_signed_response_alg: none) already match what
# Audiobookshelf/Beszel's own Authelia integration docs specify, but
# require_pkce defaults to false and has to be set explicitly for the
# apps that need it. Checked against authelia.com's own per-client
# integration pages for those two, not assumed — every existing caller
# (Mealie/ActualBudget/Vaultwarden/Gitea/Immich) passes "n" here and
# gets byte-for-byte the same client block as before this was added.
local PKCE_YAML=""
if [[ "$REQUIRE_PKCE" =~ ^[Yy]$ ]]; then
PKCE_YAML="
require_pkce: true
pkce_challenge_method: 'S256'"
fi
local CLIENT_BLOCK=" - client_id: '${CLIENT_ID}'
client_name: '${APP_NAME}'
client_secret: '${CLIENT_SECRET_HASH}'
public: false
authorization_policy: '${AUTH_POLICY}'
authorization_policy: '${AUTH_POLICY}'${PKCE_YAML}
redirect_uris:
${REDIRECT_URIS_YAML}
scopes:
@@ -2922,13 +3046,17 @@ _authelia_add_oidc_client() {
echo " 2) Vaultwarden"
echo " 3) Immich (needs multiple redirect URIs — web login, account-linking,"
echo " and the mobile app's custom-scheme callback — all registered here)"
echo " 5) Audiobookshelf (needs PKCE — checked against its own Authelia"
echo " integration docs, registered here automatically)"
echo " 6) Beszel (PocketBase-based — also needs PKCE; its own side is"
echo " configured in its Settings -> Auth providers page, not an API)"
echo " 4) Other / custom app"
echo " 0) Cancel"
echo ""
local APP_CHOICE=""
prompt_text " Choice [1-4, 0 to cancel]:" "0" APP_CHOICE
prompt_text " Choice [1-6, 0 to cancel]:" "0" APP_CHOICE
local APP_NAME="" CLIENT_ID=""
local APP_NAME="" CLIENT_ID="" REQUIRE_PKCE="n"
local -a REDIRECT_PATHS=() EXTRA_REDIRECT_URIS=()
case "$APP_CHOICE" in
1) APP_NAME="ActualBudget"; CLIENT_ID="actualbudget"; REDIRECT_PATHS=("/openid/callback") ;;
@@ -2938,6 +3066,15 @@ _authelia_add_oidc_client() {
REDIRECT_PATHS=("/auth/login" "/user-settings")
EXTRA_REDIRECT_URIS=("app.immich:///oauth-callback")
;;
5)
APP_NAME="Audiobookshelf"; CLIENT_ID="audiobookshelf"; REQUIRE_PKCE="y"
REDIRECT_PATHS=("/auth/openid/callback" "/auth/openid/mobile-redirect")
EXTRA_REDIRECT_URIS=("audiobookshelf://oauth")
;;
6)
APP_NAME="Beszel"; CLIENT_ID="beszel"; REQUIRE_PKCE="y"
REDIRECT_PATHS=("/api/oauth2-redirect")
;;
4)
prompt_text " App name (for your reference):" "" APP_NAME
[ -z "$APP_NAME" ] && { log_warning "No app name entered — nothing to do."; return 0; }
@@ -2994,7 +3131,7 @@ _authelia_add_oidc_client() {
local RESTART_AUTH=""
prompt_yn " Restart Authelia to apply? (y/n):" "y" RESTART_AUTH
_authelia_provision_oidc_client "$APP_NAME" "$CLIENT_ID" "$AUTH_POLICY" "$RESTART_AUTH" "${REDIRECT_URIS[@]}" \
_authelia_provision_oidc_client "$APP_NAME" "$CLIENT_ID" "$AUTH_POLICY" "$RESTART_AUTH" "$REQUIRE_PKCE" "${REDIRECT_URIS[@]}" \
|| return 1
local CLIENT_SECRET_PLAIN="$OIDC_CLIENT_SECRET_PLAIN"
@@ -3047,6 +3184,35 @@ _authelia_add_oidc_client() {
echo " for OAuth to work in both the browser and the Immich mobile app."
echo ""
;;
5)
echo " Audiobookshelf -> Settings -> Authentication -> enable OpenID Connect"
echo " Authentication, then fill in (checked against audiobookshelf.org's own"
echo " OIDC docs — it wants individual endpoints, not a discovery URL):"
echo " Issuer URL: https://${AUTHELIA_PORTAL_DOMAIN}"
echo " Authorize URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/authorization"
echo " Token URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/token"
echo " Userinfo URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/userinfo"
echo " JWKS URL: https://${AUTHELIA_PORTAL_DOMAIN}/jwks.json"
echo " Client ID: ${CLIENT_ID}"
echo " Client Secret: ${CLIENT_SECRET_PLAIN}"
echo " Signing Algorithm: RS256"
echo " Allowed Mobile Redirect URIs: audiobookshelf://oauth"
echo ""
;;
6)
echo " Beszel is PocketBase-based — its OAuth2 provider is a PocketBase admin"
echo " setting, not an API this script can write (checked against beszel.dev's"
echo " own docs). In the hub: Settings -> Auth providers -> OpenID Connect:"
echo " Client ID: ${CLIENT_ID}"
echo " Client Secret: ${CLIENT_SECRET_PLAIN}"
echo " Auth URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/authorization"
echo " Token URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/token"
echo " User Info URL: https://${AUTHELIA_PORTAL_DOMAIN}/api/oidc/userinfo"
echo " Register your first Beszel account with a password BEFORE touching"
echo " DISABLE_PASSWORD_AUTH/USER_CREATION in its .env — flipping those before"
echo " a working login exists risks locking the hub's UI out entirely."
echo ""
;;
esac
log_warning "The Client Secret above is shown once — it isn't stored in plaintext anywhere. Save it now."
}
+77
View File
@@ -251,6 +251,80 @@ _beszel_configure_agent() {
|| log_warning "Agent failed to start — check: docker compose -f $dir/docker-compose.yml logs beszel-agent"
}
# Registers Beszel as an Authelia OIDC client and prints exactly what to
# paste into the hub's own settings. Checked against beszel.dev's own OAuth
# docs directly: Beszel is PocketBase-based, and its OAuth2 provider config
# is a PocketBase admin-UI setting (Settings -> Auth providers), not
# something exposed by any documented API or env var — so, like
# Audiobookshelf, this only automates the Authelia half. Beszel's own
# Authelia integration doc (authelia.com) requires PKCE.
#
# Args: DIR (the .env holding DISABLE_PASSWORD_AUTH/USER_CREATION lives there)
_beszel_offer_authelia_oidc() {
local dir="$1"
declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0
[ -d "$DOCKER_DIR/authelia" ] || return 0
echo ""
local USE_SSO=""
prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Beszel? (y/n):" "n" USE_SSO
[[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0
local APP_DOMAIN
APP_DOMAIN="$(_authelia_pick_domain "Domain Beszel 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 Beszel logins via Authelia too? (y/n):" "y" _2fa
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
if ! _authelia_provision_oidc_client "Beszel" "beszel" "$AUTH_POLICY" "y" "y" \
"https://${APP_DOMAIN}/api/oauth2-redirect"; then
log_warning "Couldn't register Beszel as an OIDC client in Authelia — skipping SSO setup."
return 0
fi
echo ""
echo " In the hub: Settings -> Auth providers -> OpenID Connect:"
echo " Client ID: beszel"
echo " Client Secret: $OIDC_CLIENT_SECRET_PLAIN"
echo " Auth URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/authorization"
echo " Token URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/token"
echo " User Info URL: ${OIDC_AUTHELIA_PORTAL_URL}/api/oidc/userinfo"
echo ""
log_warning "The Client Secret above is shown once — save it now."
declare -F _authelia_scope_access >/dev/null 2>&1 && _authelia_scope_access "beszel" "$APP_DOMAIN"
# DISABLE_PASSWORD_AUTH/USER_CREATION are real, documented env vars
# (beszel.dev's own OAuth guide) — but Beszel has no default account:
# the FIRST person to register becomes admin (see this file's own
# README section), and there's no signup-fallback like Mealie's
# OIDC_SIGNUP_ENABLED convenience wording to lean on if that hasn't
# happened yet. Off by default and gated behind an explicit warning —
# flipping this before a working login exists risks locking the hub's
# UI out with no account able to reach it at all.
echo ""
local _disable_local=""
prompt_yn " Also disable Beszel's own password login, so Authelia is the only way in? Only do this AFTER you've registered a working account. (y/n):" "n" _disable_local
if [[ "$_disable_local" =~ ^[Yy]$ ]]; then
local _auto_register=""
prompt_yn " Auto-create Beszel accounts for new Authelia logins? (y/n):" "n" _auto_register
sed -i '/^DISABLE_PASSWORD_AUTH=/d; /^USER_CREATION=/d' "$dir/.env"
{
echo "DISABLE_PASSWORD_AUTH=true"
[[ "$_auto_register" =~ ^[Yy]$ ]] && echo "USER_CREATION=true"
} >> "$dir/.env"
chown "$ACTUAL_USER:$ACTUAL_USER" "$dir/.env" 2>/dev/null || true
( cd "$dir" && docker compose up -d beszel ) \
&& log_success "Beszel's own password login is now disabled — Authelia is the only way in." \
|| log_warning "Restart failed — check: docker compose -f $dir/docker-compose.yml logs beszel"
fi
}
install_beszel() {
require_docker || return 1
log_info "Installing Beszel..."
@@ -286,6 +360,7 @@ install_beszel() {
prompt_yn " The agent was never connected — set it up now? (y/n):" "y" FINISH_AGENT
[[ "$FINISH_AGENT" =~ ^[Yy]$ ]] && _beszel_configure_agent "$DIR" "http://localhost:${_WP} (or its Caddy domain, once configured)"
fi
_beszel_offer_authelia_oidc "$DIR"
return 0
;;
cancel)
@@ -423,6 +498,8 @@ BESZEL_ENV
_beszel_configure_agent "$DIR" "http://localhost:${WEB_PORT} (or its Caddy domain, once configured)"
_beszel_offer_authelia_oidc "$DIR"
write_readme "$DIR" << 'BESZEL_README'
# Beszel — lightweight server + Docker monitoring
+1 -1
View File
@@ -214,7 +214,7 @@ _gitea_offer_authelia_sso() {
prompt_yn " Require two-factor for Gitea logins via Authelia too? (y/n):" "y" _2fa
[[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor"
if ! _authelia_provision_oidc_client "Gitea" "gitea" "$AUTH_POLICY" "y" \
if ! _authelia_provision_oidc_client "Gitea" "gitea" "$AUTH_POLICY" "y" "n" \
"https://${GITEA_OIDC_DOMAIN}/user/oauth2/authelia/callback"; then
log_warning "Couldn't register Gitea as an OIDC client in Authelia — skipping SSO setup."
return 0
+140
View File
@@ -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}
+1 -1
View File
@@ -272,7 +272,7 @@ _mealie_offer_authelia_oidc() {
prompt_yn " Skip Mealie's login page entirely and jump straight to Authelia? (y/n):" "y" _auto_redirect
fi
if ! _authelia_provision_oidc_client "Mealie" "mealie" "$AUTH_POLICY" "y" "${BASE_URL}/login"; then
if ! _authelia_provision_oidc_client "Mealie" "mealie" "$AUTH_POLICY" "y" "n" "${BASE_URL}/login"; then
log_warning "Couldn't register Mealie as an OIDC client in Authelia — skipping SSO setup."
return 0
fi