Files
KNELAgentIdentityProvisioning/skills/agent-provisioning/scripts/cloudron-oidc-login.sh
T
mrcharles 627948a98e 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
2026-09-09 12:34:40 -05:00

83 lines
4.5 KiB
Bash
Executable File

#!/bin/bash
# Cloudron OIDC dashboard login (SPA-mimic, PKCE) for an identity.
# usage: cloudron-oidc-login.sh <cloudron-username> [vault-item]
# Performs: prompt=login auth -> interaction login (password + TOTP from
# vault totp_seed if present) -> consent confirm -> code exchange.
# Writes the access token to /tmp/.oidc-bearer-<username> and a session
# cookie jar /tmp/oidc-jar-<username>.txt. Prints checkpoints to stderr.
set -uo pipefail
U="${1:?usage: cloudron-oidc-login.sh <cloudron-username> [vault-item]}"
ITEM="${2:-$U Cloudron}"
BASE="https://my.knownelement.com"
CLIENT="cid-webadmin"
REDIRECT="$BASE/authcallback.html" # used in the auth request below via explicit URL encoding
DBG="${CLOUDRON_SELF_DEBUG:-0}"
if [ "$(id -un)" = "TSGCOO" ]; then
SM() { /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }
else
SM() { sudo -u TSGCOO /data2/TSGCOO/.local/bin/sm "$@" </dev/null; }
fi
PW=$(SM get "$ITEM" --field password)
[ -n "$PW" ] || { echo "FAIL: no password in vault item $ITEM" >&2; exit 1; }
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&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")
[ "$DBG" = 1 ] && echo "DBG int: ${INT:0:70}" >&2
case "$INT" in */openid/interaction/*) : ;; *) echo "FAIL: no interaction (auto-resolved session?)"; exit 3;; esac
UIDPATH=$(printf '%s' "$INT" | grep -o '/openid/interaction/[^?]*')
BODY="{\"username\":\"$U\",\"password\":\"$PW\""
if [ -n "$SEED" ]; then
TCODE=$(bash "$(dirname "$0")/totp.sh" "$SEED")
BODY="$BODY,\"totpToken\":\"$TCODE\""
fi
BODY="$BODY}"
LOGIN_RESP=$(curl -sk -b "$JAR" -c "$JAR" -X POST "$BASE$UIDPATH/login" \
-H 'Content-Type: application/json' -d "$BODY")
unset BODY
[ "$DBG" = 1 ] && echo "DBG login: ${LOGIN_RESP:0:70}" >&2
case "$LOGIN_RESP" in
*redirectTo*) : ;;
*twoFactorRequired*) echo "FAIL: 2FA required but no usable seed in vault" >&2; exit 4;;
*) echo "FAIL: login rejected: $LOGIN_RESP" >&2; exit 5;;
esac
RED=$(printf '%s' "$LOGIN_RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p')
CALLBACK=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$RED")
[ "$DBG" = 1 ] && echo "DBG after resume: ${CALLBACK:0:70}" >&2
case "$CALLBACK" in
*/openid/interaction/*)
CUIDPATH=$(printf '%s' "$CALLBACK" | grep -o '/openid/interaction/[^?]*')
CLOC=$(curl -sk -b "$JAR" -c "$JAR" -D - -o /dev/null -X POST "$BASE$CUIDPATH/confirm" \
-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')
[ "$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; }
TOKRESP=$(curl -sk -b "$JAR" -c "$JAR" -X POST "$BASE/openid/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "client_id=$CLIENT" \
--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: $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":[[:space:]]*"\([^"]*\)".*/\1/p')
echo "OK: bearer for '$WHO' at /tmp/.oidc-bearer-$U (session jar $JAR)"