Skip to content

Commit ea1898d

Browse files
committed
fix: skip the origin check for bearer-transport auth requests
Bearer transport reads no cookies, so a cross-site request has no session to ride on. The JS, Go and Rust adapters apply the check to cookie sessions only; this matches them. Cookie sessions on the guard keep it. Closes #28
1 parent 0c97616 commit ea1898d

3 files changed

Lines changed: 26 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ while cookies are `SameSite=None` and only for methods other than `GET`, `HEAD`
145145
- With no `Sec-Fetch-Site`, an `Origin: null` answers 403, and when `allowed_origins` is set any
146146
other `Origin` must be on it. A request with neither header passes.
147147

148-
A bearer token is never attached by a browser, so it is not checked.
148+
A bearer token is never attached by a browser, so it is not checked. For the same reason the auth
149+
routes skip the check on a bearer-transport request (`x-seamless-auth-transport: bearer`), which
150+
reads no cookies.
149151

150152
`User.organization_id` is the active organization, from the `org_id` claim of the verified access
151153
token, or `None` when the session has none. Like `User.roles`, it never comes from the cookie.

‎src/seamless_auth/_adapter.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,14 @@ def handle(self, request: AuthRequest) -> AuthResponse:
326326
return _json_response(500, {"error": "internal_error"})
327327

328328
def _handle(self, request: AuthRequest) -> AuthResponse:
329-
blocked = self._check_origin(request.method, request.headers)
330-
if blocked is not None:
331-
return self._write(blocked, bearer=False)
332-
333329
bearer = (request.headers.get(TRANSPORT_HEADER) or "").lower() == "bearer"
330+
# Bearer transport reads no cookies, so a cross-site request has nothing of the
331+
# user's to ride on. A custom header also needs a CORS preflight to be sent.
332+
if not bearer:
333+
blocked = self._check_origin(request.method, request.headers)
334+
if blocked is not None:
335+
return self._write(blocked, bearer=False)
336+
334337
forwarded = self._forwarded(request)
335338
method = request.method.upper()
336339
# Matched on segments, as manifest routes are, so /logout/ and //logout are

‎tests/test_adapter.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,22 @@ def test_cross_site_state_changes_are_blocked(api: FakeApi) -> None:
531531
assert r.status == 400
532532

533533

534+
def test_bearer_transport_skips_the_origin_check(api: FakeApi) -> None:
535+
api.on("POST", "/login", respond(400, {"error": "invalid_request"}))
536+
adapter = api.adapter(allowed_origins=["https://app.example"])
537+
for headers in ({"sec-fetch-site": "cross-site"}, {"origin": "null"}):
538+
r = call(
539+
adapter,
540+
"POST",
541+
"/login",
542+
body="{}",
543+
headers={"x-seamless-auth-transport": "bearer", **headers},
544+
)
545+
assert r.status == 400, r.text
546+
r = call(adapter, "POST", "/login", body="{}", headers=headers)
547+
assert (r.status, r.body) == (403, {"error": "cross_site_request_blocked"})
548+
549+
534550
APP = "https://app.example"
535551
ORIGIN_CASES = [
536552
# (allowlist, same_site, method, sec-fetch-site, origin, blocked)

0 commit comments

Comments
 (0)