Files
Oxicloud/tests/api/nc_auth_failures.hurl
T
2026-06-13 19:01:40 +02:00

283 lines
11 KiB
Plaintext

# =============================================================
# OxiCloud — Baseline: NC Basic Auth failure modes
# =============================================================
# Group P from BASELINE_TESTS_NC_WEBDAV.md.
#
# Coverage:
# P1 — no Authorization header → 401 + WWW-Authenticate
# P2 — wrong password (real user) → 401
# P3 — N wrong attempts from same IP against a THROWAWAY
# username trip the per-(account,IP) lockout
# P4 — per-IP lockout scope (the #323 regression guard):
# 6 wrong attempts from spoofed X-Forwarded-For: IP1
# lock (admin, IP1), but the SAME correct credential
# from spoofed X-Forwarded-For: IP2 still succeeds.
# Depends on OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0 in
# tests/common/server.env so the server honours the
# X-Forwarded-For header on localhost.
#
# Deliberately NOT covered here:
# P5 — External user attempts NC Basic Auth. Externals can't
# mint app passwords in the first place (the upstream gate
# is asserted in tests/api/external_users.hurl), so the
# in-middleware belt-and-braces check is unreachable via
# a black-box HTTP test. Verified by code inspection.
# =============================================================
# ─────────────────────────────────────────────────────────────
# Setup 1 — JWT login (needed to mint the app password for P4's
# positive control).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
jwt: jsonpath "$.access_token"
# ─────────────────────────────────────────────────────────────
# Setup 2 — Mint admin's NC app password.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/app-passwords
Authorization: Bearer {{jwt}}
Content-Type: application/json
{ "label": "nc_auth_failures P4 positive control" }
HTTP 200
[Captures]
nc_username: jsonpath "$.username"
nc_password: jsonpath "$.password"
ap_id: jsonpath "$.id"
# ─────────────────────────────────────────────────────────────
# P1 — PROPFIND without any Authorization header
# → 401 with `WWW-Authenticate: Basic realm="OxiCloud"`.
# NC desktop relies on this challenge to know it should
# offer credentials at all.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
HTTP 401
[Asserts]
header "WWW-Authenticate" contains "Basic"
# ─────────────────────────────────────────────────────────────
# P2 — Wrong password against a real user
# → 401 with the same WWW-Authenticate challenge.
# Anti-enumeration: response shape identical whether the
# user exists or not.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
[BasicAuth]
{{username}}: definitely-wrong-password
HTTP 401
[Asserts]
header "WWW-Authenticate" contains "Basic"
# ─────────────────────────────────────────────────────────────
# P3 — Lockout trip
#
# Consecutive bad-credential attempts against a throwaway
# username trip the per-(account, IP) lockout. The default
# threshold is 5 (`OXICLOUD_LOCKOUT_MAX_FAILURES = 5`) and
# `tests/common/server.env` does NOT override it — the only
# raised values in that env are the broader login / refresh /
# register rate-limits, which prevent the rate-limiter from
# firing AHEAD of the lockout under sustained test traffic.
# The lockout itself still engages on the 6th attempt.
#
# Once engaged, every subsequent attempt for the same
# (account, IP) pair short-circuits to 401 from the lockout
# check, BEFORE app-password verification runs. The audit log
# fires `WARN account_temporarily_locked` from
# `login_lockout_service.rs` when the cap is hit — useful
# correlation signal during a real run.
#
# A throwaway username (`nc-lockout-probe-…`) is used because
# the lockout is keyed by (username, IP); locking a throwaway
# pair never poisons admin's auth path, so downstream Hurl
# tests in run.sh that authenticate as admin keep working.
#
# Limitation of a black-box HTTP probe: the wire response is
# 401 with the same WWW-Authenticate header whether the 401
# comes from "lockout engaged" or "still just rejecting bad
# creds" — both look the same on the wire. Verification of
# the lockout-engaged branch specifically lives in the unit
# tests (`login_lockout_service.rs::tests`). What this Hurl
# test guards is that 7 consecutive attempts keep returning
# the same 401 shape (no 500s, no header drift), and the
# server-side audit log confirms the lockout engaged at 5.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-1
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-2
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-3
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-4
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-5
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-6
HTTP 401
# 7th attempt: account+IP is now locked at the middleware level.
# Continues to return 401.
PROPFIND {{base_url}}/remote.php/dav/files/nc-lockout-probe-001/
[BasicAuth]
nc-lockout-probe-001: bad-7
HTTP 401
[Asserts]
header "WWW-Authenticate" contains "Basic"
# ─────────────────────────────────────────────────────────────
# P4 — Per-IP lockout scope (#323 regression guard)
#
# Goal: prove that locking out (admin, IP1) does NOT lock out
# (admin, IP2) — the lockout is keyed by *both* parts, not by
# username alone. This was the gap the reporter demonstrated:
# an attacker spoofing X-Forwarded-For could lock a legitimate
# user out from their own IP. The fix scoped the key.
#
# Mechanic:
# 1. Pre-check: admin's app password works from the test's
# default client IP (127.0.0.1, no X-Forwarded-For).
# 2. 6 wrong attempts with X-Forwarded-For: 10.0.0.1 trip the
# lockout for (admin, 10.0.0.1).
# 3. CORRECT app password with X-Forwarded-For: 10.0.0.1 →
# 401 (still locked from THIS IP — positive demonstration
# that the lockout actually engaged, not just chance).
# 4. CORRECT app password with X-Forwarded-For: 10.0.0.2 →
# 207 (NOT locked from this IP — the load-bearing
# assertion of P4).
#
# Why this doesn't break the rest of the suite: every other
# test runs from the default client IP (127.0.0.1) without
# X-Forwarded-For, so (admin, 127.0.0.1) is untouched. The
# lockouts placed here are on (admin, 10.0.0.1) and
# (admin, 10.0.0.2 — released by the success), neither of
# which any other test touches.
#
# Requires: OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0 in
# tests/common/server.env.
# ─────────────────────────────────────────────────────────────
# ── Pre-check: app password works with no X-Forwarded-For ───
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
[BasicAuth]
{{nc_username}}: {{nc_password}}
HTTP 207
# ── Step 1: burn the lockout for (admin, 10.0.0.1) ──────────
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{username}}: p4-bad-1
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{username}}: p4-bad-2
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{username}}: p4-bad-3
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{username}}: p4-bad-4
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{username}}: p4-bad-5
HTTP 401
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{username}}: p4-bad-6
HTTP 401
# ── Step 2: CORRECT app password from IP1 — still 401 ──────
# This is the positive demonstration that the lockout engaged.
# If this were 207, the lockout would not have fired and the
# subsequent IP2 success wouldn't prove anything.
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.1
[BasicAuth]
{{nc_username}}: {{nc_password}}
HTTP 401
# ── Step 3: CORRECT app password from IP2 — 207 ────────────
# The load-bearing assertion of P4: a successful auth from a
# DIFFERENT spoofed source IP proves the lockout was scoped
# to (admin, 10.0.0.1) and not to admin alone.
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
X-Forwarded-For: 10.0.0.2
[BasicAuth]
{{nc_username}}: {{nc_password}}
HTTP 207
# ─────────────────────────────────────────────────────────────
# Teardown — Revoke the app password we minted. Keeps the
# app-passwords table clean across re-runs.
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/api/auth/app-passwords/{{ap_id}}
Authorization: Bearer {{jwt}}
HTTP 200