goauthentik / goauthentik/authentik

SAML Source ACS: duplicate autosubmit POST triggers "Replay attack detected" and uncaught SuspiciousOperation is masked as 405 (2025.10)

Open
#25,810 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
25.6k
Forks
2k
Avg merge
1d 1h
Merged PRs (30d)
644

Description

## Description

When an external SAML IdP completes login with an **autosubmit HTML form** (auto-POST binding), some browsers/automation clients submit the form **twice** (~10 ms apart, handled by two different gunicorn workers). authentik's SAML **Source** ACS endpoint (`/source/saml/acs//`) then exhibits a failure chain with three distinct problems:

1. **Double POST race**: the first POST consumes the request ID and succeeds (302); the second POST hits `_verify_request_id` and raises `SuspiciousOperation("Replay attack detected")`.
2. **Exception escapes ACSView**: `ACSView` only catches `MissingSAMLResponse` and `VerificationError`, so the `SuspiciousOperation` propagates to Django's default 500 handler (`ServerErrorView`).
3. **405 masks the real error**: `ServerErrorView` only allows `GET`/`HEAD`/`OPTIONS`, so the failing **POST** is answered with **`405 Method Not Allowed` (`Allow: GET, HEAD, OPTIONS`)** — the actual replay/verification failure is never surfaced to the client or in an accessible way.

Net effect: with `allow_idp_initiated = false` on the SAML Source, **SP-initiated SAML login through such an IdP fails deterministically (100% reproducible over 7 attempts)** with a misleading 405, and the only trace is an `internal.suspicious_request` audit event.

## Steps to reproduce

1. authentik **2025.10.0**, SAML Source configured with an external IdP whose login page ends in an autosubmit form (POST binding, `sp_binding=post`).
2. `allow_idp_initiated = false` on the Source.
3. Initiate SP-initiated login from the SP and complete the IdP login (observed with headless Chrome 152; form is submitted twice ~10 ms apart).

Server logs show two POSTs to the ACS within the same second, different `request_id` and different worker pid:

```text
POST /source/saml/acs// -> 302 runtime=797ms (first POST, succeeds)
POST /source/saml/acs// -> suspicious_request event
"Replay attack detected"
authentik/sources/saml/processors/response.py:163 in _verify_request_id
```

Stack trace excerpt:

```text
SuspiciousOperation: Replay attack detected
File "authentik/sources/saml/processors/response.py", line 163, in _verify_request_id
```

The client receives:

```text
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, OPTIONS
```

## Expected behavior

- The replayed/duplicate POST should be handled gracefully (e.g. idempotent redirect or a proper SAML error response), not raise an uncaught `SuspiciousOperation`.
- The error response for a POST to the ACS should never be a 405 claiming GET is the only allowed method — `ServerErrorView` (or the source ACS error path) must handle `POST` so the real error (replay / verification failure) is visible.

## Actual behavior

First POST succeeds; second POST raises `Replay attack detected`; the exception escapes `ACSView`; the client gets a bare `405 Method Not Allowed` that masks the real failure. Login cannot complete.

## Impact

Any IdP whose login flow renders an autosubmit POST form is effectively **unusable as a SAML Source in authentik when `allow_idp_initiated=false`**, and the 405 makes the root cause very hard to diagnose.

## Environment

- authentik version: **2025.10.0**
- Client: headless Chrome 152 (also plausible with fast real browsers submitting the autosubmit form twice)

## Related

- #24291 — double SAMLResponse submission on the **provider** (IdP) side; this report is the mirror problem on the **source** (SP/ACS) side plus the 405-masking bug.
- #12087 — 405 on SAML source login (Okta, active session); likely the same `ServerErrorView` GET-only root cause for the response code.

Happy to provide additional logs/traces if useful.

Contributor guide

Open the contributing guide

Research direction

Start with authentik/sources/saml/processors/response.py around _verify_request_id, then trace the SAML Source ACSView exception handling and ServerErrorView's allowed methods. Reproduce the duplicate POST behavior described in the issue. Done means the replayed POST is handled without an uncaught exception and the ACS response no longer masks the real failure as a 405.

Written by the indexing model from the issue text.

Assessment

Tech stack
django, python
Domain
authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.