Merge pull request #393 from outis1one/claude/frigate-authelia-openid-0l1htj
Claude/frigate authelia openid 0l1htj
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user