#!/bin/bash # Cloudron OIDC dashboard login (SPA-mimic, PKCE) for an identity. # usage: cloudron-oidc-login.sh [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- and a session # cookie jar /tmp/oidc-jar-.txt. Prints checkpoints to stderr. set -uo pipefail U="${1:?usage: cloudron-oidc-login.sh [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 "$@" &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" 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 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 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: $(printf '%s' "$TOKRESP" | head -c 120)" >&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') echo "OK: bearer for '$WHO' at /tmp/.oidc-bearer-$U (session jar $JAR)"