Merge pull request #351 from outis1one/claude/openid-authelia-actual-budget-oovjc5

Add Immich as a third OIDC client preset in authelia.sh, an 'Edit an
existing user' flow, and fix SMTP claims / a stale Authelia service
list.
This commit is contained in:
Claude
2026-08-17 15:06:31 +00:00
2 changed files with 353 additions and 24 deletions
+336 -23
View File
@@ -226,14 +226,16 @@ install_authelia() {
echo " 1) Add another protected domain to this instance (non-destructive —"
echo " one Authelia+Redis, multiple independent apex domains/logins)"
echo " 2) Add a new user (creates a users.yml entry + password hash)"
echo " 3) Register an app to log in VIA Authelia (OIDC/SSO — e.g. ActualBudget,"
echo " 3) Edit an existing user (email, password reset, 2FA reset/exempt,"
echo " promote/demote admin)"
echo " 4) Register an app to log in VIA Authelia (OIDC/SSO — e.g. ActualBudget,"
echo " Vaultwarden, or any other app with its own \"Enable OpenID\" setting)"
echo " 4) Reconfigure from scratch (regenerates secrets/users — breaks"
echo " 5) Reconfigure from scratch (regenerates secrets/users — breaks"
echo " existing sessions for every domain already on this instance)"
echo " 5) Leave as-is"
echo " 6) Leave as-is"
echo ""
local EXISTING_CHOICE=""
prompt_text " Choice [1/2/3/4/5]:" "5" EXISTING_CHOICE
prompt_text " Choice [1/2/3/4/5/6]:" "6" EXISTING_CHOICE
case "$EXISTING_CHOICE" in
1)
add_authelia_domain
@@ -244,10 +246,14 @@ install_authelia() {
return 0
;;
3)
_authelia_add_oidc_client
edit_authelia_user
return 0
;;
4)
_authelia_add_oidc_client
return 0
;;
5)
: # fall through to the full reinstall flow below
;;
*)
@@ -567,9 +573,13 @@ docker compose down # stop
## Users
- Login with the **username** (not email). Admin user: \`${AUTHELIA_ADMIN_USER}\`.
- Tell users to click **Forgot Password** on first login to set their own
password (Authelia emails a reset link via SMTP), or use Authelia's own
Settings page once logged in — that works even without SMTP configured.
- Both self-service paths need working SMTP: **Forgot Password** on the login
screen emails a reset link, and even the in-portal **Settings → Change
Password** page (for an already-logged-in user) sends a one-time code to
their email to confirm the change — confirmed live, it is not a
no-email path despite Authelia describing it as an in-session action.
If SMTP isn't working yet, use the admin-side reset instead (next line),
which never touches email.
- **Add a user:** re-run this installer (\`sudo ./setup.sh authelia\` or
\`sudo bash authelia.sh\`) and choose **"Add a new user"** from the menu —
it prompts for username/email/display name, generates the password hash,
@@ -772,10 +782,10 @@ add_authelia_user() {
echo ""
echo " Add a new user to this Authelia instance."
echo " They log in with their username (not email). A temporary password"
echo " is generated below — if SMTP isn't working, hand it to them directly"
echo " instead of relying on \"Forgot Password\"; they can set their own"
echo " password afterward from Authelia's own Settings page (no email"
echo " required for that), or via the reset email once SMTP works."
echo " is generated below — hand it to them directly. \"Forgot Password\""
echo " and Authelia's own Settings → Change Password both require working"
echo " SMTP (both email a one-time code), so until that's fixed, use this"
echo " menu's \"Edit an existing user\" → \"Reset password\" for future resets."
echo ""
local NEW_USERNAME="" NEW_DISPLAY="" NEW_EMAIL="" NEW_ADMIN=""
prompt_text " Username (lowercase, no spaces):" "" NEW_USERNAME
@@ -837,11 +847,273 @@ ${GROUPS_BLOCK}"
echo " New user: ${NEW_USERNAME}"
echo " Temp password: ${TEMP_PASS}"
echo " Give this to them directly (it's shown once, nothing stores it in"
echo " plaintext). They can log in with it as-is, then change it from"
echo " Authelia's own Settings page — no working SMTP required for that."
echo " plaintext). They can log in with it as-is and keep using it, or"
echo " change it themselves from Authelia's Settings page — but that page"
echo " emails a one-time code to confirm the change, so it needs working"
echo " SMTP. Without SMTP, use this menu's \"Edit an existing user\" →"
echo " \"Reset password\" instead — that one never touches email."
echo ""
}
# ── edit_authelia_user() helpers ──────────────────────────────────────────────
# All of these operate on a caller-supplied line range or file, never scan the
# whole file themselves, so an edit to one user's block can't bleed into a
# neighboring user (or, for the 2FA-exempt helpers, one user's exemption rule
# can't be mistaken for another's — verified against multi-user/multi-domain
# fixtures before this shipped, since a bad access_control edit here would
# break every protected domain on the instance, not just this one user).
_authelia_list_usernames() {
local users_file="$1"
awk '/^users:$/{f=1; next} f && /^ [A-Za-z0-9_-]+:$/{gsub(/^ /,""); gsub(/:$/,""); print}' "$users_file"
}
# Prints "<start_line> <end_line>" (1-indexed, inclusive) spanning just the
# given user's block in users.yml.
_authelia_user_line_range() {
local users_file="$1" username="$2"
awk -v user="$username" '
BEGIN{start=0; end=0}
/^ [A-Za-z0-9_-]+:$/ {
if (start>0 && end==0) { end=NR-1 }
if ($0 ~ "^ "user":$") { start=NR }
}
END {
if (start>0 && end==0) { end=NR }
print start, end
}
' "$users_file"
}
# Replaces the first " <field>: ..." line found within [start,end] with
# "newline" verbatim (caller supplies correct quoting for that field).
_authelia_set_user_field() {
local users_file="$1" start="$2" end="$3" field="$4" newline="$5"
awk -v s="$start" -v e="$end" -v field="$field" -v newline="$newline" '
NR>=s && NR<=e && $0 ~ "^ "field":" { print newline; next }
{ print }
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
}
# enable=true adds "- admins" under this user's groups: (no-op if already
# present); enable=false removes it. Scoped to [start,end] so it can't touch
# another user's groups list.
_authelia_toggle_admin() {
local users_file="$1" start="$2" end="$3" enable="$4"
if [ "$enable" = "true" ]; then
if ! sed -n "${start},${end}p" "$users_file" | grep -q '^ - admins$'; then
awk -v s="$start" -v e="$end" '
{ print }
NR>=s && NR<=e && /^ groups:$/ { print " - admins" }
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
fi
else
awk -v s="$start" -v e="$end" '
NR>=s && NR<=e && /^ - admins$/ { next }
{ print }
' "$users_file" > "$users_file.tmp" && mv "$users_file.tmp" "$users_file"
fi
}
# action="exempt": inserts a "policy: one_factor / subject: user:<name>" rule
# immediately before EVERY plain "policy: two_factor" catch-all domain rule in
# configuration.yml (handles multi-domain instances from add_authelia_domain
# automatically). action="restore": removes only this user's own such rules,
# leaving any other user's exemptions and the catch-all rules untouched.
# Caller is responsible for the idempotency check (only offer "exempt" in the
# menu when not already exempt, and vice versa) — this helper doesn't dedupe.
_authelia_set_2fa_exempt() {
local config_file="$1" username="$2" action="$3"
if [ "$action" = "exempt" ]; then
awk -v user="$username" '
{ lines[NR]=$0 }
END {
for (i=1; i<=NR; i++) {
if (lines[i] ~ /^ - domain:/ && lines[i+1] ~ /policy: two_factor/) {
domain = lines[i]
sub(/^ - domain: /, "", domain)
print " - domain: " domain
print " policy: one_factor"
print " subject: \"user:" user "\""
}
print lines[i]
}
}
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
else
awk -v user="$username" '
{ lines[NR]=$0 }
END {
for (i=1; i<=NR; i++) {
if (lines[i] ~ /^ - domain:/ && lines[i+1] ~ /policy: one_factor/ && lines[i+2] ~ ("subject: \"user:" user "\"")) {
i += 2
continue
}
print lines[i]
}
}
' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
fi
chown 1000:1000 "$config_file" 2>/dev/null || true
}
# Interactive: pick an existing user from users.yml, then act on them —
# edit email/display name, force a password reset, reset their 2FA device,
# toggle whether they need 2FA at all, or toggle admin group membership.
# Loops so multiple actions can be applied to the same user in one pass.
edit_authelia_user() {
local AUTHELIA_DIR="$DOCKER_DIR/authelia"
local USERS_FILE="$AUTHELIA_DIR/config/users.yml"
local CONFIG_FILE="$AUTHELIA_DIR/config/configuration.yml"
if [ ! -f "$USERS_FILE" ]; then
log_warning "No users.yml found at $USERS_FILE — install Authelia first."
return 1
fi
local -a USERNAMES
mapfile -t USERNAMES < <(_authelia_list_usernames "$USERS_FILE")
if [ "${#USERNAMES[@]}" -eq 0 ]; then
log_warning "No users found in $USERS_FILE."
return 0
fi
echo ""
echo " Existing users:"
local i=1 u
for u in "${USERNAMES[@]}"; do
echo " $i) $u"
i=$((i + 1))
done
echo ""
local SEL=""
prompt_text " Select a user by number (blank to cancel):" "" SEL
if [ -z "$SEL" ] || ! [[ "$SEL" =~ ^[0-9]+$ ]] || [ "$SEL" -lt 1 ] || [ "$SEL" -gt "${#USERNAMES[@]}" ]; then
log_info "Cancelled."
return 0
fi
local TARGET="${USERNAMES[$((SEL - 1))]}"
local CONTINUE="y"
while [[ "$CONTINUE" =~ ^[Yy]$ ]]; do
local RANGE START END
RANGE="$(_authelia_user_line_range "$USERS_FILE" "$TARGET")"
START="${RANGE% *}"; END="${RANGE#* }"
local IS_ADMIN="no"
sed -n "${START},${END}p" "$USERS_FILE" | grep -q '^ - admins$' && IS_ADMIN="yes"
local IS_EXEMPT="no"
[ -f "$CONFIG_FILE" ] && grep -qF "subject: \"user:${TARGET}\"" "$CONFIG_FILE" && IS_EXEMPT="yes"
echo ""
echo " Editing user: $TARGET (admin: $IS_ADMIN, 2FA-exempt: $IS_EXEMPT)"
echo " 1) Edit email / display name"
echo " 2) Reset password"
echo " 3) Reset 2FA device (they register a new one on next login)"
if [ "$IS_EXEMPT" = "yes" ]; then
echo " 4) Restore the 2FA requirement for this user"
else
echo " 4) Exempt this user from 2FA (one_factor only — weakens their account)"
fi
if [ "$IS_ADMIN" = "yes" ]; then
echo " 5) Demote from admin"
else
echo " 5) Promote to admin"
fi
echo " 6) Done with this user"
echo ""
local ACTION=""
prompt_text " Choice [1-6]:" "6" ACTION
case "$ACTION" in
1)
local CUR_EMAIL CUR_DISPLAY NEW_EMAIL NEW_DISPLAY
CUR_EMAIL="$(sed -n "${START},${END}p" "$USERS_FILE" | grep '^ email:' | sed 's/^ email: *//')"
CUR_DISPLAY="$(sed -n "${START},${END}p" "$USERS_FILE" | grep '^ displayname:' | sed 's/^ displayname: *//; s/^"//; s/"$//')"
prompt_text " New email [$CUR_EMAIL]:" "$CUR_EMAIL" NEW_EMAIL
prompt_text " New display name [$CUR_DISPLAY]:" "$CUR_DISPLAY" NEW_DISPLAY
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "email" " email: ${NEW_EMAIL}"
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "displayname" " displayname: \"${NEW_DISPLAY}\""
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
log_success "Updated $TARGET's email/display name."
;;
2)
log_info "Generating a new temporary password + hash..."
local NEW_TEMP_PASS NEW_HASH
NEW_TEMP_PASS="$(_authelia_gen_temp_password)"
NEW_HASH=$(docker run --rm authelia/authelia:4.39.20 \
authelia crypto hash generate argon2 --password "$NEW_TEMP_PASS" 2>/dev/null \
| grep -oP '(?<=Digest: ).*')
if [ -z "$NEW_HASH" ]; then
log_warning "Couldn't generate the password hash automatically — nothing changed. Try again."
else
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "password" " password: \"${NEW_HASH}\""
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
log_success "Password reset for $TARGET."
echo " New password: ${NEW_TEMP_PASS}"
echo " Give this to them directly — shown once, not stored in plaintext anywhere."
fi
;;
3)
if docker ps --format '{{.Names}}' | grep -q '^authelia$'; then
if docker exec authelia authelia storage user totp delete "$TARGET" --config /config/configuration.yml 2>/dev/null; then
log_success "TOTP device reset for $TARGET — they'll register a new one on next login."
else
log_warning "No TOTP device found for $TARGET (or the delete failed) — check: docker compose logs authelia"
fi
echo " WebAuthn devices (if any) aren't covered by this option — reset those manually with:"
echo " docker exec authelia authelia storage user webauthn delete --username $TARGET --config /config/configuration.yml"
else
log_warning "Authelia isn't running — start it first: cd $AUTHELIA_DIR && docker compose up -d"
fi
;;
4)
if [ "$IS_EXEMPT" = "yes" ]; then
_authelia_set_2fa_exempt "$CONFIG_FILE" "$TARGET" "restore"
log_success "Restored the two_factor requirement for $TARGET."
else
local CONFIRM_EXEMPT=""
prompt_yn " $TARGET will be able to log in with just a password (no 2FA) on every domain this instance protects. Continue? (y/n):" "n" CONFIRM_EXEMPT
if [[ "$CONFIRM_EXEMPT" =~ ^[Yy]$ ]]; then
_authelia_set_2fa_exempt "$CONFIG_FILE" "$TARGET" "exempt"
log_success "$TARGET no longer needs 2FA (one_factor only)."
else
log_info "Left as-is."
fi
fi
;;
5)
if [ "$IS_ADMIN" = "yes" ]; then
_authelia_toggle_admin "$USERS_FILE" "$START" "$END" "false"
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
log_success "$TARGET demoted from admin."
else
_authelia_toggle_admin "$USERS_FILE" "$START" "$END" "true"
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
log_success "$TARGET promoted to admin."
fi
;;
*)
ACTION="6"
;;
esac
if [[ "$ACTION" =~ ^[1245]$ ]]; then
local RESTART_AUTH=""
prompt_yn " Restart Authelia to apply this change? (y/n):" "y" RESTART_AUTH
if [ "$RESTART_AUTH" = "y" ] || [ "$RESTART_AUTH" = "Y" ]; then
(cd "$AUTHELIA_DIR" && docker compose restart authelia 2>/dev/null) \
&& log_success "Authelia restarted" \
|| log_warning "Restart failed — check: docker compose logs authelia"
fi
echo ""
prompt_yn " Do something else with $TARGET? (y/n):" "n" CONTINUE
else
CONTINUE="n"
fi
done
}
# Enables Authelia's OIDC PROVIDER feature — a distinct thing from the
# forward_auth (proxy-auth) setup install_authelia() already does. forward_auth
# gates a whole Caddy site behind an Authelia login page before the request
@@ -952,15 +1224,23 @@ _authelia_add_oidc_client() {
echo ""
echo " 1) ActualBudget"
echo " 2) Vaultwarden"
echo " 3) Other / custom app"
echo " 3) Immich (needs multiple redirect URIs — web login, account-linking,"
echo " and the mobile app's custom-scheme callback — all registered here)"
echo " 4) Other / custom app"
echo ""
local APP_CHOICE=""
prompt_text " Choice [1/2/3]:" "3" APP_CHOICE
prompt_text " Choice [1/2/3/4]:" "4" APP_CHOICE
local APP_NAME="" CLIENT_ID="" REDIRECT_PATH=""
local APP_NAME="" CLIENT_ID=""
local -a REDIRECT_PATHS=() EXTRA_REDIRECT_URIS=()
case "$APP_CHOICE" in
1) APP_NAME="ActualBudget"; CLIENT_ID="actualbudget"; REDIRECT_PATH="/openid/callback" ;;
2) APP_NAME="Vaultwarden"; CLIENT_ID="vaultwarden"; REDIRECT_PATH="/identity/connect/oidc-signin" ;;
1) APP_NAME="ActualBudget"; CLIENT_ID="actualbudget"; REDIRECT_PATHS=("/openid/callback") ;;
2) APP_NAME="Vaultwarden"; CLIENT_ID="vaultwarden"; REDIRECT_PATHS=("/identity/connect/oidc-signin") ;;
3)
APP_NAME="Immich"; CLIENT_ID="immich"
REDIRECT_PATHS=("/auth/login" "/user-settings")
EXTRA_REDIRECT_URIS=("app.immich:///oauth-callback")
;;
*)
prompt_text " App name (for your reference):" "" APP_NAME
[ -z "$APP_NAME" ] && { log_warning "No app name entered — nothing to do."; return 0; }
@@ -968,10 +1248,12 @@ _authelia_add_oidc_client() {
prompt_text " Client ID [${CLIENT_ID}]:" "$CLIENT_ID" CLIENT_ID
echo " Check ${APP_NAME}'s own OIDC/SSO docs for its exact redirect URI path"
echo " (often something like /oauth/callback, /auth/callback, /sso/callback)."
prompt_text " Redirect URI path (starting with /):" "" REDIRECT_PATH
local _redirect_path=""
prompt_text " Redirect URI path (starting with /):" "" _redirect_path
[ -n "$_redirect_path" ] && REDIRECT_PATHS=("$_redirect_path")
;;
esac
if [ -z "$CLIENT_ID" ] || [ -z "$REDIRECT_PATH" ]; then
if [ -z "$CLIENT_ID" ] || { [ "${#REDIRECT_PATHS[@]}" -eq 0 ] && [ "${#EXTRA_REDIRECT_URIS[@]}" -eq 0 ]; }; then
log_warning "Missing client ID or redirect path — nothing to do."
return 0
fi
@@ -989,7 +1271,18 @@ _authelia_add_oidc_client() {
log_warning "No domain entered — nothing to do."
return 0
fi
local REDIRECT_URI="https://${APP_DOMAIN}${REDIRECT_PATH}"
# Domain-relative paths (web login, account-linking, ...) plus any
# already-complete URIs that aren't domain-based (Immich's mobile app
# custom-scheme callback isn't reached over https at all).
local -a REDIRECT_URIS=()
local _p
for _p in "${REDIRECT_PATHS[@]}"; do
REDIRECT_URIS+=("https://${APP_DOMAIN}${_p}")
done
for _p in "${EXTRA_REDIRECT_URIS[@]}"; do
REDIRECT_URIS+=("$_p")
done
local _2fa="" AUTH_POLICY="two_factor"
prompt_yn " Require two-factor for ${APP_NAME} logins too? (y/n):" "y" _2fa
@@ -1011,13 +1304,17 @@ _authelia_add_oidc_client() {
grep -q '^ clients: \[\]$' "$CONFIG_FILE" && sed -i 's/^ clients: \[\]$/ clients:/' "$CONFIG_FILE"
local REDIRECT_URIS_YAML
REDIRECT_URIS_YAML="$(printf " - '%s'\n" "${REDIRECT_URIS[@]}")"
REDIRECT_URIS_YAML="${REDIRECT_URIS_YAML%$'\n'}"
local CLIENT_BLOCK=" - client_id: '${CLIENT_ID}'
client_name: '${APP_NAME}'
client_secret: '${CLIENT_SECRET_HASH}'
public: false
authorization_policy: '${AUTH_POLICY}'
redirect_uris:
- '${REDIRECT_URI}'
${REDIRECT_URIS_YAML}
scopes:
- 'openid'
- 'profile'
@@ -1077,6 +1374,22 @@ _authelia_add_oidc_client() {
echo " a vault other people already use."
echo ""
;;
3)
echo " Immich → Administration → Settings → OAuth Authentication:"
echo " Issuer URL: https://auth.${AUTHELIA_DOMAIN}"
echo " (Immich appends /.well-known/openid-configuration itself — paste"
echo " just the base URL above, not the full Discovery URL from earlier.)"
echo " Client ID: ${CLIENT_ID}"
echo " Client Secret: ${CLIENT_SECRET_PLAIN}"
echo " Scope: openid email profile"
echo " Enable OAuth login on that same settings page, then check its other"
echo " toggles there (auto-register new accounts, storage label claim, etc.)"
echo " — those are Immich-side choices this script doesn't set for you."
echo " Three redirect URIs were registered above: the web login, the"
echo " account-linking page, and the mobile app's callback — all needed"
echo " for OAuth to work in both the browser and the Immich mobile app."
echo ""
;;
esac
log_warning "The Client Secret above is shown once — it isn't stored in plaintext anywhere. Save it now."
}