From 4699cd5070ff2019ba2170d408d0c4a584f22321 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Wed, 9 Sep 2026 14:38:28 -0500 Subject: [PATCH] scripts: per-app SSO first-login scripts (gitea/redmine/discourse) Each script: full OIDC webflow per app (gitea /user/oauth2/cloudron, redmine /oauth?oauth_provider=1 GET-form, discourse /auth/oidc with CSRF + confirm-page token), TOTP from vault seed, session verify. Ran fleet-wide 2026-09-09: accounts created for all 32 org identities; gitea tokens + redmine/discourse api keys minted via admin levers and stored per identity in the vault. https://projects.knownelement.com/issues/942 --- .../scripts/discourse-sso-login.sh | 66 +++++++++++++++++++ .../scripts/gitea-sso-login.sh | 57 ++++++++++++++++ .../scripts/redmine-sso-login.sh | 56 ++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 skills/agent-provisioning/scripts/discourse-sso-login.sh create mode 100644 skills/agent-provisioning/scripts/gitea-sso-login.sh create mode 100644 skills/agent-provisioning/scripts/redmine-sso-login.sh diff --git a/skills/agent-provisioning/scripts/discourse-sso-login.sh b/skills/agent-provisioning/scripts/discourse-sso-login.sh new file mode 100644 index 0000000..5e4d2b4 --- /dev/null +++ b/skills/agent-provisioning/scripts/discourse-sso-login.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Discourse SSO first-login via Cloudron OIDC (openid_connect). +# usage: discourse-sso-login.sh [vault-item] +# Verifies a logged-in session at /my/summary. +set -uo pipefail +U="${1:?usage: discourse-sso-login.sh [vault-item]}" +ITEM="${2:-$U Cloudron}" +BASE="https://community.turnsys.com" +IDP="https://my.knownelement.com" +JAR="/tmp/discourse-jar-$U.txt"; rm -f "$JAR" +SM() { if [ "$(id -un)" = "TSGCOO" ]; then /data2/TSGCOO/.local/bin/sm "$@" &2; exit 1; } +SEED=$(SM get "$ITEM" --field totp_seed 2>/dev/null || true) + +# Discourse gates omniauth behind CSRF + a confirm page +CSRF=$(curl -sk -b "$JAR" -c "$JAR" -H 'X-Requested-With: XMLHttpRequest' "$BASE/session/csrf" | sed -n 's/.*"csrf":"\([^"]*\)".*/\1/p') +[ -n "$CSRF" ] || { echo "FAIL: no csrf token" >&2; exit 2; } +CONFIRM_HTML=$(curl -sk -b "$JAR" -c "$JAR" -H "X-CSRF-Token: $CSRF" -H 'X-Requested-With: XMLHttpRequest' "$BASE/auth/oidc") +FORMTOKEN=$(printf '%s' "$CONFIRM_HTML" | grep -o 'name="authenticity_token"[^>]*value="[^"]*"' | head -1 | sed 's/.*value="//; s/"$//') +[ -n "$FORMTOKEN" ] || { echo "FAIL: no confirm form token" >&2; exit 2; } +INT=$(curl -sk -b "$JAR" -c "$JAR" -X POST "$BASE/auth/oidc" \ + --data-urlencode "authenticity_token=$FORMTOKEN" --data-urlencode "button=continue" \ + -o /dev/null -w '%{redirect_url}') +[ -n "$INT" ] || { echo "FAIL: no oidc redirect" >&2; exit 2; } +# follow the provider auth URL down to the interaction +INT=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$INT") +case "$INT" in + */openid/interaction/*) : ;; + *) echo "FAIL: no interaction reached: $INT" >&2; 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 "$IDP$UIDPATH/login" -H 'Content-Type: application/json' -d "$BODY") +unset BODY +case "$LOGIN_RESP" in + *redirectTo*) : ;; + *) echo "FAIL: login rejected: $LOGIN_RESP" >&2; exit 5 ;; +esac +RED=$(printf '%s' "$LOGIN_RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p') + +FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$RED") +case "$FINAL" in + */openid/interaction/*) + CUIDPATH=$(printf '%s' "$FINAL" | grep -o '/openid/interaction/[^?]*') + CLOC=$(curl -sk -b "$JAR" -c "$JAR" -D - -o /dev/null -X POST "$IDP$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; } + CLOC=$(printf '%s' "$CLOC" | sed 's|^https//|https://|; s|^http//|http://|') + case "$CLOC" in http://*|https://*) : ;; /*) CLOC="$IDP$CLOC" ;; *) CLOC="$IDP/$CLOC" ;; esac + FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$CLOC") + ;; +esac + +WHO=$(curl -sk -b "$JAR" "$BASE/session/current.json" | sed -n 's/.*"username":"\([^"]*\)".*/\1/p' | head -1) +if [ -n "$WHO" ]; then + echo "OK: $WHO logged into discourse (session verified)" + exit 0 +fi +echo "FAIL: no discourse session, landed: $FINAL" >&2 +exit 7 diff --git a/skills/agent-provisioning/scripts/gitea-sso-login.sh b/skills/agent-provisioning/scripts/gitea-sso-login.sh new file mode 100644 index 0000000..b91d16f --- /dev/null +++ b/skills/agent-provisioning/scripts/gitea-sso-login.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Gitea SSO first-login via Cloudron OIDC — creates/binds the gitea account. +# usage: gitea-sso-login.sh [vault-item] +# Verifies a logged-in session at /user/settings and echoes the gitea username. +set -uo pipefail +U="${1:?usage: gitea-sso-login.sh [vault-item]}" +ITEM="${2:-$U Cloudron}" +BASE="https://git.knownelement.com" +IDP="https://my.knownelement.com" +ENTRY="$BASE/user/oauth2/cloudron" +JAR="/tmp/gitea-jar-$U.txt"; rm -f "$JAR" +SM() { if [ "$(id -un)" = "TSGCOO" ]; then /data2/TSGCOO/.local/bin/sm "$@" &2; exit 1; } +SEED=$(SM get "$ITEM" --field totp_seed 2>/dev/null || true) + +INT=$(curl -sk -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$ENTRY") +case "$INT" in + */openid/interaction/*) : ;; + *) echo "FAIL: no interaction reached: $INT" >&2; 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 "$IDP$UIDPATH/login" -H 'Content-Type: application/json' -d "$BODY") +unset BODY +case "$LOGIN_RESP" in + *redirectTo*) : ;; + *) echo "FAIL: login rejected: $LOGIN_RESP" >&2; exit 5 ;; +esac +RED=$(printf '%s' "$LOGIN_RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p') + +FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$RED") +# consent interaction may appear for app clients +case "$FINAL" in + */openid/interaction/*) + CUIDPATH=$(printf '%s' "$FINAL" | grep -o '/openid/interaction/[^?]*') + CLOC=$(curl -sk -b "$JAR" -c "$JAR" -D - -o /dev/null -X POST "$IDP$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; } + CLOC=$(printf '%s' "$CLOC" | sed 's|^https://|https://|; s|^https//|https://|; s|^http//|http://|') + case "$CLOC" in http://*|https://*) : ;; /*) CLOC="$IDP$CLOC" ;; *) CLOC="$IDP/$CLOC" ;; esac + FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$CLOC") + ;; +esac + +CODE=$(curl -sk -b "$JAR" -o /dev/null -w '%{http_code}' "$BASE/user/settings") +if [ "$CODE" = "200" ]; then + echo "OK: $U logged into gitea (session verified)" + exit 0 +fi +echo "FAIL: no gitea session (settings HTTP $CODE), landed: $FINAL" >&2 +exit 7 diff --git a/skills/agent-provisioning/scripts/redmine-sso-login.sh b/skills/agent-provisioning/scripts/redmine-sso-login.sh new file mode 100644 index 0000000..1bf2be5 --- /dev/null +++ b/skills/agent-provisioning/scripts/redmine-sso-login.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Redmine SSO first-login via Cloudron OIDC (redmine_oauth plugin, GET form). +# usage: redmine-sso-login.sh [vault-item] +# Verifies a logged-in session at /my/account. +set -uo pipefail +U="${1:?usage: redmine-sso-login.sh [vault-item]}" +ITEM="${2:-$U Cloudron}" +BASE="https://projects.knownelement.com" +IDP="https://my.knownelement.com" +JAR="/tmp/redmine-jar-$U.txt"; rm -f "$JAR" +SM() { if [ "$(id -un)" = "TSGCOO" ]; then /data2/TSGCOO/.local/bin/sm "$@" &2; exit 1; } +SEED=$(SM get "$ITEM" --field totp_seed 2>/dev/null || true) + +# entry: the oauth GET form kick-off +INT=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$BASE/oauth?oauth_provider=1") +case "$INT" in + */openid/interaction/*) : ;; + *) echo "FAIL: no interaction reached: $INT" >&2; 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 "$IDP$UIDPATH/login" -H 'Content-Type: application/json' -d "$BODY") +unset BODY +case "$LOGIN_RESP" in + *redirectTo*) : ;; + *) echo "FAIL: login rejected: $LOGIN_RESP" >&2; exit 5 ;; +esac +RED=$(printf '%s' "$LOGIN_RESP" | sed -n 's/.*"redirectTo":"\([^"]*\)".*/\1/p') + +FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$RED") +case "$FINAL" in + */openid/interaction/*) + CUIDPATH=$(printf '%s' "$FINAL" | grep -o '/openid/interaction/[^?]*') + CLOC=$(curl -sk -b "$JAR" -c "$JAR" -D - -o /dev/null -X POST "$IDP$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; } + CLOC=$(printf '%s' "$CLOC" | sed 's|^https//|https://|; s|^http//|http://|') + case "$CLOC" in http://*|https://*) : ;; /*) CLOC="$IDP$CLOC" ;; *) CLOC="$IDP/$CLOC" ;; esac + FINAL=$(curl -sk -b "$JAR" -c "$JAR" -o /dev/null -w '%{url_effective}' -L --max-redirs 8 "$CLOC") + ;; +esac + +CODE=$(curl -sk -b "$JAR" -o /dev/null -w '%{http_code}' "$BASE/my/account") +if [ "$CODE" = "200" ]; then + echo "OK: $U logged into redmine (session verified)" + exit 0 +fi +echo "FAIL: no redmine session (my/account HTTP $CODE), landed: $FINAL" >&2 +exit 7