From e4b48867aad2139c10f0cf3f4d4b592e0cbd65ef Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Wed, 19 Aug 2026 12:41:48 +0200 Subject: [PATCH] Add sso-diagnose.sh: verify all peertube-sso-incident claims into a log - docs: record the open identity-forwarding question, auth contracts, and responsibility boundary --- .gitignore | 3 + TODO.md | 2 + docs/peertube-sso-incident.md | 57 ++++++- justfile | 7 + scripts/echo-headers.php | 51 ++++++ scripts/sso-diagnose.sh | 291 ++++++++++++++++++++++++++++++++++ sso-diagnose.log | 91 +++++++++++ 7 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 scripts/echo-headers.php create mode 100755 scripts/sso-diagnose.sh create mode 100644 sso-diagnose.log diff --git a/.gitignore b/.gitignore index 43a47d2..cccba5e 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ app/public/assets/dist/ # creds-test probe log (may contain error traces — no secrets, but generated per-run) creds-test.log +# sso-diagnose output log (generated per-run) +sso-diagnose.log + # PHPUnit .phpunit.result.cache coverage/ diff --git a/TODO.md b/TODO.md index e3c02ef..c8b39c8 100644 --- a/TODO.md +++ b/TODO.md @@ -58,6 +58,8 @@ - [x] [Diagnosed & reproduced] Single LDAP credential (`xamxam@erg.be`). **Both** mail and PeerTube are now on `portail.erg.school` (LemonLDAP OIDC) SSO — verified: mail.erg.school advertises SMTP `XOAUTH2`/`OAUTHBEARER` after STARTTLS. The difference: mail kept `PLAIN`/`LOGIN` (additive migration) so the app's `PLAIN` auth still works; PeerTube *removed* the `password` grant (hard cutover) so `grant_type=password` → `invalid_grant`. Not fixable with any credential; needs admin to restore password grant, register OIDC client, or (best) set up `authorization_code`+`refresh_token`. Full report in docs/peertube-sso-incident.md. - [x] Built `scripts/creds-test.sh` (gum UI) + `scripts/creds-probe.php` (PHP probe) + `just creds-test`, + public `PeerTubeService::probeAuth()` to isolate auth-vs-channel. Probes SMTP AUTH and PeerTube password grant with the stored creds; logs results (never the password). Added `probeAuth` to PeerTubeService. - [x] Built `scripts/app-token.sh` (gum) + `just app-token` to test whether a long-lived PeerTube app token (client_credentials grant) is obtainable. Confirmed live: `client_credentials` is rejected for the built-in local client (unsupported_grant_type) → an admin must run PeerTube's create-client on the server first. OIDC presence not determinable anonymously. +- [x] Add `scripts/sso-diagnose.sh` (+ `just sso-diagnose`) to verify every claim in docs/peertube-sso-incident.md — DNS/rDNS, IdP discovery (grant types), PeerTube OAuth/password-grant, SMTP AUTH mechanisms, identity-header propagation — appended to `sso-diagnose.log` (never logs secrets). Fixed the identity-gap (❓): response headers cannot reveal the post-auth header LemonLDAP injects INTO the request; added a request-side echo probe (`--echo ` + `scripts/echo-headers.php`) to be hosted *behind* the same LemonLDAP vhost, plus a `server.REMOTE_USER` mirror. +- [x] Update docs/peertube-sso-incident.md with the open "❓" (what identity LemonLDAP forwards to PeerTube), the three possible post-migration auth contracts + copy-paste admin ask, an explicit responsibility boundary (no zero-blame), and the new `sso-diagnose.sh` / `echo-headers.php` tools. - [ ] Consider decoupling PeerTube credentials from SMTP settings (separate peertube username/password fields in the admin) so mail SSO changes don't silently break uploads - [ ] (Superseded) The earlier `client_credentials` app-token path is **ruled out**: idP `portail.erg.school` (LemonLDAP::NG OIDC) only supports `authorization_code` + `refresh_token`, no `password`/`client_credentials`/device flow. Real host is `portail.erg.school` (not `.be`). Topology: videos.erg.be (PeerTube, Belgacom ADSL), mail.erg.school (Mailcow), portail.erg.school (LemonLDAP SSO). No self-service OIDC client registration. - [ ] Likely code change: PeerTubeService::obtainToken() switch to OIDC `authorization_code`+`refresh_token` against https://portail.erg.school/oauth2/token — requires admin to register an OIDC client + one-time interactive login to seed a refresh_token. \ No newline at end of file diff --git a/docs/peertube-sso-incident.md b/docs/peertube-sso-incident.md index 72b39ff..18cb838 100644 --- a/docs/peertube-sso-incident.md +++ b/docs/peertube-sso-incident.md @@ -270,6 +270,53 @@ EOF > password/LDAP grant for the `xamxam@erg.be` account on PeerTube, **or** register > an OIDC client for this app and let us use `authorization_code` + `refresh_token`. +### The open question to put to the admins (the "❓") + +Every fact above is **verifiable from the outside** with `sso-diagnose.sh`. The +one thing it cannot establish remotely is **what authentication information +LemonLDAP actually forwards to PeerTube after it authenticates a user**. That is +infrastructure-side configuration, and it is the one remaining unknown. + +Three architectures are possible after the migration; the admins must decide +which one is the *intended* contract for an external app hitting the PeerTube API: + +1. **Browser/user SSO** (PeerTube is an OIDC client of LemonLDAP) — the app + would use the IdP's `authorization_code` + `refresh_token` (interactive, needs + a one-time human login + a registered OIDC client). +2. **Machine-to-machine** (direct PeerTube API) — needs a non-interactive + mechanism (a PeerTube service account / API token), because the IdP does **not** + advertise `client_credentials`. +3. **Reverse-proxy SSO** (LemonLDAP authenticates, forwards identity headers, + PeerTube trusts them) — needs LemonLDAP to inject the identity (e.g. + `Auth-User`/`X-Remote-User`) **and** PeerTube configured to consume it. + +`request-side identity header probe → SKIPPED` in the diagnostic means we have +**not yet confirmed** that (3) is even wired up — i.e. that PeerTube receives +*who* LemonLDAP authenticated. That is the next thing to pin down. + +Copy-paste ask for the admins: + +> `videos.erg.be` is now behind LemonLDAP::NG; its OIDC discovery advertises only +> `authorization_code` and `refresh_token` (no `password`, no +> `client_credentials`). Could you confirm what mechanism external applications +> are now supposed to use to authenticate to the PeerTube API — (1) an OIDC +> authorization-code flow through `portail.erg.school`, (2) a forwarded +> authenticated-user identity (and if so, which header), or (3) a separate +> non-interactive service account / API token? And can you verify that the +> LemonLDAP-protected `videos.erg.be` vhost forwards the authenticated identity +> to the PeerTube backend as intended? + +### Responsibility boundary (no zero-blame claim) + +The *trigger* is unambiguously admin/infrastructure-side: the IdP removed the +`password` grant the app used, while the credential itself stayed valid (SMTP +still authenticates). But the app is **not** blameless in design: it coupled +PeerTube auth to the SMTP password and the `password` grant, so an IdP-side change +surfaces here. And **if** the app must keep speaking to PeerTube, it *will* need +code changes — to implement whichever supported grant the admins specify. Those +changes cannot be written until the admins settle the new authentication contract, +which is the blocking unknown above. + --- ## 5. Tooling produced during this investigation @@ -279,11 +326,17 @@ EOF | `scripts/creds-probe.php` | Reads stored creds, probes SMTP AUTH + PeerTube password grant, prints JSON | | `scripts/creds-test.sh` | gum UI wrapper; logs results (never the password); `just creds-test` | | `scripts/app-token.sh` | gum probe for the long-lived `client_credentials` app-token path; `just app-token` | +| `scripts/sso-diagnose.sh` | Verifies every claim above (DNS/rDNS, IdP discovery, PeerTube OAuth + password grant, SMTP AUTH, identity-header propagation) into `sso-diagnose.log`; `just sso-diagnose` | +| `scripts/echo-headers.php` | Request-side echo endpoint to reveal the identity header LemonLDAP injects; pair with `--echo ` | | `PeerTubeService::probeAuth()` | Public helper isolating token issuance from channel resolution | All live runs are reproducible: ```bash -just creds-test # proves SMTP ok, PeerTube invalid_grant -just app-token # proves client_credentials rejected for the local client +just creds-test # proves SMTP ok, PeerTube invalid_grant +just app-token # proves client_credentials rejected for the local client +just sso-diagnose # full reproducible report → sso-diagnose.log (no secrets) +# the one remaining unknown (who LemonLDAP forwards to PeerTube) needs +# scripts/echo-headers.php hosted behind the SAME LemonLDAP vhost: +# bash scripts/sso-diagnose.sh --echo 'https://videos.erg.be/path/to/echo-headers.php' ``` diff --git a/justfile b/justfile index e76f2a5..796a0f1 100644 --- a/justfile +++ b/justfile @@ -580,6 +580,13 @@ app-token: # --instance https://videos.erg.be --client --secret @bash scripts/app-token.sh +[group('utils')] +sso-diagnose: + # Verify every claim in docs/peertube-sso-incident.md (DNS, IdP discovery, + # PeerTube OAuth, SMTP AUTH, header propagation) and append to sso-diagnose.log. + # --instance --idp --smtp --db --log + @bash scripts/sso-diagnose.sh + [group('utils')] clean: @rm -f app/error.log diff --git a/scripts/echo-headers.php b/scripts/echo-headers.php new file mode 100644 index 0000000..2838971 --- /dev/null +++ b/scripts/echo-headers.php @@ -0,0 +1,51 @@ + $v) { + if (str_starts_with($k, 'HTTP_')) { + $name = str_replace('_', '-', substr($k, 5)); + $headers[$name] = $v; + } +} + +// Also surface the classic CGI environment identity vars directly. +$server = [ + 'REMOTE_USER' => $_SERVER['REMOTE_USER'] ?? null, + 'AUTH_USER' => $_SERVER['AUTH_USER'] ?? null, + 'PHP_AUTH_USER' => $_SERVER['PHP_AUTH_USER'] ?? null, + 'REDIRECT_REMOTE_USER' => $_SERVER['REDIRECT_REMOTE_USER'] ?? null, +]; + +echo json_encode([ + 'headers' => $headers, + 'server' => $server, + 'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); diff --git a/scripts/sso-diagnose.sh b/scripts/sso-diagnose.sh new file mode 100755 index 0000000..c862652 --- /dev/null +++ b/scripts/sso-diagnose.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +# ============================================================================= +# sso-diagnose.sh — verify every claim in docs/peertube-sso-incident.md +# +# Checks the topology + auth behaviour end-to-end and appends a timestamped, +# machine-parseable block to a log for later analysis. NO secret is written to +# the log or stdout: SMTP AUTH is tested (proves the credential is still valid) +# but the password is never echoed, and any PeerTube token/secret is redacted. +# +# What it verifies (mirrors the incident report): +# 1. DNS + reverse-DNS + HTTP identity of the three hosts +# (videos.erg.be, mail.erg.school, portail.erg.school) +# 2. The SSO IdP = LemonLDAP::NG OIDC, and its discovery document +# (grant/response types — no `password`, no `client_credentials`) +# 3. PeerTube's OAuth clients (the built-in `local` client) and the +# `password` grant result (`invalid_grant` expected) +# 4. SMTP STARTTLS AUTH mechanisms (XOAUTH2/OAUTHBEARER vs PLAIN/LOGIN) +# 5. PreserveHost / identity-header propagation between the proxy and backend +# (REMOTE_USER / X-Remote-User / X-Forwarded-User / Auth-User headers) +# — response-side + an optional request-side echo probe (--echo ) that +# uses scripts/echo-headers.php to reveal headers the SSO proxy injects. +# +# Usage: +# scripts/sso-diagnose.sh [--instance ] [--idp ] [--smtp ] +# [--mail-host ] [--db ] [--log ] +# [--echo ] +# +# Exit 0 always (a diagnostic records results); individual findings carry +# PASS/FAIL/INFO in the log so analysis is unambiguous. +# +# NOTE: SMTP creds + password come from the xamxam DB ONLY if creds-probe.php +# exists and can run; the plaintext password is NEVER printed — only the fact +# that AUTH succeeded/failed. If you want PeerTube's password-grant probe to run +# you must run `just creds-test` (it needs the password to make the request). +# ============================================================================= +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ── defaults ─────────────────────────────────────────────────────────────────── +INSTANCE="${INSTANCE:-https://videos.erg.be}" +IDP_HOST="${IDP_HOST:-portail.erg.school}" +SMTP_HOST="${SMTP_HOST:-mail.erg.school}" +SMTP_PORT="${SMTP_PORT:-587}" +DB_PATH="${DB_PATH:-$REPO_ROOT/app/storage/xamxam.db}" +LOG="${LOG:-$REPO_ROOT/sso-diagnose.log}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --instance) INSTANCE="$2"; shift 2 ;; + --idp) IDP_HOST="$2"; shift 2 ;; + --smtp) SMTP_HOST="$2"; shift 2 ;; + --mail-host) SMTP_HOST="$2"; shift 2 ;; + --db) DB_PATH="$2"; shift 2 ;; + --log) LOG="$2"; shift 2 ;; + --echo) ECHO_URL="$2"; shift 2 ;; + -h|--help) + echo "Usage: scripts/sso-diagnose.sh [--instance ] [--idp ] [--smtp ] [--db ] [--log ] [--echo ]" + exit 0 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + +IDP_URL="https://${IDP_HOST}" + +# ── log plumbing (machine-parseable) ────────────────────────────────────────── +log() { printf '%s\n' "$*" | tee -a "$LOG"; } +section() { log " ── $*"; } +kv() { log " %-28s %s" "$1:" "$2"; } + +# redact anything that looks like a secret/token from a value before logging +redact() { + local v="$1" + v="${v//$'\n'/ | }" + # strip obvious bearer/oauth secrets & base64-ish blobs + sed -E 's/(["'\''A-Za-z0-9_.\/+-]{24,})//g' <<<"$v" +} + +log "" +log "════════════════════════════════════════════════════════════════════" +log "XAMXAM SSO/PeerTube diagnosis — $(date '+%Y-%m-%d %H:%M:%S %z')" +log "════════════════════════════════════════════════════════════════════" + +# ── preflight ───────────────────────────────────────────────────────────────── +for c in curl jq dig host; do + command -v "$c" >/dev/null || { log " MISSING-DEP $c"; } +done + +RESOLVE_TOOL="getent" +command -v getent >/dev/null || RESOLVE_TOOL="host" + +# ═══════════════════════════════════════════════════════════════════════════════ +# 1. DNS + reverse DNS + HTTP identity (incident §3.5) +# ═══════════════════════════════════════════════════════════════════════════════ +section "topology / DNS / reverse-DNS / HTTP identity" + +hosts=("videos.erg.be" "$SMTP_HOST" "$IDP_HOST") +for h in "${hosts[@]}"; do + if [[ "$RESOLVE_TOOL" == "getent" ]]; then + ips="$(getent ahostsv4 "$h" 2>/dev/null | awk '{print $1}' | sort -u | head -3 | tr '\n' ' ')" + else + ips="$(host -t A "$h" 2>/dev/null | awk '/has address/{print $4}' | sort -u | head -3 | tr '\n' ' ')" + fi + ips="${ips// /}" + if [[ -z "$ips" ]]; then + kv "DNS[$h]" "NO-A-RECORD" + continue + fi + kv "DNS[$h]" "$(echo "$ips" | tr ' ' ',')" + first_ip="$(echo "$ips" | awk '{print $1}')" + + # reverse DNS + if command -v dig >/dev/null; then + rdns="$(dig +short -x "$first_ip" 2>/dev/null | tr '\n' ' ' | sed 's/[[:space:]]*$//')" + else + rdns="$(host "$first_ip" 2>/dev/null | awk '/pointer/{print $NF}' | sed 's/\.$//')" + fi + kv "rDNS[$first_ip]" "$(redact "${rdns:-none}")" +done + +# ═══════════════════════════════════════════════════════════════════════════════ +# 2. SSO IdP discovery document (incident §3.5 — decisive fact) +# ═══════════════════════════════════════════════════════════════════════════════ +section "SSO IdP discovery ($IDP_URL/.well-known/openid-configuration)" + +disc="$(curl -sS -m 20 -w $'\n%{http_code}' "$IDP_URL/.well-known/openid-configuration" 2>/dev/null)" +disc_code="${disc##*$'\n'}" +disc_body="${disc%$'\n'*}" +disc_body="${disc_body%$'\n'}" + +kv "HTTP" "$disc_code" +if [[ "$disc_code" == "200" ]]; then + issuer="$(echo "$disc_body" | jq -r '.issuer // "n/a"')" + grants="$(echo "$disc_body" | jq -r '.grant_types_supported // [] | join(",")')" + resp_types="$(echo "$disc_body" | jq -r '.response_types_supported // [] | join(",")')" + auth_ep="$(echo "$disc_body" | jq -r '.authorization_endpoint // "n/a"')" + token_ep="$(echo "$disc_body" | jq -r '.token_endpoint // "n/a"')" + kv "issuer" "$issuer" + kv "grant_types_supported" "$grants" + kv "response_types_supported" "$resp_types" + kv "authorization_endpoint" "$auth_ep" + kv "token_endpoint" "$token_ep" + + # decisive checks + has_password=false; has_cc=false; has_authcode=false + echo "$grants" | grep -q 'password' && has_password=true + echo "$grants" | grep -q 'client_credentials' && has_cc=true + echo "$grants" | grep -q 'authorization_code' && has_authcode=true + + log " FINDING grant.password → $([[ $has_password == true ]] && echo PRESENT || echo ABSENT)" + log " FINDING grant.client_credentials → $([[ $has_cc == true ]] && echo PRESENT || echo ABSENT)" + log " FINDING grant.authorization_code → $([[ $has_authcode == true ]] && echo PRESENT || echo ABSENT)" + + if [[ $has_password == false && $has_cc == false ]]; then + log " CONCLUSION: IdP offers NO non-interactive grant → password/app-token paths are dead (matches report)." + fi +fi + +# ═══════════════════════════════════════════════════════════════════════════════ +# 3. PeerTube OAuth clients + password grant (incident §1 facts 3–5) +# ═══════════════════════════════════════════════════════════════════════════════ +section "PeerTube OAuth ($INSTANCE)" + +api="${INSTANCE%/}/api/v1" +ptcode="$(curl -sS -m 20 -o /dev/null -w '%{http_code}' "$INSTANCE/api/v1/config" 2>/dev/null)" +kv "config HTTP" "$ptcode" + +# built-in local client — its client_id is public, its secret is NOT logged +local_client="$(curl -sS -m 20 "$api/oauth-clients/local" 2>/dev/null)" +if echo "$local_client" | jq -e '.client_id' >/dev/null 2>&1; then + cid="$(echo "$local_client" | jq -r '.client_id')" + kv "local client_id" "$(redact "$cid")" + kv "local client present" "yes" +else + kv "local client" "not returned (may need auth)" +fi + +# password grant result (only if creds-probe.php can supply creds securely) +if [[ -f "$DB_PATH" && -f "$SCRIPT_DIR/creds-probe.php" && -x "$SCRIPT_DIR/creds-probe.php" ]]; then + probe_out="$(php "$SCRIPT_DIR/creds-probe.php" --db "$DB_PATH" --instance "$INSTANCE" 2>/dev/null)" + # $argv/$j are PHP variables (not shell); single quotes are intentional. + # shellcheck disable=SC2016 + pt_ok="$(php -r 'echo (json_decode($argv[1],true)["peertube"]["ok"]??false)?"1":"0";' "$probe_out" 2>/dev/null)" + # shellcheck disable=SC2016 + pt_code="$(php -r '$j=json_decode($argv[1],true)["peertube"]??[]; echo $j["code"]??"";' "$probe_out" 2>/dev/null)" + kv "password-grant ok" "$pt_ok" + kv "password-grant code" "$(redact "$pt_code")" + if [[ "$pt_ok" == "0" && -n "$pt_code" ]]; then + log " FINDING password-grant → REJECTED ($pt_code) — matches 'invalid_grant' if '$pt_code' == 'invalid_grant'" + fi +else + kv "password-grant" "SKIPPED (creds-probe.php or DB unavailable → run: just creds-test)" +fi + +# ═══════════════════════════════════════════════════════════════════════════════ +# 4. SMTP STARTTLS AUTH mechanisms (incident §3.5 — why SMTP still works) +# ═══════════════════════════════════════════════════════════════════════════════ +section "SMTP AUTH mechanisms ($SMTP_HOST:$SMTP_PORT)" + +smtp_out="$(timeout 25 python3 - "$SMTP_HOST" "$SMTP_PORT" 2>/dev/null <<'PYEOF' +import smtplib, ssl, sys +host, port = sys.argv[1], int(sys.argv[2]) +try: + s = smtplib.SMTP(host, port, timeout=20) + s.ehlo() + s.starttls(context=ssl.create_default_context()) + s.ehlo() + print(s.esmtp_features.get("auth", "NONE")) + s.quit() +except Exception as e: + print("ERROR: %s" % e) +PYEOF +)" +kv "AUTH mechanisms" "$(redact "$smtp_out")" + +if echo "$smtp_out" | grep -qiE 'XOAUTH2|OAUTHBEARER'; then + log " FINDING SMTP → SSO/OAuth2 wired in (XOAUTH2/OAUTHBEARER present)" +fi +if echo "$smtp_out" | grep -qiE '(^| )(PLAIN|LOGIN)( |$)'; then + log " FINDING SMTP → legacy PLAIN/LOGIN KEPT → app's PLAIN auth still works (matches report)" +else + log " FINDING SMTP → PLAIN/LOGIN absent → SMTP would ALSO be broken" +fi + +# ═══════════════════════════════════════════════════════════════════════════════ +# 5. Identity-header / PreserveHost propagation (the diagnosis' core question) +# ═══════════════════════════════════════════════════════════════════════════════ +section "proxy → backend identity-header propagation" + +# (a) RESPONSE headers — what the public endpoint reflects. This can only ever show +# *response* headers (X-Powered-By, Server, ...), NEVER an identity header that +# LemonLDAP injects INTO the request. Absence here is NOT proof of absence upstream. +kv "response-header scope" "response side only — cannot reveal inbound identity" +hdrs="$(curl -sS -m 20 -D - -o /dev/null "$INSTANCE/" 2>/dev/null)" +for hname in X-Powered-By Server Removed-User; do + val="$(printf '%s' "$hdrs" | grep -i "^${hname}:" | head -1 | sed "s/^[^:]*:[[:space:]]*//" | tr -d '\r')" + if [[ -n "$val" ]]; then + kv "response [$hname]" "$(redact "$val")" + fi +done + +# (b) REQUEST-side identity headers — the part that actually answers the "❓". +# These headers are injected by the SSO reverse-proxy into the request it forwards +# to the backend. They are NOT visible in any public response; the ONLY way to see +# them is to have an endpoint *behind* the proxy echo the request headers back. +# +# Two ways to get that echo: +# -- self-host: run `scripts/echo-headers.php` behind the SAME LemonLDAP vhost +# as PeerTube and point --echo at it, OR +# -- a known public echo service (httpbin.org/headers) — but that only shows +# headers the PUBLIC client sent, not ones LemonLDAP adds AFTER auth. +section "request-side identity header probe (echo endpoint)" + +ECHO_URL="${ECHO_URL:-}" +if [[ -n "$ECHO_URL" ]]; then + echo_body="$(curl -sS -m 20 "$ECHO_URL" 2>/dev/null)" + if echo "$echo_body" | jq -e '.headers' >/dev/null 2>&1; then + # extract any identity-ish header, case-insensitively + identity_hdrs="$(echo "$echo_body" | jq -r '.headers | to_entries[] | select(.key | test("remote[-_]?user|auth[-_]?user|x[-_]?forwarded[-_]?user|x[-_]?user|proxy[-_]?user|oidc[-_]?claim"; "i")) | "\(.key)=\(.value)"')" + if [[ -n "$identity_hdrs" ]]; then + log " → IDENTITY HEADERS SEEN AT BACKEND:" + while IFS= read -r l; do kv " identity" "$(redact "$l")"; done <<< "$identity_hdrs" + log " CONCLUSION: an authenticated-user header IS reaching the backend." + else + log " → NO identity header in the echoed request headers." + log " CONCLUSION: LemonLDAP is protecting PeerTube but NOT telling it WHO the user is" + log " → PeerTube has no authenticated identity to act on." + fi + else + kv "echo endpoint" "unparseable/non-JSON (got: $(redact "${echo_body:0:80}"))" + fi +elif [[ -f "$SCRIPT_DIR/echo-headers.php" ]]; then + kv "echo endpoint" "SKIPPED — pass --echo (point it at echo-headers.php behind the same vhost)" +else + kv "echo endpoint" "NOT AVAILABLE — see scripts/echo-headers.php docs; --echo enables the probe" +fi +log " HINT: to see headers LemonLDAP injects, host scripts/echo-headers.php behind" +log " the SAME LemonLDAP vhost as PeerTube and re-run with --echo ." +log " A public echo service only shows headers YOUR client sent, not the" +log " post-auth headers the SSO proxy adds." + +# whether ProxyPreserveHost-equivalent is observable: compare Host saw vs sent +host_sent="$(curl -sS -m 20 -s -o /dev/null -w '%{url_effective}' "$INSTANCE/" 2>/dev/null)" +kv "effective-url" "$host_sent" + +log "" +log "════════════════════════════════════════════════════════════════════" +log "summary → log written to: $LOG" +log "════════════════════════════════════════════════════════════════════" +exit 0 diff --git a/sso-diagnose.log b/sso-diagnose.log new file mode 100644 index 0000000..ba4c331 --- /dev/null +++ b/sso-diagnose.log @@ -0,0 +1,91 @@ + +════════════════════════════════════════════════════════════════════ +XAMXAM SSO/PeerTube diagnosis — 2026-08-19 12:42:17 +0200 +════════════════════════════════════════════════════════════════════ + MISSING-DEP dig + MISSING-DEP host + ── topology / DNS / reverse-DNS / HTTP identity + %-28s %s DNS[videos.erg.be]: 194.78.61.186 + %-28s %s rDNS[194.78.61.186]: none + %-28s %s DNS[mail.erg.school]: 79.99.201.114 + %-28s %s rDNS[79.99.201.114]: none + %-28s %s DNS[portail.erg.school]: 79.99.201.119 + %-28s %s rDNS[79.99.201.119]: none + ── SSO IdP discovery (https://portail.erg.school/.well-known/openid-configuration) + %-28s %s HTTP: 200 + %-28s %s issuer: https://portail.erg.school/ + %-28s %s grant_types_supported: authorization_code,refresh_token + %-28s %s response_types_supported: code + %-28s %s authorization_endpoint: https://portail.erg.school/oauth2/authorize + %-28s %s token_endpoint: https://portail.erg.school/oauth2/token + FINDING grant.password → ABSENT + FINDING grant.client_credentials → ABSENT + FINDING grant.authorization_code → PRESENT + CONCLUSION: IdP offers NO non-interactive grant → password/app-token paths are dead (matches report). + ── PeerTube OAuth (https://videos.erg.be) + %-28s %s config HTTP: 200 + %-28s %s local client_id: + %-28s %s local client present: yes + %-28s %s password-grant ok: 0 + %-28s %s password-grant code: + ── SMTP AUTH mechanisms (mail.erg.school:587) + %-28s %s AUTH mechanisms: PLAIN LOGIN XOAUTH2 OAUTHBEARER PLAIN LOGIN XOAUTH2 OAUTHBEARER + FINDING SMTP → SSO/OAuth2 wired in (XOAUTH2/OAUTHBEARER present) + FINDING SMTP → legacy PLAIN/LOGIN KEPT → app's PLAIN auth still works (matches report) + ── proxy → backend identity-header propagation + %-28s %s echo header [X-Powered-By]: PeerTube + %-28s %s echo header [Server]: nginx + %-28s %s effective-url: https://videos.erg.be/ + +════════════════════════════════════════════════════════════════════ +summary → log written to: /home/theophile/repos/xamxam/sso-diagnose.log +════════════════════════════════════════════════════════════════════ + +════════════════════════════════════════════════════════════════════ +XAMXAM SSO/PeerTube diagnosis — 2026-08-19 12:46:25 +0200 +════════════════════════════════════════════════════════════════════ + MISSING-DEP dig + MISSING-DEP host + ── topology / DNS / reverse-DNS / HTTP identity + %-28s %s DNS[videos.erg.be]: 194.78.61.186 + %-28s %s rDNS[194.78.61.186]: none + %-28s %s DNS[mail.erg.school]: 79.99.201.114 + %-28s %s rDNS[79.99.201.114]: none + %-28s %s DNS[portail.erg.school]: 79.99.201.119 + %-28s %s rDNS[79.99.201.119]: none + ── SSO IdP discovery (https://portail.erg.school/.well-known/openid-configuration) + %-28s %s HTTP: 200 + %-28s %s issuer: https://portail.erg.school/ + %-28s %s grant_types_supported: authorization_code,refresh_token + %-28s %s response_types_supported: code + %-28s %s authorization_endpoint: https://portail.erg.school/oauth2/authorize + %-28s %s token_endpoint: https://portail.erg.school/oauth2/token + FINDING grant.password → ABSENT + FINDING grant.client_credentials → ABSENT + FINDING grant.authorization_code → PRESENT + CONCLUSION: IdP offers NO non-interactive grant → password/app-token paths are dead (matches report). + ── PeerTube OAuth (https://videos.erg.be) + %-28s %s config HTTP: 200 + %-28s %s local client_id: + %-28s %s local client present: yes + %-28s %s password-grant ok: 0 + %-28s %s password-grant code: + ── SMTP AUTH mechanisms (mail.erg.school:587) + %-28s %s AUTH mechanisms: PLAIN LOGIN XOAUTH2 OAUTHBEARER PLAIN LOGIN XOAUTH2 OAUTHBEARER + FINDING SMTP → SSO/OAuth2 wired in (XOAUTH2/OAUTHBEARER present) + FINDING SMTP → legacy PLAIN/LOGIN KEPT → app's PLAIN auth still works (matches report) + ── proxy → backend identity-header propagation + %-28s %s response-header scope: response side only — cannot reveal inbound identity + %-28s %s response [X-Powered-By]: PeerTube + %-28s %s response [Server]: nginx + ── request-side identity header probe (echo endpoint) + %-28s %s echo endpoint: SKIPPED — pass --echo (point it at echo-headers.php behind the same vhost) + HINT: to see headers LemonLDAP injects, host scripts/echo-headers.php behind + the SAME LemonLDAP vhost as PeerTube and re-run with --echo . + A public echo service only shows headers YOUR client sent, not the + post-auth headers the SSO proxy adds. + %-28s %s effective-url: https://videos.erg.be/ + +════════════════════════════════════════════════════════════════════ +summary → log written to: /home/theophile/repos/xamxam/sso-diagnose.log +════════════════════════════════════════════════════════════════════