scripts: fix oidc code extraction + Cloudron 10 totp routes
- sed kept the URL prefix in the auth-code extraction; the garbage code was the cause of every invalid_grant on the token exchange - enrollment now uses the Cloudron 10 profile-scoped routes mined from the dashboard bundle: POST /api/v1/profile/totp_secret and POST /api/v1/profile/totp_enable - seed storage verifies the setfield write and falls back to a full vault item recreate (smcli setfield 400s on some ciphers) - prompt=login added to the auth URL for session-switch flows Verified: 32 COO-org identities enrolled end-to-end (seed in vault + real TOTP login as each identity). Full detail on the #942 comment. https://projects.knownelement.com/issues/942
This commit is contained in:
@@ -22,7 +22,7 @@ PW=$(SM get "$ITEM" --field password)
|
||||
SEED=$(SM get "$ITEM" --field totp_seed 2>/dev/null || true)
|
||||
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n' | cut -c1-64)
|
||||
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')
|
||||
AUTH="$BASE/openid/auth?client_id=$CLIENT&response_type=code&scope=openid%20email%20profile&redirect_uri=https%3A%2F%2Fmy.knownelement.com%2Fauthcallback.html&code_challenge=$CHALLENGE&code_challenge_method=S256"
|
||||
AUTH="$BASE/openid/auth?client_id=$CLIENT&response_type=code&scope=openid%20email%20profile&redirect_uri=https%3A%2F%2Fmy.knownelement.com%2Fauthcallback.html&code_challenge=$CHALLENGE&code_challenge_method=S256&prompt=login"
|
||||
JAR="/tmp/oidc-jar-$U.txt"; rm -f "$JAR"
|
||||
|
||||
INT=$(curl -sk -o /dev/null -w '%{url_effective}' -c "$JAR" -L --max-redirs 8 "$AUTH")
|
||||
@@ -56,10 +56,14 @@ case "$CALLBACK" in
|
||||
-d '' | grep -i '^location:' | head -1 | tr -d '\r' | sed 's/^[Ll]ocation: //')
|
||||
[ -n "$CLOC" ] || { echo "FAIL: consent confirm produced no redirect" >&2; exit 6; }
|
||||
case "$CLOC" in http*) : ;; *) CLOC="$BASE$CLOC" ;; esac
|
||||
[ "$DBG" = 1 ] && echo "DBG cloc: ${CLOC:0:90}" >&2
|
||||
CALLBACK=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$CLOC")
|
||||
;;
|
||||
esac
|
||||
CODE=$(printf '%s' "$CALLBACK" | sed -n 's/[?&]code=\([^&]*\).*/\1/p')
|
||||
CODE=$(printf '%s' "$CALLBACK" | sed -n 's/^.*[?&]code=\([^&]*\).*$/\1/p')
|
||||
[ "$DBG" = 1 ] && { echo "DBG callback: ${CALLBACK:0:120}" >&2; echo "DBG code raw: $CODE" >&2; }
|
||||
# codes may arrive percent-encoded in the Location header; decode before exchange
|
||||
CODE=$(printf '%s' "$CODE" | perl -pe 's/%([0-9A-Fa-f]{2})/chr(hex($1))/ge')
|
||||
[ "$DBG" = 1 ] && echo "DBG code len: ${#CODE}" >&2
|
||||
[ -n "$CODE" ] || { echo "FAIL: no authorization code at callback: $CALLBACK" >&2; exit 6; }
|
||||
|
||||
@@ -71,8 +75,8 @@ TOKRESP=$(curl -sk -b "$JAR" -c "$JAR" -X POST "$BASE/openid/token" \
|
||||
--data-urlencode "redirect_uri=$REDIRECT" \
|
||||
--data-urlencode "code_verifier=$VERIFIER")
|
||||
ACCESS=$(printf '%s' "$TOKRESP" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
|
||||
[ -n "$ACCESS" ] || { echo "FAIL: token exchange: $(printf '%s' "$TOKRESP" | head -c 120)" >&2; exit 7; }
|
||||
[ -n "$ACCESS" ] || { echo "FAIL: token exchange: $TOKRESP" >&2; exit 7; }
|
||||
printf '%s' "$ACCESS" > "/tmp/.oidc-bearer-$U"
|
||||
chmod 600 "/tmp/.oidc-bearer-$U"
|
||||
WHO=$(curl -sk "$BASE/api/v1/profile" -H "Authorization: Bearer $ACCESS" | sed -n 's/.*"username":"\([^"]*\)".*/\1/p')
|
||||
WHO=$(curl -sk "$BASE/api/v1/profile" -H "Authorization: Bearer $ACCESS" | sed -n 's/.*"username":[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
echo "OK: bearer for '$WHO' at /tmp/.oidc-bearer-$U (session jar $JAR)"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Enroll TOTP for an identity (2FA must NOT be enforced yet — run
|
||||
# cloudron-oidc-login.sh first, or admin totp_disable for seed-lost users).
|
||||
# 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]
|
||||
# Captures the seed into the vault item and verifies end-to-end.
|
||||
# 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}"
|
||||
@@ -10,23 +11,42 @@ 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")
|
||||
MYUID=$(curl -sk "$BASE/api/v1/profile" -H "Authorization: Bearer $ACCESS" | sed -n 's/.*"id":"\(uid-[^"]*\)".*/\1/p')
|
||||
[ -n "$MYUID" ] || { echo "FAIL: bearer rejected" >&2; exit 2; }
|
||||
SECRET=$(curl -sk "$BASE/api/v1/users/$MYUID/totp_setup" -H "Authorization: Bearer $ACCESS" | sed -n 's/.*"secret":"\([^"]*\)".*/\1/p')
|
||||
if [ -z "$SECRET" ]; then
|
||||
SECRET=$(curl -sk -X POST "$BASE/api/v1/users/$MYUID/totp_setup" -H "Authorization: Bearer $ACCESS" -H 'Content-Type: application/json' -d '{}' | sed -n 's/.*"secret":"\([^"]*\)".*/\1/p')
|
||||
fi
|
||||
[ -n "$SECRET" ] || { echo "FAIL: no totp secret returned" >&2; exit 3; }
|
||||
|
||||
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")
|
||||
ENAB=$(curl -sk -X POST "$BASE/api/v1/users/$MYUID/totp_setup" -H "Authorization: Bearer $ACCESS" -H 'Content-Type: application/json' -d "{\"totpSecret\":\"$SECRET\",\"totpToken\":\"$CODE\"}")
|
||||
case "$ENAB" in *Invalid*|*404*|*"No such route"*) echo "PARTIAL: enable said: $(printf '%s' "$ENAB" | head -c 100)";; esac
|
||||
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; }
|
||||
SM setfield "$ITEM" totp_seed "$SECRET" >/dev/null
|
||||
# end-to-end verify: fresh login with the stored seed
|
||||
# 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
|
||||
STORED=0
|
||||
if SM setfield "$ITEM" totp_seed "$SECRET" >/dev/null 2>&1; then
|
||||
STORED=1
|
||||
else
|
||||
VU=$(SM get "$ITEM" --field username); VP=$(SM get "$ITEM" --field password); VR=$(SM get "$ITEM" --field uri 2>/dev/null || true)
|
||||
[ -n "$VR" ] || VR="$BASE"
|
||||
SM rm "$ITEM" >/dev/null 2>&1
|
||||
SM set "$ITEM" "username=$VU" "password=$VP" "uri=$VR" "totp_seed=$SECRET" >/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
|
||||
WHO=$(curl -sk "$BASE/api/v1/profile" -H "Authorization: Bearer $(cat "$BEARER_FILE")" | sed -n 's/.*"username":"\([^"]*\)".*/\1/p')
|
||||
[ "$WHO" = "$U" ] && echo "OK: $U 2FA enrolled, seed in vault ($ITEM), TOTP login verified as $WHO" \
|
||||
&& exit 0
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user