Files
Oxicloud/tests/api/auth_magic_link_login.hurl
T

163 lines
8.4 KiB
Plaintext
Raw Normal View History

# =============================================================
# 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"