test(login/register): via password or magic-link

Password login

┌─────┬────────────────────────────────────────────────────┬────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
│  #  │                        Case                        │         Where          │                                          Assertion                                          │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L1  │ Login by username                                  │ auth_login.hurl Case 1 │ 200 + access_token, user.email match                                                        │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L2  │ Login by email (dispatch on @)                     │ auth_login.hurl Case 2 │ 200, same session shape as L1                                                               │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L3  │ Bad password on username path                      │ auth_login.hurl Case 3 │ 403 anti-enum                                                                               │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L4  │ Bad password on email path                         │ auth_login.hurl Case 4 │ 403 anti-enum (same shape as L3)                                                            │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L5  │ Unknown username                                   │ auth_login.hurl Case 5 │ 403 anti-enum (same shape as L3)                                                            │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L6  │ Unknown email                                      │ auth_login.hurl Case 6 │ 403 anti-enum (same shape as L3)                                                            │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L7  │ /api/auth/oidc/providers reports methods correctly │ auth_login.hurl Case 7 │ password_login_enabled: true, magic_link_login_enabled: true, require_verified_email: false │
└─────┴────────────────────────────────────────────────────┴────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘

Password registration

┌─────┬───────────────────────────────────────────────────┬──────────────────────────────┬─────────────────────────────────────────────────────────┐
│  #  │                       Case                        │            Where             │                        Assertion                        │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R1  │ Classic username + email + password → uniform 200 │ registration.hurl Step 2     │ anti-enum message contains "request received"           │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R2  │ Login after register works                        │ registration.hurl Step 2b    │ 200 + session for the new user                          │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R3  │ Email collision → uniform 200 (no rewrite)        │ registration.hurl Steps 8-10 │ attacker password doesn't work; original account intact │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R4  │ Username collision → uniform 200                  │ registration.hurl Step 11    │ same anti-enum shape                                    │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R5  │ Off-domain rejection                              │ registration.hurl Step 12    │ 403 RegistrationDomainNotAllowed                        │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R6  │ Case-insensitive domain match                     │ registration.hurl Step 12b   │ uniform 200 on charlie@EXAMPLE.COM                      │
└─────┴───────────────────────────────────────────────────┴──────────────────────────────┴────────────────────────────┘

Magic-link registration (email-only signup)

┌─────┬──────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────┐
│  #  │                                               Case                                               │             Where             │                   Assertion                    │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR1 │ Email-only signup → welcome mail queued                                                          │ registration.hurl Step 3      │ uniform 200 + browser-binding cookie set       │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR2 │ Welcome mail contains magic-link URL                                                             │ registration.hurl Step 4      │ captured from mock SMTP                        │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR3 │ PR 22 cross-browser confirmation page                                                            │ registration.hurl Step 5a     │ 200 HTML "different browser"                   │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR4 │ Cookie-bound redemption lands on SPA                                                             │ registration.hurl Step 5b     │ 302 → /files (SvelteKit route, post-migration) │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR5 │ email_verified_at stamped after redemption                                                       │ registration.hurl Step 6      │ field present on /api/auth/me                  │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR6 │ Second magic-link post-signup                                                                    │ registration.hurl Step 7      │ uniform 200                                    │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR7 │ Profile PATCH — no-op, name set, empty-string rejected, username-taken 409, claim-once 409, etc. │ registration.hurl Steps 6a–6i │ full profile lifecycle                         │
└─────┴──────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────┘

Magic-link login (existing account)

