test(oidc): test auto_redirect_if_standalone_oidc and RP iniiate logout
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# OxiCloud test-server env file for the SSO-only, NO-auto-redirect posture.
|
||||
#
|
||||
# Same as server-with-oidc-only.env EXCEPT OXICLOUD_AUTH_POLICIES is not
|
||||
# set. Proves the /login middleware is opt-in — with AUTH_METHODS=oidc
|
||||
# alone the SPA still renders at /login (with just the SSO button) and
|
||||
# the middleware falls through. Under this posture the user has to click
|
||||
# the button to start the OIDC flow instead of being auto-redirected.
|
||||
#
|
||||
# Paired with tests/oidc/sso-only-no-policy.hurl; both driven by the
|
||||
# `run-sso-only.sh` runner in a two-phase sequence.
|
||||
|
||||
# ── Shared test config (mirrors server.env) ────────────────────────────────
|
||||
DATABASE_URL=postgres://oxicloud_test:oxicloud_test@localhost:5433/oxicloud_test
|
||||
OXICLOUD_DB_CONNECTION_STRING=postgres://oxicloud_test:oxicloud_test@localhost:5433/oxicloud_test
|
||||
OXICLOUD_STATIC_PATH=./static
|
||||
OXICLOUD_JWT_SECRET=test-secret-do-not-use-in-prod-minimum-32-chars
|
||||
OXICLOUD_ENABLE_AUTH=true
|
||||
OXICLOUD_ENABLE_TRASH=true
|
||||
OXICLOUD_ENABLE_SEARCH=true
|
||||
OXICLOUD_ENABLE_FILE_SHARING=true
|
||||
OXICLOUD_ENABLE_MUSIC=true
|
||||
OXICLOUD_EXPOSE_SYSTEM_USERS=true
|
||||
OXICLOUD_WOPI_ENABLED=false
|
||||
OXICLOUD_NEXTCLOUD_ENABLED=true
|
||||
|
||||
RUST_LOG="warn,audit=info,oxicloud::infrastructure::services::oidc_service=info,oxicloud::application::services::auth_application_service=info"
|
||||
|
||||
OXICLOUD_RATE_LIMIT_REFRESH_MAX=3600
|
||||
OXICLOUD_RATE_LIMIT_LOGIN_MAX=3600
|
||||
OXICLOUD_RATE_LIMIT_REGISTER_MAX=3600
|
||||
OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0
|
||||
|
||||
# Mock SMTP — kept wired even though magic-link login is disabled under the
|
||||
# OIDC master rule, so the invite/mail transport doesn't 503 unconfigured.
|
||||
OXICLOUD_SMTP_MOCK=true
|
||||
OXICLOUD_SMTP_HOST=localhost
|
||||
OXICLOUD_SMTP_PORT=25
|
||||
OXICLOUD_SMTP_FROM='OxiCloud Tests <test@oxicloud.local>'
|
||||
OXICLOUD_SMTP_TLS=none
|
||||
OXICLOUD_ALLOW_EXTERNAL_USERS=true
|
||||
|
||||
# ── OIDC client wired at the fake-idp sidecar (SSO-only) ───────────────────
|
||||
OXICLOUD_OIDC_ENABLED=true
|
||||
OXICLOUD_OIDC_ISSUER_URL=http://localhost:1081
|
||||
OXICLOUD_OIDC_CLIENT_ID=oxicloud-test
|
||||
OXICLOUD_OIDC_CLIENT_SECRET=test-client-secret-not-used-in-prod
|
||||
OXICLOUD_OIDC_REDIRECT_URI=http://localhost:8090/api/auth/oidc/callback
|
||||
OXICLOUD_OIDC_SCOPES="openid profile email"
|
||||
OXICLOUD_OIDC_FRONTEND_URL=http://localhost:8090
|
||||
OXICLOUD_OIDC_AUTO_PROVISION=true
|
||||
OXICLOUD_OIDC_PROVIDER_NAME=MockSSO-NoPolicy
|
||||
OXICLOUD_OIDC_ADMIN_GROUPS=admin-users
|
||||
|
||||
# Same modern SSO-only mechanism as server-with-oidc-only.env.
|
||||
OXICLOUD_AUTH_METHODS=oidc
|
||||
|
||||
# ── The DELIBERATE OMISSION ────────────────────────────────────────────────
|
||||
# OXICLOUD_AUTH_POLICIES is NOT set here. This is the whole point of the
|
||||
# test — with AUTH_METHODS=oidc alone, the login middleware in
|
||||
# src/interfaces/web/mod.rs::oidc_standalone_login_redirect must fall
|
||||
# through (SPA shell served at /login) instead of returning 302. The
|
||||
# with-policy variant (server-with-oidc-only.env) proves the flip side.
|
||||
|
||||
OXICLOUD_REQUIRE_VERIFIED_EMAIL=false
|
||||
@@ -1,18 +1,25 @@
|
||||
# OxiCloud test-server env file for the MANUAL SSO-only auto-redirect test.
|
||||
# OxiCloud test-server env file for the SSO-only auto-redirect test.
|
||||
#
|
||||
# Layered on top of server-with-oidc.env: identical EXCEPT
|
||||
# OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true, which makes OIDC the ONLY
|
||||
# login method (magic-link is already hard-disabled whenever OIDC is
|
||||
# enabled, per the "OIDC master rule" — see example.env). This is the
|
||||
# config the frontend's login-page auto-redirect guard
|
||||
# (frontend/src/routes/login/+page.svelte) actually fires under —
|
||||
# tests/common/server-with-oidc.env keeps password login on, so the
|
||||
# automated tests/oidc/oidc.hurl suite never exercises the redirect.
|
||||
# Used by BOTH runners on port 8090 / IdP 1081:
|
||||
# * tests/oidc/run-sso-only.sh — automated, drives Hurl assertions
|
||||
# (server-side /login 302 + RP-initiated logout post_logout_url shape).
|
||||
# * tests/oidc/run-manual-sso-only.sh — human-run browser eyeball to
|
||||
# confirm zero login-form flash before the redirect fires.
|
||||
#
|
||||
# Used by tests/oidc/run-manual-sso-only.sh (human-run, not CI). Distinct
|
||||
# ports (8090 / IdP 1081) so it doesn't collide with a concurrently running
|
||||
# `just api-test` (which uses 8087 / IdP 1080) or a local `cargo run` dev
|
||||
# server.
|
||||
# What makes it "SSO-only":
|
||||
# * OXICLOUD_AUTH_METHODS=oidc — allowlist is [Oidc] only. Password
|
||||
# and magic-link are both hard-off at the deployment level; the
|
||||
# fail-fast validator in config.rs refuses to boot if `oidc` is in
|
||||
# the list without OXICLOUD_OIDC_ENABLED=true (or vice versa in a
|
||||
# future major).
|
||||
# * OXICLOUD_AUTH_POLICIES=auto_redirect_if_standalone_oidc — the
|
||||
# policy switch that makes GET /login return a server-side 302 to
|
||||
# /api/auth/oidc/authorize BEFORE the SPA loads (no form flash).
|
||||
# Interception lives in src/interfaces/web/mod.rs.
|
||||
#
|
||||
# Distinct ports (8090 / IdP 1081) so it doesn't collide with a
|
||||
# concurrently running `just api-test` (which uses 8087 / IdP 1080) or a
|
||||
# local `cargo run` dev server.
|
||||
#
|
||||
# `--config` makes the binary read THIS file verbatim — there is no
|
||||
# auto-merge with server.env, so every variable the server needs has
|
||||
@@ -70,9 +77,21 @@ OXICLOUD_OIDC_PROVIDER_NAME=MockSSO-Only
|
||||
# Group-to-role mapping — same fake-idp claim shape as server-with-oidc.env.
|
||||
OXICLOUD_OIDC_ADMIN_GROUPS=admin-users
|
||||
|
||||
# The single flag that makes OIDC the ONLY login method: is_password_login_allowed()
|
||||
# is exactly `!disable_password_login` (auth_application_service.rs). Magic-link
|
||||
# is already hard-disabled whenever OIDC is enabled, regardless of AUTH_METHODS.
|
||||
OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true
|
||||
# Modern SSO-only mechanism (preferred over legacy
|
||||
# OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true, which still works but is a
|
||||
# per-flag toggle instead of the composable allowlist below).
|
||||
#
|
||||
# AUTH_METHODS=oidc restricts the effective allowlist to [Oidc]. Password
|
||||
# and magic-link both refuse at the endpoint layer. Combined with the
|
||||
# OIDC master rule (magic-link hard-off whenever OIDC is enabled) this
|
||||
# closes every non-SSO login path.
|
||||
OXICLOUD_AUTH_METHODS=oidc
|
||||
|
||||
# AUTH_POLICIES: additive switches to auth behavior. The
|
||||
# auto_redirect_if_standalone_oidc token makes GET /login return a 302
|
||||
# to /api/auth/oidc/authorize (server-side, via web-layer middleware) so
|
||||
# the SPA never renders. Loop-guards are built in — a Location or
|
||||
# ?error= query on /login falls through to the SPA shell.
|
||||
OXICLOUD_AUTH_POLICIES=auto_redirect_if_standalone_oidc
|
||||
|
||||
OXICLOUD_REQUIRE_VERIFIED_EMAIL=false
|
||||
|
||||
@@ -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