The fallback hardcoded username/password/uri/totp_seed and silently destroyed live API-key fields on any enroll (bit vpentops 2026-09-09). Rebuilds the full field set from the item JSON now. https://projects.knownelement.com/issues/942#note-5514
68 lines
3.6 KiB
Bash
Executable File
68 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Enroll TOTP for a Cloudron identity (Cloudron 10 API: profile-scoped routes).
|
|
# Prereq: cloudron-oidc-login.sh <user> has run (bearer at /tmp/.oidc-bearer-<user>).
|
|
# usage: cloudron-totp-enroll.sh <cloudron-username> [vault-item]
|
|
# Flow: POST /profile/totp_secret -> seed -> TOTP code -> POST /profile/totp_enable
|
|
# -> seed into vault -> end-to-end verify via fresh TOTP login.
|
|
set -euo pipefail
|
|
U="${1:?usage: cloudron-totp-enroll.sh <cloudron-username> [vault-item]}"
|
|
ITEM="${2:-$U Cloudron}"
|
|
BASE="https://my.knownelement.com"
|
|
BEARER_FILE="/tmp/.oidc-bearer-$U"
|
|
[ -f "$BEARER_FILE" ] || { echo "FAIL: run cloudron-oidc-login.sh $U first" >&2; exit 1; }
|
|
ACCESS=$(cat "$BEARER_FILE")
|
|
|
|
SECRET=$(curl -sk -X POST "$BASE/api/v1/profile/totp_secret" \
|
|
-H "Authorization: Bearer $ACCESS" -H 'Content-Type: application/json' -d '{}' \
|
|
| sed -n 's/.*"secret":[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
[ -n "$SECRET" ] || { echo "FAIL: no totp secret returned (bearer stale? re-login)" >&2; exit 3; }
|
|
|
|
CODE=$(bash "$(dirname "$0")/totp.sh" "$SECRET")
|
|
ENABLED=$(curl -sk -X POST "$BASE/api/v1/profile/totp_enable" \
|
|
-H "Authorization: Bearer $ACCESS" -H 'Content-Type: application/json' \
|
|
-d "{\"totpToken\":\"$CODE\"}" -w '|%{http_code}')
|
|
printf '%s' "$ENABLED" | grep -q '|200$' || { echo "FAIL: totp_enable said: $(printf '%s' "$ENABLED" | head -c 150)" >&2; exit 4; }
|
|
|
|
SM() { if [ "$(id -un)" = "TSGCOO" ]; then /data2/TSGCOO/.local/bin/sm "$@" </dev/null; else sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm "$@" </dev/null; fi; }
|
|
# store seed; smcli setfield 400s ("Data missing") on some ciphers, so verify
|
|
# the write and fall back to a full item recreate with the seed included.
|
|
# The recreate MUST carry EVERY existing field (username/password/uri whether
|
|
# stored as login or as type-1 fields, plus any app-key fields) — dropping
|
|
# fields here silently destroyed live API keys once (2026-09-09).
|
|
STORED=0
|
|
if SM setfield "$ITEM" totp_seed "$SECRET" >/dev/null 2>&1; then
|
|
STORED=1
|
|
else
|
|
JSON=$(SM get "$ITEM" 2>/dev/null)
|
|
ARGS=()
|
|
# rebuild every custom field, skipping an existing totp_seed
|
|
while IFS=$'\t' read -r fname fval; do
|
|
[ -n "$fname" ] || continue
|
|
[ "$fname" = "totp_seed" ] && continue
|
|
case "$fval" in *[[:space:]]*) continue ;; esac # multiline values can't ride argv; surface below
|
|
ARGS+=("$fname=$fval")
|
|
done < <(printf '%s' "$JSON" | jq -r '.fields[]? | "\(.name)\t\(.value)"' 2>/dev/null)
|
|
LU=$(printf '%s' "$JSON" | jq -r '.login.username // empty' 2>/dev/null)
|
|
LP=$(printf '%s' "$JSON" | jq -r '.login.password // empty' 2>/dev/null)
|
|
[ -n "$LU" ] && ARGS+=("username=$LU")
|
|
[ -n "$LP" ] && ARGS+=("password=$LP")
|
|
ARGS+=("totp_seed=$SECRET")
|
|
SM rm "$ITEM" >/dev/null 2>&1
|
|
SM set "$ITEM" "${ARGS[@]}" >/dev/null 2>&1 && STORED=1
|
|
fi
|
|
[ "$STORED" = 1 ] || { echo "FAIL: could not store seed in vault ($ITEM)" >&2; exit 5; }
|
|
[ "$(SM get "$ITEM" --field totp_seed)" = "$SECRET" ] || { echo "FAIL: seed readback mismatch ($ITEM)" >&2; exit 5; }
|
|
|
|
# end-to-end verify: fresh login WITH the stored seed lands a working bearer
|
|
bash "$(dirname "$0")/cloudron-oidc-login.sh" "$U" "$ITEM" >/dev/null 2>&1
|
|
if [ -s "$BEARER_FILE" ]; then
|
|
PROF=$(curl -sk "$BASE/api/v1/profile" -H "Authorization: Bearer $(cat "$BEARER_FILE")")
|
|
WHO=$(printf '%s' "$PROF" | sed -n 's/.*"username":[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
TOTP_ON=$(printf '%s' "$PROF" | sed -n 's/.*"totpEnabled":[[:space:]]*\(true\|false\).*/\1/p')
|
|
if [ "$WHO" = "$U" ] && [ "$TOTP_ON" = "true" ]; then
|
|
echo "OK: $U 2FA enrolled (totpEnabled=true), seed in vault ($ITEM), TOTP login verified"
|
|
exit 0
|
|
fi
|
|
fi
|
|
echo "PARTIAL: verify failed — check state for $U"
|