┌─────┬──────────────────────────────────────────────────────────┬──────────────────────────────────────┬───────────────────────────────────────┐
│  #  │                           Case                           │                Where                 │                             Assertion                              │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML1 │ Baseline password login still works                      │ auth_magic_link_login.hurl Steps 1-2 │ 200                                                                │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML2 │ magic-link/send with email identifier                    │ auth_magic_link_login.hurl Step 3    │ uniform 200 + cookie                                               │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML3 │ magic-link/send with username identifier (dispatch on @) │ auth_magic_link_login.hurl Step 4    │ uniform 200                                                        │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML4 │ Password-user policy: mail actually sent                 │ auth_magic_link_login.hurl Step 5    │ SMTP capture proves permit_magic_link_for_password_users in effect │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML5 │ Redemption creates a session                             │ auth_magic_link_login.hurl Steps 6-7 │ 302 → /files, /api/auth/me returns the same user                   │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML6 │ Anti-enum on unknown identifier                          │ auth_magic_link_login.hurl Step 8    │ same uniform 200 shape as ML3                                      │
└─────┴──────────────────────────────────────────────────────────┴──────────────────────────────────────┴───────────────────────────────────────┘

OIDC

┌─────┬────────────────────────────────────────────────────────────────────────┬───────────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐
│  #  │                                  Case                                  │       Where       │                                                        Assertion                                                        │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O1  │ Setup local admin (bootstrap)                                          │ oidc.hurl Step 1  │ 201                                                                                                                     │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O2  │ Providers endpoint — OIDC visible                                      │ oidc.hurl Step 2  │ enabled: true, provider_name: MockSSO, password_login_enabled: true, magic_link_login_enabled: false (OIDC-master rule) │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O2b │ Magic-link/send refused (endpoint layer)                               │ oidc.hurl Step 2b │ 403 MagicLinkLoginDisabled — proves the policy gate fires, not a 503 SMTP-unwired                                       │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O3  │ Authorize redirect includes PKCE + state                               │ oidc.hurl Step 3  │ 307 to fake IdP                                                                                                         │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O4  │ IdP round-trip + JIT provisioning                                      │ oidc.hurl Step 4  │ Callback lands on /login?oidc_code=…                                                                                    │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O5  │ Code exchange → session cookies                                        │ oidc.hurl Step 5  │ 200 + all three cookies                                                                                                 │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O6  │ JIT profile mapping (name, given/family, picture, groups → admin role) │ oidc.hurl Step 6  │ every claim reflected on /api/auth/me                                                                                   │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O7  │ Refresh rotation on OIDC session                                       │ oidc.hurl Step 7  │ new access/refresh/CSRF cookies                                                                                         │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O8  │ Refreshed cookies authenticate                                         │ oidc.hurl Step 8  │ 200 on /api/auth/me                                                                                                     │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O9  │ Repeat login = same local user (no dup)                                │ oidc.hurl Step 9  │ user_id stable                                                                                                          │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O10 │ Anti-takeover: unverified email → refused                              │ oidc.hurl Step 10 │ 401/403                                                                                                                 │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O11 │ One-time code replay refused                                           │ oidc.hurl Step 11 │ second /exchange → 401                                                                                                  │
└─────┴────────────────────────────────────────────────────────────────────────┴───────────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘

