test(oidc): test auto_redirect_if_standalone_oidc and RP iniiate logout
This commit is contained in:
@@ -115,6 +115,15 @@ const configuration = {
|
||||
// (all-device) revocation.
|
||||
backchannel_logout_uri: `${OXICLOUD_BASE_URL}/api/auth/oidc/backchannel-logout`,
|
||||
backchannel_logout_session_required: true,
|
||||
// RP-initiated logout — required for tests/oidc/sso-only.hurl to
|
||||
// exercise the `post_logout_url` shape returned by OxiCloud's
|
||||
// /api/auth/logout when the session is OIDC-backed. The `/login`
|
||||
// URLs on both automated (8087) and manual (8090) ports are
|
||||
// registered so both runners can drive the flow.
|
||||
post_logout_redirect_uris: [
|
||||
'http://localhost:8087/login',
|
||||
'http://localhost:8090/login',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -181,6 +190,12 @@ const configuration = {
|
||||
// and POSTs it to OxiCloud. That's the same wire shape a real
|
||||
// IdP produces, so OxiCloud's validator is exercised end-to-end.
|
||||
backchannelLogout: { enabled: true },
|
||||
// RP-Initiated Logout 1.0. Turning it on advertises
|
||||
// `end_session_endpoint` in discovery so OxiCloud's
|
||||
// `build_end_session_url` (invoked from POST /api/auth/logout)
|
||||
// returns a real URL instead of None. Without this the SSO-only
|
||||
// Hurl assertion `post_logout_url is present` fails silently.
|
||||
rpInitiatedLogout: { enabled: true },
|
||||
},
|
||||
|
||||
// Put scope-implied claims (name, given_name, family_name,
|
||||
|
||||
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
# AUTOMATED SSO-only integration test — two phases.
|
||||
#
|
||||
# Both phases run against the SAME fake IdP + SAME database (spawned
|
||||
# once) but restart OxiCloud between them so the boot config differs:
|
||||
#
|
||||
# Phase A — server-with-oidc-only-no-policy.env
|
||||
# OXICLOUD_AUTH_METHODS=oidc, no AUTH_POLICIES
|
||||
# → GET /login must return 200 (SPA shell, no redirect)
|
||||
# → providers.auto_redirect_to_oidc == false
|
||||
# Driven by sso-only-no-policy.hurl.
|
||||
#
|
||||
# Phase B — server-with-oidc-only.env
|
||||
# OXICLOUD_AUTH_METHODS=oidc AND
|
||||
# OXICLOUD_AUTH_POLICIES=auto_redirect_if_standalone_oidc
|
||||
# → GET /login must return 307 to /api/auth/oidc/authorize
|
||||
# → providers.auto_redirect_to_oidc == true
|
||||
# → full OIDC dance + RP-initiated logout assertions
|
||||
# Driven by sso-only.hurl.
|
||||
#
|
||||
# Phase order matters: A runs first because it doesn't touch DB state
|
||||
# (no admin bootstrap). B runs second and does the admin bootstrap via
|
||||
# JIT provisioning. Restarting OxiCloud between phases is cheap
|
||||
# (~500ms) and cleaner than a hot config reload.
|
||||
#
|
||||
# Sibling script tests/oidc/run-manual-sso-only.sh runs Phase B only
|
||||
# and stops after "server ready" so a human can eyeball the browser
|
||||
# flow — keep both: this script proves the wire contract, the manual
|
||||
# one proves the UX.
|
||||
#
|
||||
# Ports: OxiCloud on 8090, fake IdP on 1081 (distinct from 8087 / 1080
|
||||
# so this can run alongside `just api-test` or a local dev server).
|
||||
#
|
||||
# Prerequisites: docker, cargo, node >= 20, npm, hurl >= 4.0.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
COMMON="$REPO_ROOT/tests/common"
|
||||
OIDC_DIR="$REPO_ROOT/tests/oidc"
|
||||
FAKE_IDP_DIR="$OIDC_DIR/fake_idp"
|
||||
|
||||
# shellcheck source=sso-only.env
|
||||
source "$OIDC_DIR/sso-only.env"
|
||||
|
||||
SERVER_PORT="${base_url##*:}"
|
||||
IDP_PORT="${oidc_issuer##*:}"
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
log() { echo "[sso-only] $*"; }
|
||||
die() { echo "[sso-only] ERROR: $*" >&2; exit 1; }
|
||||
|
||||
wait_for_http() {
|
||||
local url="$1" timeout="${2:-60}"
|
||||
local deadline=$(( $(date +%s) + timeout ))
|
||||
until curl -sf "$url" >/dev/null 2>&1; do
|
||||
[[ $(date +%s) -ge $deadline ]] && die "Timeout waiting for $url"
|
||||
sleep 0.5
|
||||
done
|
||||
}
|
||||
|
||||
# ── Fake-IdP process management (mirrors tests/oidc/run.sh) ────────────────
|
||||
kill_fake_idp() {
|
||||
pkill -f "tests/oidc/fake_idp/server.js" 2>/dev/null || true
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
local pids
|
||||
pids=$(lsof -ti :"$IDP_PORT" 2>/dev/null || true)
|
||||
if [[ -n "$pids" ]]; then
|
||||
# shellcheck disable=SC2086
|
||||
kill -9 $pids 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Server process management ──────────────────────────────────────────────
|
||||
SERVER_PID=""
|
||||
|
||||
start_oxicloud() {
|
||||
local env_file="$1"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
OXICLOUD_SERVER_PORT=$SERVER_PORT
|
||||
OXICLOUD_STORAGE_PATH="$REPO_ROOT/tests/oidc/storage-sso-only"
|
||||
set +a
|
||||
log "Starting OxiCloud (config: $(basename "$env_file"))..."
|
||||
"$OXICLOUD_BIN" --config "$env_file" &
|
||||
SERVER_PID=$!
|
||||
wait_for_http "$base_url/ready" 120
|
||||
log "Server is ready (pid $SERVER_PID)."
|
||||
}
|
||||
|
||||
stop_oxicloud() {
|
||||
if [[ -n "$SERVER_PID" ]]; then
|
||||
log "Stopping OxiCloud (pid $SERVER_PID)..."
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
SERVER_PID=""
|
||||
# Give the OS a moment to release the port; without this a fast
|
||||
# restart occasionally loses the bind on macOS.
|
||||
sleep 0.3
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Teardown (always runs on exit) ─────────────────────────────────────────
|
||||
cleanup() {
|
||||
stop_oxicloud
|
||||
log "Stopping fake-idp..."
|
||||
kill_fake_idp
|
||||
bash "$COMMON/stop-db.sh" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ── 1. Postgres ────────────────────────────────────────────────────────────
|
||||
bash "$COMMON/spawn-db.sh"
|
||||
|
||||
# ── 2. Fake IdP (Node) ─────────────────────────────────────────────────────
|
||||
log "Installing fake-idp dependencies..."
|
||||
if [[ -f "$FAKE_IDP_DIR/package-lock.json" ]]; then
|
||||
(cd "$FAKE_IDP_DIR" && npm ci --silent --no-audit --no-fund)
|
||||
else
|
||||
(cd "$FAKE_IDP_DIR" && npm install --silent --no-audit --no-fund)
|
||||
fi
|
||||
|
||||
log "Sweeping any orphan fake-idp processes from prior runs..."
|
||||
kill_fake_idp
|
||||
sleep 0.3
|
||||
|
||||
log "Starting fake-idp on port $IDP_PORT..."
|
||||
FAKE_IDP_ISSUER="$oidc_issuer" FAKE_IDP_PORT="$IDP_PORT" \
|
||||
OXICLOUD_BASE_URL_FOR_BCL="$base_url" \
|
||||
node "$FAKE_IDP_DIR/server.js" > /tmp/fake-idp-sso-only.log 2>&1 &
|
||||
log "Waiting for fake-idp discovery endpoint..."
|
||||
wait_for_http "$oidc_issuer/.well-known/openid-configuration" 30
|
||||
log "fake-idp is ready (logs: /tmp/fake-idp-sso-only.log)"
|
||||
|
||||
# ── 3. Wipe storage once ───────────────────────────────────────────────────
|
||||
export OXICLOUD_STORAGE_PATH="$REPO_ROOT/tests/oidc/storage-sso-only"
|
||||
# shellcheck source=../common/wipe-storage.sh
|
||||
source "$COMMON/wipe-storage.sh"
|
||||
wipe_storage "$OXICLOUD_STORAGE_PATH"
|
||||
|
||||
# ── 3.5. Ensure the SPA is built (static-dist/) ────────────────────────────
|
||||
# Both phases hit /login and expect the SPA shell response (Phase A as
|
||||
# the primary assertion, Phase B as the loop-guard fallthrough). Without
|
||||
# static-dist/ the ServeDir fallback would 404 those calls.
|
||||
DIST_DIR="$REPO_ROOT/static-dist"
|
||||
if [[ ! -f "$DIST_DIR/index.html" ]]; then
|
||||
log "Building SvelteKit SPA (static-dist/index.html missing)..."
|
||||
(cd "$REPO_ROOT/frontend" \
|
||||
&& npm ci --silent --no-audit --no-fund \
|
||||
&& npm run build) || die "Frontend build failed; static-dist/ is required"
|
||||
fi
|
||||
|
||||
# ── 4. Build OxiCloud once ─────────────────────────────────────────────────
|
||||
BUILD_TARGET="${BUILD_TARGET:-debug}"
|
||||
OXICLOUD_BIN="$REPO_ROOT/target/$BUILD_TARGET/oxicloud"
|
||||
|
||||
if [[ ! -x "$OXICLOUD_BIN" ]]; then
|
||||
log "Building OxiCloud server ($BUILD_TARGET)..."
|
||||
case "$BUILD_TARGET" in
|
||||
debug) (cd "$REPO_ROOT" && cargo build 2>&1 | tail -n 20) || die "cargo build failed" ;;
|
||||
release) (cd "$REPO_ROOT" && cargo build --release 2>&1 | tail -n 20) || die "cargo build --release failed" ;;
|
||||
*) die "Unsupported BUILD_TARGET='$BUILD_TARGET' (expected 'debug' or 'release')" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# Phase A — SSO-only, NO auto-redirect policy
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
log ""
|
||||
log "════════════ Phase A: SSO-only, no auto-redirect ════════════"
|
||||
start_oxicloud "$COMMON/server-with-oidc-only-no-policy.env"
|
||||
log "Running sso-only-no-policy.hurl..."
|
||||
hurl --variables-file "$OIDC_DIR/sso-only.env" \
|
||||
--file-root "$REPO_ROOT/tests" \
|
||||
--test --jobs 1 \
|
||||
"$OIDC_DIR/sso-only-no-policy.hurl"
|
||||
log "Phase A passed."
|
||||
stop_oxicloud
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# Phase B — SSO-only, auto_redirect_if_standalone_oidc policy on
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
log ""
|
||||
log "════════════ Phase B: SSO-only, auto-redirect ON ════════════"
|
||||
start_oxicloud "$COMMON/server-with-oidc-only.env"
|
||||
log "Running sso-only.hurl..."
|
||||
hurl --variables-file "$OIDC_DIR/sso-only.env" \
|
||||
--file-root "$REPO_ROOT/tests" \
|
||||
--test --jobs 1 \
|
||||
"$OIDC_DIR/sso-only.hurl"
|
||||
log "Phase B passed."
|
||||
|
||||
log ""
|
||||
log "SSO-only tests (both phases) passed."
|
||||
@@ -0,0 +1,69 @@
|
||||
# =============================================================
|
||||
# OxiCloud — SSO-only WITHOUT auto-redirect policy
|
||||
# =============================================================
|
||||
# Sibling to tests/oidc/sso-only.hurl. Both run against
|
||||
# server-with-oidc-only-no-policy.env (OXICLOUD_AUTH_METHODS=oidc but
|
||||
# OXICLOUD_AUTH_POLICIES unset) and prove the SPECIFIC posture difference
|
||||
# the auto_redirect_if_standalone_oidc policy makes:
|
||||
#
|
||||
# * with policy (sso-only.hurl): GET /login → 307 to /api/auth/oidc/authorize
|
||||
# * without policy (this file): GET /login → 200 (SPA shell)
|
||||
#
|
||||
# In this posture the SPA renders the login page with the SSO button;
|
||||
# the user clicks it to start the OIDC flow. Everything else about the
|
||||
# OIDC surface is identical, so we DON'T re-run the full OIDC dance —
|
||||
# that's covered by sso-only.hurl + oidc.hurl. This file only asserts
|
||||
# what actually differs.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Providers reports SSO-only but WITHOUT auto-redirect.
|
||||
# The `auto_redirect_to_oidc` field on the DTO comes from
|
||||
# AuthApplicationService::auto_redirect_to_oidc(), which
|
||||
# requires the policy in the vector. Without it → false.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/oidc/providers
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.enabled" == true
|
||||
jsonpath "$.password_login_enabled" == false
|
||||
jsonpath "$.magic_link_login_enabled" == false
|
||||
# The load-bearing difference from sso-only.hurl:
|
||||
jsonpath "$.auto_redirect_to_oidc" == false
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — GET /login must NOT redirect. The middleware's
|
||||
# `should_redirect` predicate evaluates false because
|
||||
# `auto_redirect_to_oidc()` returns false (policy absent),
|
||||
# so the request falls through to the SPA fallback service.
|
||||
# Result: 200 with the SPA shell HTML.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/login
|
||||
[Options]
|
||||
location: false
|
||||
|
||||
HTTP 200
|
||||
# Deliberately no Location-header assertion — we're proving its ABSENCE
|
||||
# by way of the 200 status. If the middleware had incorrectly fired the
|
||||
# redirect this would be a 307.
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — The authorize endpoint still works (user clicks the SSO
|
||||
# button → SPA fetches this URL → server redirects to IdP).
|
||||
# Same shape as sso-only.hurl Step 4; here we only assert
|
||||
# the FIRST hop returns a valid IdP URL, which is enough to
|
||||
# prove the OIDC surface is functional under this posture.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/oidc/authorize
|
||||
[Options]
|
||||
location: false
|
||||
|
||||
HTTP 307
|
||||
[Asserts]
|
||||
# Location points at the fake IdP's /auth endpoint with the OAuth2
|
||||
# response_type / client_id / redirect_uri / PKCE dance.
|
||||
header "Location" matches "^{{oidc_issuer}}/auth\\?response_type=code&client_id={{oidc_client_id}}&"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Variables fed to Hurl for the SSO-only integration test.
|
||||
#
|
||||
# Ports distinct from test.env (8087 / 1080) so this can run alongside
|
||||
# `just api-test` or a concurrent OIDC suite without collision.
|
||||
base_url=http://localhost:8090
|
||||
oidc_issuer=http://localhost:1081
|
||||
oidc_authorize_endpoint=http://localhost:1081/auth
|
||||
# The OIDC client the fake IdP registers — same client_id whether the
|
||||
# SSO-only test or the plain OIDC test drives it. Used to assert the
|
||||
# `client_id=` param in the RP-initiated logout URL.
|
||||
oidc_client_id=oxicloud-test
|
||||
@@ -0,0 +1,193 @@
|
||||
# =============================================================
|
||||
# OxiCloud — SSO-only posture: server-side /login 302 + RP-initiated logout
|
||||
# =============================================================
|
||||
# Complements tests/oidc/oidc.hurl (which runs with OXICLOUD_AUTH_METHODS
|
||||
# accepting password + oidc and never fires the auto-redirect middleware).
|
||||
# This suite runs against tests/common/server-with-oidc-only.env which
|
||||
# sets:
|
||||
# * OXICLOUD_AUTH_METHODS=oidc
|
||||
# * OXICLOUD_AUTH_POLICIES=auto_redirect_if_standalone_oidc
|
||||
#
|
||||
# What it proves the plain OIDC suite can't:
|
||||
# 1. GET /api/auth/oidc/providers reports the standalone-OIDC posture
|
||||
# correctly (auto_redirect_to_oidc=true, password + magic-link off).
|
||||
# 2. GET /login returns a server-side 302 to /api/auth/oidc/authorize
|
||||
# BEFORE the SPA loads (interception lives in web/mod.rs, wired via
|
||||
# an axum middleware layer).
|
||||
# 3. GET /login?error=… falls through to the SPA shell (loop-guard so
|
||||
# an IdP failure doesn't put the browser in an infinite redirect).
|
||||
# 4. POST /api/auth/logout on an OIDC-backed session returns
|
||||
# `post_logout_url` shaped exactly like the RP-initiated logout URL
|
||||
# Keycloak / other IdPs expect: end_session_endpoint +
|
||||
# id_token_hint + post_logout_redirect_uri + client_id.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Providers discovery reports the standalone-OIDC posture.
|
||||
# The SPA no longer reads auto_redirect_to_oidc (server-side
|
||||
# redirect handles it), but the field is still exposed for
|
||||
# diagnostics / future clients.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/oidc/providers
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.enabled" == true
|
||||
jsonpath "$.password_login_enabled" == false
|
||||
# Magic-link is hard-off whenever OIDC is enabled (OIDC master rule)
|
||||
# regardless of what AUTH_METHODS says. Belt-and-braces with the
|
||||
# allowlist which also excludes it.
|
||||
jsonpath "$.magic_link_login_enabled" == false
|
||||
# The policy is on, no other method is live, so the flag resolves true.
|
||||
jsonpath "$.auto_redirect_to_oidc" == true
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — /login returns 302 to /api/auth/oidc/authorize.
|
||||
# location: false so we assert on the header rather than
|
||||
# following. The middleware intercepts BEFORE ServeDir would
|
||||
# hand out the SPA shell, so no HTML body is produced.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/login
|
||||
[Options]
|
||||
location: false
|
||||
|
||||
HTTP 307
|
||||
[Asserts]
|
||||
# axum::response::Redirect::temporary → 307 with the target as Location.
|
||||
header "Location" == "/api/auth/oidc/authorize"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — Loop-guard: /login?error=… must NOT redirect. The IdP
|
||||
# bounces here on failure (Keycloak returns to
|
||||
# post_logout_redirect_uri with ?error= on some flows); a
|
||||
# middleware that redirected regardless would ping-pong the
|
||||
# browser between OxiCloud and the failing IdP forever.
|
||||
# Falling through to the SPA lets the login page render the
|
||||
# error banner.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/login?error=access_denied
|
||||
[Options]
|
||||
location: false
|
||||
|
||||
HTTP 200
|
||||
# No Location header — the ServeDir fallback served the SPA shell.
|
||||
# We don't assert on the body (the shell is minimal HTML) because the
|
||||
# 200 status alone proves the middleware fell through instead of
|
||||
# returning a redirect.
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3b — Loop-guard: /login?oidc_code=… must also NOT redirect.
|
||||
# This is the callback landing URL — the SPA reads the code
|
||||
# from the query string and swaps it for a session. If the
|
||||
# middleware redirected on this we'd never complete the login.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/login?oidc_code=deadbeef
|
||||
[Options]
|
||||
location: false
|
||||
|
||||
HTTP 200
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 — Full OIDC dance. No local admin exists yet — SSO-only means
|
||||
# the first admin bootstraps by logging in via OIDC and getting
|
||||
# the admin role via the group mapping (OXICLOUD_OIDC_ADMIN_GROUPS
|
||||
# matches the fake IdP's `admin-users` group claim).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/oidc/authorize
|
||||
[Options]
|
||||
location: false
|
||||
|
||||
HTTP 307
|
||||
[Captures]
|
||||
idp_url: header "Location"
|
||||
|
||||
|
||||
GET {{idp_url}}
|
||||
[Options]
|
||||
location: true
|
||||
location-trusted: true
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
oidc_code: url regex "oidc_code=([a-f0-9]+)"
|
||||
|
||||
|
||||
POST {{base_url}}/api/auth/oidc/exchange
|
||||
Content-Type: application/json
|
||||
{ "code": "{{oidc_code}}" }
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.user.username" == "oidc_user"
|
||||
# Group-to-role mapping worked — this is now the admin (and the only
|
||||
# user).
|
||||
jsonpath "$.user.role" == "admin"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — Confirm the session is live before we log out. Load-bearing
|
||||
# for Step 6: without proving /me works first, a 401 in Step 6
|
||||
# could mean "logout worked" OR "session was never live".
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/me
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.username" == "oidc_user"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6 — RP-initiated logout returns the end_session URL. The backend
|
||||
# reads the OIDC id_token from the session row, calls the OIDC
|
||||
# service to build the URL from discovery's end_session_endpoint
|
||||
# + id_token_hint + post_logout_redirect_uri + client_id.
|
||||
# The SPA reads `post_logout_url` and window.location.replace's
|
||||
# to it — see AppShell.svelte::onLogout.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/logout
|
||||
Content-Type: application/json
|
||||
{}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
# Field is present.
|
||||
jsonpath "$.post_logout_url" isString
|
||||
# Points at the IdP's end_session_endpoint (oidc-provider mounts it at
|
||||
# /session/end by default).
|
||||
jsonpath "$.post_logout_url" matches "^{{oidc_issuer}}/session/end\\?"
|
||||
# id_token_hint is present and non-empty (JWT-shaped: three dot-separated
|
||||
# base64url segments).
|
||||
jsonpath "$.post_logout_url" matches "id_token_hint=[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
|
||||
# post_logout_redirect_uri points back at /login on this deployment.
|
||||
# The value is URL-encoded so we look for the encoded form.
|
||||
jsonpath "$.post_logout_url" contains "post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A8090%2Flogin"
|
||||
# client_id echoes the configured OIDC client. Real IdPs (Keycloak
|
||||
# post-19) use this to fall back to the registered post-logout redirect
|
||||
# when the id_token_hint has expired.
|
||||
jsonpath "$.post_logout_url" contains "client_id={{oidc_client_id}}"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 7 — Local session gone. The backend cleared the auth cookies
|
||||
# alongside returning post_logout_url; the browser normally
|
||||
# proceeds to navigate to the IdP, but we skip that hop here
|
||||
# and verify locally that the cookies + session row are dead.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/me
|
||||
|
||||
HTTP 401
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 8 — Non-OIDC-session logout returns {} (no post_logout_url).
|
||||
# We can't easily manufacture a password/magic-link session
|
||||
# under SSO-only posture (both are refused at the endpoint
|
||||
# layer). Left as a note; unit test in
|
||||
# auth_application_service covers the `Ok(None)` return branch
|
||||
# when session.oidc_id_token IS NULL.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
Reference in New Issue
Block a user