skills: cloudron.md records pointer
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/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"
|
||||
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)"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/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).
|
||||
# usage: cloudron-totp-enroll.sh <cloudron-username> [vault-item]
|
||||
# Captures the seed into the vault item and verifies end-to-end.
|
||||
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")
|
||||
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; }
|
||||
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
|
||||
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
|
||||
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
|
||||
fi
|
||||
echo "PARTIAL: verify failed — check state for $U"
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Full SSO login for an identity into one of the four systems, via cookie jar.
|
||||
# usage: oidc-login.sh <glpi|gitea|discourse|redmine> <login>
|
||||
# Reads the Cloudron password from vault item "<login> Cloudron" (env login
|
||||
# triple; USERNAME may be the email form). Success leaves cookie jar at
|
||||
# /tmp/oidc-<login>-<system>.txt and prints the landing URL.
|
||||
# TOTP-enforced identity? Set TOTP_CODE=$(scripts/totp.sh <seed>) first.
|
||||
set -euo pipefail
|
||||
SYS="${1:?usage: oidc-login.sh <glpi|gitea|discourse|redmine> <login>}"
|
||||
LOGIN="${2:?usage: oidc-login.sh <glpi|gitea|discourse|redmine> <login>}"
|
||||
case "$SYS" in
|
||||
glpi) START="https://cmdb.knownelement.com/plugins/singlesignon/front/callback.php/provider/1?remember=1"; APP="https://cmdb.knownelement.com";;
|
||||
gitea) START="https://git.knownelement.com/user/oauth2/cloudron"; APP="https://git.knownelement.com";;
|
||||
discourse) START="https://community.turnsys.com/auth/openid_connect"; APP="https://community.turnsys.com";;
|
||||
redmine) START="https://projects.knownelement.com/oauth"; APP="https://projects.knownelement.com";;
|
||||
*) echo "unknown system: $SYS" >&2; exit 2;;
|
||||
esac
|
||||
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
|
||||
eval "$(SM env "$LOGIN Cloudron")"
|
||||
[ -n "${PASSWORD:-}" ] || { echo "vault item '$LOGIN Cloudron' empty" >&2; exit 1; }
|
||||
USER_FIELD="${USERNAME:-$LOGIN}"
|
||||
JAR="/tmp/oidc-${LOGIN}-${SYS}.txt"; rm -f "$JAR"
|
||||
|
||||
# 1. Start the SSO flow; follow redirects to the OIDC interaction page.
|
||||
INTURL=$(curl -s -o /dev/null -w '%{redirect_url}' -c "$JAR" "$START")
|
||||
case "$INTURL" in
|
||||
https://my.knownelement.com/openid/interaction/*) : ;;
|
||||
*) echo "no interaction redirect (got: '${INTURL:-none}'). If this is redmine, see references/systems.md (known bounce). If discourse, the route may need CSRF via a real session." >&2; exit 3;;
|
||||
esac
|
||||
|
||||
# 2. Interaction login. Field name is `username` (NOT `login`).
|
||||
BODY="{\"username\":\"$USER_FIELD\",\"password\":\"$PASSWORD\""
|
||||
if [ -n "${TOTP_CODE:-}" ]; then BODY="$BODY,\"totpToken\":\"$TOTP_CODE\""; fi
|
||||
BODY="$BODY}"
|
||||
RESP=$(curl -s -b "$JAR" -c "$JAR" -X POST "${INTURL}/login" \
|
||||
-H 'Content-Type: application/json' -d "$BODY")
|
||||
unset BODY
|
||||
|
||||
case "$RESP" in
|
||||
*redirectTo*)
|
||||
RED=$(printf '%s' "$RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p')
|
||||
case "$RED" in http*) : ;; *) RED="$APP$RED" ;; esac
|
||||
FINAL=$(curl -s -L -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' "$RED")
|
||||
echo "logged in: cookie jar $JAR"
|
||||
echo "landing: $FINAL"
|
||||
echo "verify the identity before use (users/current or /api/v1/user)."
|
||||
;;
|
||||
*twoFactorRequired*)
|
||||
echo "2FA enforced and no/invalid TOTP_CODE given." >&2
|
||||
echo "If the seed is in the vault: TOTP_CODE=\$(scripts/totp.sh \$(sm get '$LOGIN Cloudron' --field totp_seed)) $0 $SYS $LOGIN" >&2
|
||||
exit 4;;
|
||||
*)
|
||||
echo "login rejected: $RESP" >&2
|
||||
exit 5;;
|
||||
esac
|
||||
@@ -11,8 +11,8 @@ COUNTER=$(printf '%016x' $(( NOW / STEP )))
|
||||
# bash vars cannot hold null bytes - keep the counter as a FORMAT string and
|
||||
# let printf emit the raw bytes straight into the pipe.
|
||||
FMT=$(printf '%s' "$COUNTER" | sed 's/../\\x&/g')
|
||||
# intentional: FMT emits raw counter bytes incl. NULs - the whole point
|
||||
# shellcheck disable=SC2059
|
||||
# intentional: FMT emits raw counter bytes (NULs) - the whole point
|
||||
# shellcheck disable=SC2059
|
||||
MAC=$(printf "$FMT" | openssl dgst -sha1 -mac HMAC -macopt "hexkey:$KEYHEX" -binary \
|
||||
| od -An -tx1 | tr -d ' \n')
|
||||
OFFSET=$(( 0x${MAC: -1} ))
|
||||
|
||||
Reference in New Issue
Block a user