test
This commit is contained in:
Edouard Vanbelle
2026-07-14 01:46:33 +02:00
parent 01da450cf6
commit e94063d96a
27 changed files with 1746 additions and 300 deletions
+23
View File
@@ -82,3 +82,26 @@ Content-Type: application/json
{ "username": "ghost@nowhere.invalid", "password": "{{password}}" }
HTTP 403
# ─────────────────────────────────────────────────────────────
# Case 7 — /api/auth/oidc/providers advertises the auth-method
# policy the SPA needs to render the correct forms.
#
# tests/common/server.env has OXICLOUD_OIDC_ENABLED=false,
# OXICLOUD_SMTP_MOCK=true (so SMTP is "wired"), and the default
# OXICLOUD_AUTH_METHODS (both methods allowed). Expected shape:
# enabled: false — no OIDC IdP configured
# password_login_enabled: true — default allowlist includes it
# magic_link_login_enabled: true — SMTP wired + allowlist + no OIDC
# require_verified_email: false — default
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/auth/oidc/providers
HTTP 200
[Asserts]
jsonpath "$.enabled" == false
jsonpath "$.password_login_enabled" == true
jsonpath "$.magic_link_login_enabled" == true
jsonpath "$.require_verified_email" == false
+162
View File
@@ -0,0 +1,162 @@
# =============================================================
# OxiCloud — magic-link login for password users
# =============================================================
# Regression pin for the `OXICLOUD_AUTH_POLICIES=permit_magic_link_for_password_users`
# switch. Default eligibility ladder refuses `has_password` accounts
# (the strict argument: mailbox-strength shouldn't shadow the stronger
# credential). Operators who prefer modern-SaaS UX opt-in via this
# policy; when set, `POST /api/auth/magic-link/send` mints a login token
# for accounts that also have a password.
#
# Cross-file coupling: `tests/common/server.env` sets
# `OXICLOUD_AUTH_POLICIES=permit_magic_link_for_password_users`. Without
# it, Step 2 below would land on `reason="has_password"` and mail nothing
# — Step 3's SMTP capture would fail with an empty inbox.
#
# What is NOT exercised here:
# * OIDC-master rule: covered separately in tests/oidc/oidc.hurl
# step 2b (magic-link SEND refused when OIDC is enabled).
# * `has_password` rejection under the strict default: can't be
# exercised in the same run — the env is global. Rust unit test
# on `magic_link_eligibility()` covers it directly.
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 — Admin login. Needed to reach the mock-SMTP capture
# endpoint (admin-scoped: /api/admin/smtp/test/captured).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
alice_token: jsonpath "$.access_token"
# ─────────────────────────────────────────────────────────────
# Step 2 — Baseline: admin logs in normally with a password.
# Confirms nothing about the policy has broken the
# classic path. Same call as Step 1, kept as a
# named baseline for readers of the test log.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Asserts]
jsonpath "$.access_token" exists
# ─────────────────────────────────────────────────────────────
# Step 3 — Request a magic-link for the SAME user via email.
# Anti-enum uniform 200 regardless of eligibility, so
# the real proof of "policy fired, mail actually sent"
# is the SMTP capture in Step 5. Without the policy
# in server.env, this same request would be refused
# under `reason="has_password"` and no mail would be
# captured.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/magic-link/send
Content-Type: application/json
{ "email": "{{email}}" }
HTTP 200
[Asserts]
jsonpath "$.message" contains "sign-in link"
# ─────────────────────────────────────────────────────────────
# Step 4 — Same request, but with the LOGIN-IDENTIFIER passed
# as a username (no `@`). Server dispatches on `@` and
# resolves the username to the registered email BEFORE
# rate-limiting, so `admin` and `admin@example.com`
# bucket on one budget. Uniform 200 either way.
#
# The browser-binding challenge cookie is captured HERE
# (not on Step 3): each `/send` request mints a fresh
# challenge, and Step 5 will fetch the MOST RECENT mail —
# which was minted by this very request. Capturing from
# Step 3 instead would pair a stale cookie with Step 4's
# token, and Step 6's redemption would land on PR 22's
# cross-browser confirmation page (200 HTML) instead of
# the direct 302.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/magic-link/send
Content-Type: application/json
{ "email": "{{username}}" }
HTTP 200
[Asserts]
jsonpath "$.message" contains "sign-in link"
[Captures]
alice_magic_cookie: header "set-cookie" regex "oxicloud_magic_request=([^;]+)"
# ─────────────────────────────────────────────────────────────
# Step 5 — Capture the mail. The mock SMTP records every
# outbound message keyed on the recipient. Two magic-
# link mails should have landed (steps 3 and 4), both
# addressed to the admin's registered email. The
# captured endpoint returns the MOST RECENT one — we
# extract its link.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/admin/smtp/test/captured?to={{email}}
Authorization: Bearer {{alice_token}}
HTTP 200
[Asserts]
jsonpath "$.to" == "{{email}}"
jsonpath "$.text_body" matches "/magic/v1/[A-Za-z0-9_-]+"
[Captures]
alice_magic_url: jsonpath "$.text_body" regex "(https?://[^\\s]+/magic/v1/[A-Za-z0-9_-]+)"
# ─────────────────────────────────────────────────────────────
# Step 6 — Redeem the link with the matching browser-binding
# cookie. Internal user, no resource target → lands
# on `/files` (SPA route). Access-token cookie is set
# on the redirect response.
# ─────────────────────────────────────────────────────────────
GET {{alice_magic_url}}
Cookie: oxicloud_magic_request={{alice_magic_cookie}}
HTTP 302
[Asserts]
header "Location" == "/files"
[Captures]
alice_magic_access_token: cookie "oxicloud_access"
# ─────────────────────────────────────────────────────────────
# Step 7 — The cookie session works: /api/auth/me returns the
# admin's own profile. Proves the magic-link redemption
# created a real session for the password-holding user
# — the point of the whole `permit_magic_link_for_password_users`
# policy.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/auth/me
Authorization: Bearer {{alice_magic_access_token}}
HTTP 200
[Asserts]
jsonpath "$.email" == "{{email}}"
jsonpath "$.username" == "{{username}}"
# ─────────────────────────────────────────────────────────────
# Step 8 — Anti-enum sanity: magic-link for a non-existent
# identifier. Same uniform 200 shape, no mail sent.
# The audit log records reason="no_account" — not
# observable from the client, but the response shape
# is IDENTICAL to Step 3, which is the whole point.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/magic-link/send
Content-Type: application/json
{ "email": "ghost-user-that-doesnt-exist" }
HTTP 200
[Asserts]
jsonpath "$.message" contains "sign-in link"
+37 -13
View File
@@ -159,7 +159,11 @@ GET {{magic_url}}
HTTP 302
[Asserts]
header "Location" == "/#/files/folder/{{ext_folder_id}}"
# SvelteKit `files/[...path]` accepts a folder ID as a path segment.
# Historical value pre-migration was `/#/files/folder/{id}` (legacy
# vanilla-frontend hash-routing). Kept in sync with the redemption
# handler in src/interfaces/api/handlers/magic_link_handler.rs.
header "Location" == "/files/{{ext_folder_id}}"
[Captures]
bob_access_token: cookie "oxicloud_access"
@@ -240,9 +244,15 @@ HTTP 200
[Asserts]
jsonpath "$.id" == "{{alice_user_id}}"
jsonpath "$.is_external" == false
# PR 23 — alice is the admin set up via classic password registration
# and has never clicked a magic-link, so her email is unverified.
jsonpath "$.email_verified_at" not exists
# Setup admin is auto-verified at creation. `setup_create_admin` stamps
# `email_verified_at = NOW()` — admin fiat counts as verification,
# matching the OIDC-JIT convention. Rationale: an operator running the
# first-run wizard is authoritative by construction (they set the
# password at the console on a fresh install). Without this, flipping
# `OXICLOUD_REQUIRE_VERIFIED_EMAIL=true` on an existing deployment
# would lock the sole admin out of their own instance. The admin login
# exemption is a second layer of defense; this stamp is the primary.
jsonpath "$.email_verified_at" exists
# 11e — bob CANNOT enumerate unrelated users. A random UUID returns 404
# (anti-enumeration; same response as "user doesn't exist").
@@ -350,7 +360,7 @@ jsonpath "$.message" contains "sign-in link"
# 15b — Capture the fresh email; extract the NEW magic-link URL.
# This is a NULL-resource token (login flow), so redemption
# will land on /#/sharedwithme rather than a deep-link.
# will land on /shared-with-me rather than a deep-link.
GET {{base_url}}/api/admin/smtp/test/captured?to=bob@externalcompany.com
Authorization: Bearer {{alice_token}}
@@ -376,13 +386,17 @@ body contains "different browser"
# 15c-ii — Same token, with `?confirm=1` to acknowledge the
# cross-browser redemption. PR 22 audit-logs
# `cross_browser_confirmed=true` on the success line.
# Lands on /#/sharedwithme since the token has no
# resource target.
# Lands on /shared-with-me since the token has no
# resource target (external user, NULL resource_kind).
GET {{login_magic_url}}?confirm=1
HTTP 302
[Asserts]
header "Location" == "/#/sharedwithme"
# SvelteKit route (path-based). Historical value pre-migration was
# `/#/sharedwithme` (legacy vanilla-frontend hash-routing). Kept in
# sync with `redirect_target()` in
# src/interfaces/api/handlers/magic_link_handler.rs.
header "Location" == "/shared-with-me"
[Captures]
bob_relogin_token: cookie "oxicloud_access"
@@ -408,10 +422,15 @@ Authorization: Bearer {{alice_token}}
HTTP 404
# 15f — Email maps to an existing internal user with a password
# (Alice the admin) → uniform 200 but the magic link is NOT
# actually sent. has_login_credential() short-circuits the
# service so password/OIDC accounts cannot be bypassed via
# mailbox ownership at the moment of request.
# (Alice the admin). The test env has
# `OXICLOUD_AUTH_POLICIES=permit_magic_link_for_password_users`
# set globally in `tests/common/server.env`, so the `has_password`
# eligibility check is bypassed and the link IS minted. Under
# the STRICT default (policy absent), the eligibility ladder
# would refuse with `reason="has_password"` and no mail would
# ship — that path is covered by a Rust unit test on
# `magic_link_eligibility()` because it needs the opposite env
# which we can't hot-swap mid-run.
POST {{base_url}}/api/auth/magic-link/send
Content-Type: application/json
{ "email": "{{email}}" }
@@ -420,10 +439,15 @@ HTTP 200
[Asserts]
jsonpath "$.message" contains "sign-in link"
# With the permit policy, a mail WAS captured. Rate-limit slot burned
# either way (increment fires before eligibility) — Step 16's math
# still holds.
GET {{base_url}}/api/admin/smtp/test/captured?to={{email}}
Authorization: Bearer {{alice_token}}
HTTP 404
HTTP 200
[Asserts]
jsonpath "$.to" == "{{email}}"
# ─────────────────────────────────────────────────────────────
+9 -4
View File
@@ -5,8 +5,8 @@
# `POST /api/auth/register`. Email-only signup:
# - returns a uniform 200 message (no JWT, no UserDto)
# - mints a welcome magic-link mailed to `email`
# - redemption lands the new internal user on `/#/files`
# (not `/#/sharedwithme`, which is for externals)
# - redemption lands the new internal user on `/files`
# (not `/shared-with-me`, which is for externals)
#
# Requires `OXICLOUD_SMTP_MOCK=true` (set in tests/common/server.env).
# =============================================================
@@ -111,14 +111,19 @@ body contains "different browser"
# Step 5b — Same link, this time with the matching cookie.
# PR 22 binds the magic-link to the requesting browser;
# a matching cookie redeems instantly. Internal user
# with no resource target → lands on `/#/files`.
# with no resource target → lands on `/files`.
# ─────────────────────────────────────────────────────────────
GET {{pr18_magic_url}}
Cookie: oxicloud_magic_request={{pr18_magic_cookie}}
HTTP 302
[Asserts]
header "Location" == "/#/files"
# SPA route (SvelteKit path-based). Historical value pre-migration was
# `/#/files` (legacy vanilla frontend hash-routing). Changed alongside
# the migration off the legacy shell — landing on the hash route now
# serves the legacy `static/index.html` with its meta-CSP inline
# scripts, which the SPA CSP blocks.
header "Location" == "/files"
[Captures]
pr18_access_token: cookie "oxicloud_access"
+1
View File
@@ -147,6 +147,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
"$API_DIR/setup.hurl" \
"$API_DIR/auth_login.hurl" \
"$API_DIR/auth_session_lifecycle.hurl" \
"$API_DIR/auth_magic_link_login.hurl" \
"$API_DIR/registration.hurl" \
"$API_DIR/nc_status_capabilities.hurl" \
"$API_DIR/nc_login_flow_v2.hurl" \
+16
View File
@@ -39,6 +39,19 @@ OXICLOUD_RATE_LIMIT_LOGIN_MAX=3600
OXICLOUD_RATE_LIMIT_REGISTER_MAX=3600
OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0
# Mock SMTP — same block as server.env. Required so `magic-link/send`
# reaches the policy gate (returns 403 MagicLinkLoginDisabled under the
# OIDC-master rule) instead of short-circuiting to 503 ServiceUnavailable
# because the invite service is unconfigured. The captured-mail endpoint
# is still available even when magic-link login is refused — invitations
# to non-OIDC recipients still route through this transport.
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 ──────────────────────────────
# tests/oidc/fake_idp/server.js (panva/node-oidc-provider) publishes the
# issuer at the root URL; discovery is at /.well-known/openid-configuration
@@ -66,3 +79,6 @@ OXICLOUD_OIDC_PROVIDER_NAME=MockSSO
# match. This is the standard Authentik/Keycloak/Entra pattern: an
# IdP group becomes an OxiCloud role.
OXICLOUD_OIDC_ADMIN_GROUPS=admin-users
OXICLOUD_AUTH_METHODS=password,magic_link
OXICLOUD_REQUIRE_VERIFIED_EMAIL=false
+15
View File
@@ -79,6 +79,21 @@ OXICLOUD_ALLOW_EXTERNAL_USERS=true
# outside it deliberately.
OXICLOUD_REGISTRATION_ALLOWED_EMAIL_DOMAINS=example.com,example.test
# Auth-policy vector. Enables the "magic-link login is allowed for
# accounts that also have a password" branch — required by
# auth_magic_link_login.hurl (alice has a password AND requests a
# magic-link login). Inert for every other test: `has_password`
# refusal only fires when the endpoint is called, and no other file
# calls `magic-link/send` for a password-holding account.
OXICLOUD_AUTH_POLICIES=permit_magic_link_for_password_users
OXICLOUD_AUTH_METHODS=password,magic_link
# Explicit pin — `--config` now overrides shell env (main.rs uses
# `from_filename_override`), but pinning here documents the intended
# test-env state. Flip to true for the deferred `tests/verify_email/`
# suite; leaving false here keeps every other suite on the "verified
# email not required" path (charlie's classic register+login etc.).
OXICLOUD_REQUIRE_VERIFIED_EMAIL=false
# PR 12 — magic-link rate-limit caps lowered so external_users.hurl can
# exercise the cap behaviour with a small, deterministic request count.
# Production defaults are 50 / 5 / 200 respectively (see example.env).
+25
View File
@@ -69,6 +69,31 @@ jsonpath "$.enabled" == true
jsonpath "$.provider_name" == "MockSSO"
jsonpath "$.authorize_endpoint" == "/api/auth/oidc/authorize"
jsonpath "$.password_login_enabled" == true
# OIDC-master rule: magic-link login must be reported as OFF when OIDC
# is enabled, regardless of `OXICLOUD_AUTH_METHODS` or SMTP wiring.
# Magic-link would bypass any 2FA / step-up the IdP enforces; refusing
# it at the deployment level is a hard invariant. The SPA reads this
# to hide the "Send sign-in link" affordance.
jsonpath "$.magic_link_login_enabled" == false
# ─────────────────────────────────────────────────────────────
# Step 2b — OIDC-master rule regression at the endpoint layer.
# `POST /api/auth/magic-link/send` is refused with 403
# `MagicLinkLoginDisabled` when OIDC is enabled. The
# mock SMTP is configured (server-with-oidc.env has the
# full SMTP block) so this proves the policy gate fires
# BEFORE the "SMTP not wired" 503, which would otherwise
# mask the real reason. The 403 error_type is the machine-
# readable contract the SPA switches on.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/magic-link/send
Content-Type: application/json
{ "email": "someone@example.com" }
HTTP 403
[Asserts]
jsonpath "$.error_type" == "MagicLinkLoginDisabled"
# ─────────────────────────────────────────────────────────────