mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix(auth): isolate server/console session cookies by name
The server (:8080) and console (:8090) both set a cookie named `turnstone_auth`. Cookies ignore port (RFC 6265), so on a shared host (localhost dev, the Electron build, single-box installs) logging into one surface overwrote the other's cookie and 401'd the first session. Give each surface its own cookie name -- `turnstone_auth_server` / `turnstone_auth_console` -- threaded as a required `cookie_name` argument through the cookie builders, `check_request`, `AuthMiddleware`, and the six shared auth handlers (login/logout/setup/whoami/refresh/oidc_callback). Each app passes its own constant; the parameter is required (no default) so a forgotten caller fails loudly instead of silently reverting to the legacy name. Names key on role, not node: the cluster shares one JWT identity and the console->node proxy re-mints a bearer token (dropping Set-Cookie), so per-instance names would break identity portability and aren't used. Hard cutover: the legacy `turnstone_auth` cookie is no longer read and self-expires within its 24h TTL (one forced re-login). JWT audience was already enforced, so the shared cookie was a session clobber, not an auth bypass.
This commit is contained in:
+10
-4
@@ -63,7 +63,10 @@ Auth is always enabled. All API endpoints except public paths require a valid to
|
||||
Include a token in one of two ways:
|
||||
|
||||
- **Bearer header**: `Authorization: Bearer <token>`
|
||||
- **Cookie**: `turnstone_auth=<token>` (set automatically by the login endpoint)
|
||||
- **Cookie**: the surface-scoped auth cookie — `turnstone_auth_server` on
|
||||
turnstone-server, `turnstone_auth_console` on turnstone-console (set
|
||||
automatically by the login endpoint). The names differ so the two surfaces,
|
||||
when co-hosted on one origin, don't overwrite each other's session.
|
||||
|
||||
The server accepts two token types:
|
||||
|
||||
@@ -102,7 +105,8 @@ Authenticate with credentials and receive a JWT. Accepts two credential formats:
|
||||
}
|
||||
```
|
||||
|
||||
The response also sets a `turnstone_auth` HttpOnly cookie containing the JWT.
|
||||
The response also sets a surface-scoped HttpOnly cookie containing the JWT
|
||||
(`turnstone_auth_server` on turnstone-server, `turnstone_auth_console` on turnstone-console).
|
||||
|
||||
**Response (failure):** `401`
|
||||
|
||||
@@ -114,7 +118,8 @@ The response also sets a `turnstone_auth` HttpOnly cookie containing the JWT.
|
||||
|
||||
### `POST /v1/api/auth/logout`
|
||||
|
||||
Clears the `turnstone_auth` cookie. No request body required.
|
||||
Clears the surface-scoped auth cookie (`turnstone_auth_server` /
|
||||
`turnstone_auth_console`). No request body required.
|
||||
|
||||
**Response:** `200`
|
||||
|
||||
@@ -199,7 +204,8 @@ this endpoint.
|
||||
}
|
||||
```
|
||||
|
||||
The response also sets a `turnstone_auth` HttpOnly cookie containing the JWT.
|
||||
The response also sets a surface-scoped HttpOnly cookie containing the JWT
|
||||
(`turnstone_auth_server` on turnstone-server, `turnstone_auth_console` on turnstone-console).
|
||||
|
||||
**Response (already set up):** `409`
|
||||
|
||||
|
||||
@@ -1191,7 +1191,8 @@ Three hierarchical scopes control endpoint access:
|
||||
`/metrics`, `/openapi.json`, `/docs`, `/api/auth/*`, and `/api/auth/setup`
|
||||
are always allowed.
|
||||
2. **Token extraction** — `Authorization: Bearer <token>` header first, then
|
||||
`turnstone_auth` cookie as fallback.
|
||||
surface-scoped auth cookie (`turnstone_auth_server` on the node server,
|
||||
`turnstone_auth_console` on the console) as fallback.
|
||||
3. **Token type detection** — dots in the token indicate JWT; `ts_` prefix
|
||||
indicates API token.
|
||||
4. **Validation** — JWT signature check or API token hash lookup in storage.
|
||||
|
||||
+166
-37
@@ -8,6 +8,9 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from turnstone.core.auth import (
|
||||
AUTH_COOKIE,
|
||||
AUTH_COOKIE_CONSOLE,
|
||||
AUTH_COOKIE_SERVER,
|
||||
WRITE_PATHS,
|
||||
_extract_bearer,
|
||||
_extract_cookie,
|
||||
@@ -374,48 +377,59 @@ class TestExtractCookie:
|
||||
|
||||
class TestMakeSetCookie:
|
||||
def test_contains_token(self):
|
||||
val = make_set_cookie("tok_abc")
|
||||
assert "turnstone_auth=tok_abc" in val
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_SERVER)
|
||||
assert "turnstone_auth_server=tok_abc" in val
|
||||
|
||||
def test_httponly(self):
|
||||
assert "HttpOnly" in make_set_cookie("tok_abc")
|
||||
assert "HttpOnly" in make_set_cookie("tok_abc", AUTH_COOKIE_SERVER)
|
||||
|
||||
def test_samesite_lax(self):
|
||||
assert "SameSite=Lax" in make_set_cookie("tok_abc")
|
||||
assert "SameSite=Lax" in make_set_cookie("tok_abc", AUTH_COOKIE_SERVER)
|
||||
|
||||
def test_path(self):
|
||||
assert "Path=/" in make_set_cookie("tok_abc")
|
||||
assert "Path=/" in make_set_cookie("tok_abc", AUTH_COOKIE_SERVER)
|
||||
|
||||
def test_max_age_default(self):
|
||||
val = make_set_cookie("tok_abc")
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_SERVER)
|
||||
assert "Max-Age=86400" in val # 24 hours (matches JWT expiry)
|
||||
|
||||
def test_max_age_custom(self):
|
||||
val = make_set_cookie("tok_abc", max_age=3600)
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_SERVER, max_age=3600)
|
||||
assert "Max-Age=3600" in val
|
||||
|
||||
def test_secure_default(self):
|
||||
val = make_set_cookie("tok_abc")
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_SERVER)
|
||||
assert "; Secure" in val
|
||||
|
||||
def test_secure_false(self):
|
||||
val = make_set_cookie("tok_abc", secure=False)
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_SERVER, secure=False)
|
||||
assert "; Secure" not in val
|
||||
|
||||
def test_secure_true(self):
|
||||
val = make_set_cookie("tok_abc", secure=True)
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_SERVER, secure=True)
|
||||
assert "; Secure" in val
|
||||
|
||||
def test_uses_provided_name(self):
|
||||
# Name is honored verbatim; one surface's name never leaks into the other.
|
||||
val = make_set_cookie("tok_abc", AUTH_COOKIE_CONSOLE)
|
||||
assert "turnstone_auth_console=tok_abc" in val
|
||||
assert "turnstone_auth_server" not in val
|
||||
|
||||
|
||||
class TestMakeClearCookie:
|
||||
def test_max_age_zero(self):
|
||||
assert "Max-Age=0" in make_clear_cookie()
|
||||
assert "Max-Age=0" in make_clear_cookie(AUTH_COOKIE_SERVER)
|
||||
|
||||
def test_empty_value(self):
|
||||
assert "turnstone_auth=;" in make_clear_cookie()
|
||||
assert "turnstone_auth_server=;" in make_clear_cookie(AUTH_COOKIE_SERVER)
|
||||
|
||||
def test_httponly(self):
|
||||
assert "HttpOnly" in make_clear_cookie()
|
||||
assert "HttpOnly" in make_clear_cookie(AUTH_COOKIE_SERVER)
|
||||
|
||||
def test_uses_provided_name(self):
|
||||
val = make_clear_cookie(AUTH_COOKIE_CONSOLE)
|
||||
assert "turnstone_auth_console=;" in val
|
||||
assert "Max-Age=0" in val
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -437,47 +451,67 @@ class TestCheckRequest:
|
||||
return f"Bearer {create_jwt('u1', frozenset({'read', 'write', 'approve'}), 'test', self._SECRET)}"
|
||||
|
||||
def test_public_path_no_token_ok(self):
|
||||
allowed, status, msg, _result = check_request("GET", "/health", None)
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/health", None, cookie_name=AUTH_COOKIE_SERVER
|
||||
)
|
||||
assert allowed is True
|
||||
assert status == 200
|
||||
|
||||
def test_public_root_no_token_ok(self):
|
||||
allowed, status, msg, _result = check_request("GET", "/", None)
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/", None, cookie_name=AUTH_COOKIE_SERVER
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
def test_public_static_no_token_ok(self):
|
||||
allowed, status, msg, _result = check_request("GET", "/static/style.css", None)
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/static/style.css", None, cookie_name=AUTH_COOKIE_SERVER
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
def test_api_no_token_401(self):
|
||||
allowed, status, msg, _result = check_request("GET", "/api/workstreams", None)
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/api/workstreams", None, cookie_name=AUTH_COOKIE_SERVER
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
assert "Unauthorized" in msg
|
||||
|
||||
def test_api_invalid_token_401(self):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/api/workstreams", "Bearer wrong_token"
|
||||
"GET", "/api/workstreams", "Bearer wrong_token", cookie_name=AUTH_COOKIE_SERVER
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
|
||||
def test_api_read_token_ok(self, read_jwt):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/api/workstreams", read_jwt, jwt_secret=self._SECRET
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
assert status == 200
|
||||
|
||||
def test_api_full_token_ok(self, full_jwt):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/api/workstreams", full_jwt, jwt_secret=self._SECRET
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
full_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
def test_write_read_token_403(self, read_jwt):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"POST", "/api/workstreams/abc/send", read_jwt, jwt_secret=self._SECRET
|
||||
"POST",
|
||||
"/api/workstreams/abc/send",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -485,14 +519,22 @@ class TestCheckRequest:
|
||||
|
||||
def test_write_full_token_ok(self, full_jwt):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"POST", "/api/workstreams/abc/send", full_jwt, jwt_secret=self._SECRET
|
||||
"POST",
|
||||
"/api/workstreams/abc/send",
|
||||
full_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
assert status == 200
|
||||
|
||||
def test_approve_read_token_403(self, read_jwt):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"POST", "/api/workstreams/abc/approve", read_jwt, jwt_secret=self._SECRET
|
||||
"POST",
|
||||
"/api/workstreams/abc/approve",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -504,6 +546,7 @@ class TestCheckRequest:
|
||||
"/node/node-a/api/workstreams/abc/send",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -515,6 +558,7 @@ class TestCheckRequest:
|
||||
"/node/node-a/api/workstreams/abc/send/",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -522,7 +566,11 @@ class TestCheckRequest:
|
||||
def test_direct_write_trailing_slash_read_token_403(self, read_jwt):
|
||||
"""Trailing slash must not bypass write-role check on direct routes."""
|
||||
allowed, status, msg, _result = check_request(
|
||||
"POST", "/api/workstreams/abc/send/", read_jwt, jwt_secret=self._SECRET
|
||||
"POST",
|
||||
"/api/workstreams/abc/send/",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -534,6 +582,7 @@ class TestCheckRequest:
|
||||
"/node/node-a/api/workstreams/abc/send",
|
||||
full_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
@@ -544,6 +593,7 @@ class TestCheckRequest:
|
||||
"/node/node-a/v1/api/workstreams/abc/send",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -555,6 +605,7 @@ class TestCheckRequest:
|
||||
"/node/node-a/v1/api/workstreams/abc/send",
|
||||
full_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
@@ -565,6 +616,7 @@ class TestCheckRequest:
|
||||
"/node/node-a/v1/api/cluster/workstreams/new",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -572,26 +624,40 @@ class TestCheckRequest:
|
||||
def test_proxy_read_endpoint_read_token_ok(self, read_jwt):
|
||||
"""Read tokens can access proxy read endpoints."""
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/node/node-a/api/workstreams", read_jwt, jwt_secret=self._SECRET
|
||||
"GET",
|
||||
"/node/node-a/api/workstreams",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
def test_console_create_ws_read_token_403(self, read_jwt):
|
||||
"""Read tokens cannot create workstreams."""
|
||||
allowed, status, msg, _result = check_request(
|
||||
"POST", "/api/cluster/workstreams/new", read_jwt, jwt_secret=self._SECRET
|
||||
"POST",
|
||||
"/api/cluster/workstreams/new",
|
||||
read_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
|
||||
def test_approve_full_token_ok(self, full_jwt):
|
||||
allowed, status, msg, _result = check_request(
|
||||
"POST", "/api/workstreams/abc/approve", full_jwt, jwt_secret=self._SECRET
|
||||
"POST",
|
||||
"/api/workstreams/abc/approve",
|
||||
full_jwt,
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
def test_no_auth_header_string(self):
|
||||
allowed, status, msg, _result = check_request("GET", "/api/dashboard", "")
|
||||
allowed, status, msg, _result = check_request(
|
||||
"GET", "/api/dashboard", "", cookie_name=AUTH_COOKIE_SERVER
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
|
||||
@@ -619,8 +685,9 @@ class TestCheckRequestWithCookie:
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
None,
|
||||
cookie_header=f"turnstone_auth={read_jwt}",
|
||||
cookie_header=f"{AUTH_COOKIE_SERVER}={read_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
assert status == 200
|
||||
@@ -630,8 +697,9 @@ class TestCheckRequestWithCookie:
|
||||
"POST",
|
||||
"/api/workstreams/abc/send",
|
||||
f"Bearer {full_jwt}",
|
||||
cookie_header=f"turnstone_auth={read_jwt}",
|
||||
cookie_header=f"{AUTH_COOKIE_SERVER}={read_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
@@ -640,8 +708,9 @@ class TestCheckRequestWithCookie:
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
None,
|
||||
cookie_header="turnstone_auth=wrong_token",
|
||||
cookie_header=f"{AUTH_COOKIE_SERVER}=wrong_token",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
@@ -651,8 +720,9 @@ class TestCheckRequestWithCookie:
|
||||
"POST",
|
||||
"/api/workstreams/abc/send",
|
||||
None,
|
||||
cookie_header=f"turnstone_auth={read_jwt}",
|
||||
cookie_header=f"{AUTH_COOKIE_SERVER}={read_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 403
|
||||
@@ -662,8 +732,9 @@ class TestCheckRequestWithCookie:
|
||||
"POST",
|
||||
"/api/workstreams/abc/send",
|
||||
None,
|
||||
cookie_header=f"turnstone_auth={full_jwt}",
|
||||
cookie_header=f"{AUTH_COOKIE_SERVER}={full_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
@@ -673,6 +744,7 @@ class TestCheckRequestWithCookie:
|
||||
"/api/workstreams",
|
||||
None,
|
||||
cookie_header=None,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
@@ -682,6 +754,7 @@ class TestCheckRequestWithCookie:
|
||||
"POST",
|
||||
"/api/auth/login",
|
||||
None,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
@@ -690,9 +763,56 @@ class TestCheckRequestWithCookie:
|
||||
"POST",
|
||||
"/api/auth/logout",
|
||||
None,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
|
||||
# --- Cookie-name isolation (the server/console fix) -----------------------
|
||||
|
||||
def test_legacy_cookie_name_rejected(self, read_jwt):
|
||||
"""A pre-isolation ``turnstone_auth`` cookie no longer authenticates once
|
||||
the surface is configured for a scoped name: existing sessions must
|
||||
re-login after the rename (intentional hard cutover, no read-fallback)."""
|
||||
allowed, status, _, _r = check_request(
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
None,
|
||||
cookie_header=f"{AUTH_COOKIE}={read_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
|
||||
def test_console_cookie_name_not_read_by_server(self, read_jwt):
|
||||
"""The console's cookie name is invisible to a server-configured surface,
|
||||
so a console session can't satisfy a server request (no cross-read)."""
|
||||
allowed, status, _, _r = check_request(
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
None,
|
||||
cookie_header=f"{AUTH_COOKIE_CONSOLE}={read_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is False
|
||||
assert status == 401
|
||||
|
||||
def test_both_cookies_present_no_clobber(self, read_jwt):
|
||||
"""With distinct names both surfaces' cookies coexist in one jar; the
|
||||
server reads only its own and authenticates even with a console cookie
|
||||
also present — the core property the rename buys."""
|
||||
allowed, status, _, _r = check_request(
|
||||
"GET",
|
||||
"/api/workstreams",
|
||||
None,
|
||||
cookie_header=f"{AUTH_COOKIE_CONSOLE}=other; {AUTH_COOKIE_SERVER}={read_jwt}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed is True
|
||||
assert status == 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests — actual HTTP server with auth enabled
|
||||
@@ -1020,6 +1140,10 @@ class TestServerLogin:
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "jwt" in data
|
||||
# Server surface sets ITS OWN scoped cookie — not the console's, not the legacy name.
|
||||
set_cookie = resp.headers.get("set-cookie", "")
|
||||
assert AUTH_COOKIE_SERVER in set_cookie
|
||||
assert AUTH_COOKIE_CONSOLE not in set_cookie
|
||||
|
||||
def test_cookie_auth_on_api(self):
|
||||
# Login to get cookie (TestClient tracks cookies automatically)
|
||||
@@ -1044,6 +1168,7 @@ class TestServerLogin:
|
||||
assert logout_resp.status_code == 200
|
||||
cookie = logout_resp.headers.get("set-cookie", "")
|
||||
assert "Max-Age=0" in cookie
|
||||
assert AUTH_COOKIE_SERVER in cookie
|
||||
|
||||
# API should now fail (cookie cleared)
|
||||
resp = self.test_client.get("/v1/api/workstreams")
|
||||
@@ -1070,8 +1195,6 @@ class TestServerLogin:
|
||||
|
||||
def test_refresh_returns_new_jwt_and_cookie(self):
|
||||
"""POST /api/auth/refresh re-mints the cookie with a fresh exp."""
|
||||
from turnstone.core.auth import AUTH_COOKIE
|
||||
|
||||
# Storage needs get_user_permissions for the refresh re-resolve path.
|
||||
# Mock is shared across tests in the class — re-arm here in case a
|
||||
# prior test left it default.
|
||||
@@ -1098,7 +1221,7 @@ class TestServerLogin:
|
||||
# and refresh produce identical iat/exp claims and therefore an
|
||||
# identical token, which is fine: the cookie still gets re-set.
|
||||
cookie_hdr = refresh.headers.get("set-cookie", "")
|
||||
assert AUTH_COOKIE in cookie_hdr
|
||||
assert AUTH_COOKIE_SERVER in cookie_hdr
|
||||
assert "HttpOnly" in cookie_hdr
|
||||
|
||||
# The refreshed cookie must keep working.
|
||||
@@ -1279,7 +1402,10 @@ class TestConsoleLogin:
|
||||
json={"username": "testuser", "password": "testpass"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert "turnstone_auth" in resp.headers.get("set-cookie", "")
|
||||
# Exact scoped name (not the legacy prefix) and no server-name bleed.
|
||||
set_cookie = resp.headers.get("set-cookie", "")
|
||||
assert AUTH_COOKIE_CONSOLE in set_cookie
|
||||
assert AUTH_COOKIE_SERVER not in set_cookie
|
||||
|
||||
def test_cookie_auth_on_api(self):
|
||||
self.test_client.post(
|
||||
@@ -1559,6 +1685,7 @@ class TestJWTVersionClaim:
|
||||
jwt_secret=self.SECRET,
|
||||
jwt_audience=JWT_AUD_SERVER,
|
||||
jwt_version="1.2",
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed
|
||||
assert result is not None
|
||||
@@ -1581,6 +1708,7 @@ class TestJWTVersionClaim:
|
||||
jwt_secret=self.SECRET,
|
||||
jwt_audience=JWT_AUD_SERVER,
|
||||
jwt_version="1.2",
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed
|
||||
|
||||
@@ -1602,6 +1730,7 @@ class TestJWTVersionClaim:
|
||||
jwt_secret=self.SECRET,
|
||||
jwt_audience=JWT_AUD_SERVER,
|
||||
jwt_version="1.2",
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert not allowed
|
||||
assert status == 401
|
||||
|
||||
@@ -7,6 +7,7 @@ import time
|
||||
import pytest
|
||||
|
||||
from turnstone.core.auth import (
|
||||
AUTH_COOKIE_SERVER,
|
||||
AuthResult,
|
||||
_authenticate_token,
|
||||
check_request,
|
||||
@@ -273,6 +274,7 @@ class TestCheckRequestScopes:
|
||||
"/api/workstreams/abc/send",
|
||||
f"Bearer {jwt_tok}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert not allowed
|
||||
assert status == 403
|
||||
@@ -285,6 +287,7 @@ class TestCheckRequestScopes:
|
||||
"/api/workstreams/abc/approve",
|
||||
f"Bearer {jwt_tok}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert not allowed
|
||||
assert status == 403
|
||||
@@ -297,6 +300,7 @@ class TestCheckRequestScopes:
|
||||
"/api/workstreams/abc/approve",
|
||||
f"Bearer {jwt_tok}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed
|
||||
assert result is not None
|
||||
@@ -309,6 +313,7 @@ class TestCheckRequestScopes:
|
||||
"/api/workstreams/abc/send",
|
||||
f"Bearer {jwt_tok}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert allowed
|
||||
assert result is not None
|
||||
@@ -321,6 +326,7 @@ class TestCheckRequestScopes:
|
||||
"/api/workstreams/abc/send",
|
||||
f"Bearer {jwt_tok}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert not allowed
|
||||
assert status == 403
|
||||
@@ -332,6 +338,7 @@ class TestCheckRequestScopes:
|
||||
"/v1/api/admin/users",
|
||||
f"Bearer {jwt_tok}",
|
||||
jwt_secret=self._SECRET,
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
)
|
||||
assert not allowed
|
||||
assert status == 403
|
||||
|
||||
@@ -24,6 +24,8 @@ from turnstone.console.server import (
|
||||
admin_list_oidc_identities,
|
||||
)
|
||||
from turnstone.core.auth import (
|
||||
AUTH_COOKIE_CONSOLE,
|
||||
AUTH_COOKIE_SERVER,
|
||||
AuthResult,
|
||||
LoginRateLimiter,
|
||||
handle_oidc_authorize,
|
||||
@@ -46,7 +48,7 @@ async def _oidc_authorize(request: Request) -> Response:
|
||||
|
||||
|
||||
async def _oidc_callback(request: Request) -> Response:
|
||||
return await handle_oidc_callback(request, "test-audience")
|
||||
return await handle_oidc_callback(request, "test-audience", cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -292,7 +294,8 @@ class TestOIDCCallback:
|
||||
assert resp.status_code == 302
|
||||
assert "oidc_success=1" in resp.headers["location"]
|
||||
assert "set-cookie" in resp.headers
|
||||
assert "turnstone_auth=" in resp.headers["set-cookie"]
|
||||
set_cookie = resp.headers["set-cookie"]
|
||||
assert set_cookie.split(";", 1)[0].partition("=")[0] == AUTH_COOKIE_SERVER
|
||||
|
||||
def test_oidc_not_configured_returns_404(self, storage: SQLiteBackend) -> None:
|
||||
app = Starlette(
|
||||
@@ -645,7 +648,9 @@ class TestOIDCCallback:
|
||||
# Wire a callback bound to the CONSOLE audience. After bug-3 the
|
||||
# stored audience must take precedence.
|
||||
async def _console_callback(request: Request) -> Response:
|
||||
return await handle_oidc_callback(request, "turnstone-console")
|
||||
return await handle_oidc_callback(
|
||||
request, "turnstone-console", cookie_name=AUTH_COOKIE_CONSOLE
|
||||
)
|
||||
|
||||
jwt_secret = "test-jwt-secret-key-padded-32b!!"
|
||||
app = Starlette(
|
||||
@@ -670,7 +675,7 @@ class TestOIDCCallback:
|
||||
set_cookie = resp.headers["set-cookie"]
|
||||
cookie_kv = set_cookie.split(";", 1)[0]
|
||||
name, _, token = cookie_kv.partition("=")
|
||||
assert name == "turnstone_auth"
|
||||
assert name == AUTH_COOKIE_CONSOLE
|
||||
assert token
|
||||
|
||||
# Decoding without audience verification first to inspect the claim.
|
||||
|
||||
@@ -47,6 +47,7 @@ from turnstone.console.metrics import ConsoleMetrics
|
||||
from turnstone.console.router import ConsoleRouter
|
||||
from turnstone.core.audit import record_audit
|
||||
from turnstone.core.auth import (
|
||||
AUTH_COOKIE_CONSOLE,
|
||||
JWT_AUD_CONSOLE,
|
||||
JWT_AUD_SERVER,
|
||||
AuthMiddleware,
|
||||
@@ -1574,14 +1575,14 @@ async def auth_login(request: Request) -> Response:
|
||||
"""Authenticate via username:password or legacy token, return JWT."""
|
||||
from turnstone.core.auth import handle_auth_login
|
||||
|
||||
return await handle_auth_login(request, JWT_AUD_CONSOLE)
|
||||
return await handle_auth_login(request, JWT_AUD_CONSOLE, cookie_name=AUTH_COOKIE_CONSOLE)
|
||||
|
||||
|
||||
async def auth_logout(request: Request) -> Response:
|
||||
"""POST /v1/api/auth/logout — clear auth cookie."""
|
||||
from turnstone.core.auth import handle_auth_logout
|
||||
|
||||
return await handle_auth_logout(request)
|
||||
return await handle_auth_logout(request, cookie_name=AUTH_COOKIE_CONSOLE)
|
||||
|
||||
|
||||
async def auth_status(request: Request) -> Response:
|
||||
@@ -1595,14 +1596,14 @@ async def auth_setup(request: Request) -> Response:
|
||||
"""POST /v1/api/auth/setup — create first admin user (public, one-time only)."""
|
||||
from turnstone.core.auth import handle_auth_setup
|
||||
|
||||
return await handle_auth_setup(request, JWT_AUD_CONSOLE)
|
||||
return await handle_auth_setup(request, JWT_AUD_CONSOLE, cookie_name=AUTH_COOKIE_CONSOLE)
|
||||
|
||||
|
||||
async def auth_whoami(request: Request) -> Response:
|
||||
"""GET /v1/api/auth/whoami — return authenticated user info."""
|
||||
from turnstone.core.auth import handle_auth_whoami
|
||||
|
||||
return await handle_auth_whoami(request)
|
||||
return await handle_auth_whoami(request, cookie_name=AUTH_COOKIE_CONSOLE)
|
||||
|
||||
|
||||
async def auth_refresh(request: Request) -> Response:
|
||||
@@ -1613,7 +1614,7 @@ async def auth_refresh(request: Request) -> Response:
|
||||
"""
|
||||
from turnstone.core.auth import handle_auth_refresh
|
||||
|
||||
return await handle_auth_refresh(request, JWT_AUD_CONSOLE)
|
||||
return await handle_auth_refresh(request, JWT_AUD_CONSOLE, cookie_name=AUTH_COOKIE_CONSOLE)
|
||||
|
||||
|
||||
async def oidc_authorize(request: Request) -> Response:
|
||||
@@ -1627,7 +1628,7 @@ async def oidc_callback(request: Request) -> Response:
|
||||
"""GET /v1/api/auth/oidc/callback — OIDC callback, exchange code for JWT."""
|
||||
from turnstone.core.auth import handle_oidc_callback
|
||||
|
||||
return await handle_oidc_callback(request, JWT_AUD_CONSOLE)
|
||||
return await handle_oidc_callback(request, JWT_AUD_CONSOLE, cookie_name=AUTH_COOKIE_CONSOLE)
|
||||
|
||||
|
||||
async def mcp_oauth_authorize(request: Request) -> Response:
|
||||
@@ -13564,7 +13565,12 @@ def _build_console_middleware(cors_origins: list[str] | None = None) -> list[Mid
|
||||
|
||||
stack.append(cors_middleware(cors_origins))
|
||||
stack.append(
|
||||
Middleware(AuthMiddleware, jwt_audience=JWT_AUD_CONSOLE, jwt_version=jwt_version_slot())
|
||||
Middleware(
|
||||
AuthMiddleware,
|
||||
jwt_audience=JWT_AUD_CONSOLE,
|
||||
jwt_version=jwt_version_slot(),
|
||||
cookie_name=AUTH_COOKIE_CONSOLE,
|
||||
)
|
||||
)
|
||||
return stack
|
||||
|
||||
|
||||
+42
-21
@@ -54,7 +54,14 @@ log = get_logger(__name__)
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
AUTH_COOKIE = "turnstone_auth"
|
||||
AUTH_COOKIE = "turnstone_auth" # legacy unscoped name (pre-isolation); see per-surface names below
|
||||
# Per-surface cookie names. The server (:8080) and console (:8090) are co-hostable
|
||||
# on a single origin; cookies ignore port, so distinct *names* keep one surface's
|
||||
# session from clobbering the other's. Keyed by role/audience, NOT by node — the
|
||||
# cluster shares one JWT identity (same secret + audience), so a token stays
|
||||
# portable across nodes and per-instance names would break proxy identity re-mint.
|
||||
AUTH_COOKIE_SERVER = "turnstone_auth_server"
|
||||
AUTH_COOKIE_CONSOLE = "turnstone_auth_console"
|
||||
TOKEN_PREFIX = "ts_"
|
||||
TOKEN_BYTES = 32 # 64 hex chars after prefix
|
||||
|
||||
@@ -801,11 +808,12 @@ def check_request(
|
||||
jwt_audience: str = "",
|
||||
jwt_version: str = "",
|
||||
storage: Any = None,
|
||||
cookie_name: str,
|
||||
) -> tuple[bool, int, str, AuthResult | None]:
|
||||
"""Validate a request.
|
||||
|
||||
Checks ``Authorization: Bearer <token>`` first, then falls back to the
|
||||
``turnstone_auth`` cookie. Token types are auto-detected:
|
||||
*cookie_name* cookie. Token types are auto-detected:
|
||||
|
||||
- Contains ``.`` → JWT (validated with *jwt_secret*)
|
||||
- Starts with ``ts_`` → API token (looked up in *storage* by hash)
|
||||
@@ -818,7 +826,7 @@ def check_request(
|
||||
# Extract token from header or cookie
|
||||
raw_token = _extract_bearer(auth_header)
|
||||
if raw_token is None:
|
||||
raw_token = _extract_cookie(cookie_header, AUTH_COOKIE)
|
||||
raw_token = _extract_cookie(cookie_header, cookie_name)
|
||||
|
||||
if not raw_token:
|
||||
return False, 401, "Unauthorized: missing or invalid token", None
|
||||
@@ -932,22 +940,26 @@ def _extract_cookie(cookie_header: str | None, name: str) -> str | None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_set_cookie(token: str, max_age: int = 86400, *, secure: bool | None = None) -> str:
|
||||
def make_set_cookie(
|
||||
token: str, cookie_name: str, max_age: int = 86400, *, secure: bool | None = None
|
||||
) -> str:
|
||||
"""Return a ``Set-Cookie`` header value that stores the auth token.
|
||||
|
||||
*cookie_name* selects the per-surface cookie (``AUTH_COOKIE_SERVER`` /
|
||||
``AUTH_COOKIE_CONSOLE``) so co-hosted surfaces don't share a jar slot.
|
||||
When *secure* is ``None`` (default) the ``Secure`` flag is set
|
||||
unconditionally. Pass ``secure=False`` only for plaintext development.
|
||||
*max_age* defaults to 24 hours to match the default JWT expiry.
|
||||
"""
|
||||
val = f"{AUTH_COOKIE}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={max_age}"
|
||||
val = f"{cookie_name}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={max_age}"
|
||||
if secure is None or secure:
|
||||
val += "; Secure"
|
||||
return val
|
||||
|
||||
|
||||
def make_clear_cookie() -> str:
|
||||
"""Return a ``Set-Cookie`` header value that expires the auth cookie."""
|
||||
return f"{AUTH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"
|
||||
def make_clear_cookie(cookie_name: str) -> str:
|
||||
"""Return a ``Set-Cookie`` header value that expires the named auth cookie."""
|
||||
return f"{cookie_name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"
|
||||
|
||||
|
||||
def is_secure_request(headers: dict[str, str], scheme: str = "") -> bool:
|
||||
@@ -1094,10 +1106,18 @@ class AuthMiddleware:
|
||||
server (``JWT_AUD_SERVER``) and the console (``JWT_AUD_CONSOLE``).
|
||||
"""
|
||||
|
||||
def __init__(self, app: ASGIApp, jwt_audience: str = "", jwt_version: str = "") -> None:
|
||||
def __init__(
|
||||
self,
|
||||
app: ASGIApp,
|
||||
jwt_audience: str = "",
|
||||
jwt_version: str = "",
|
||||
*,
|
||||
cookie_name: str,
|
||||
) -> None:
|
||||
self.app = app
|
||||
self._jwt_audience = jwt_audience
|
||||
self._jwt_version = jwt_version
|
||||
self._cookie_name = cookie_name
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] != "http":
|
||||
@@ -1128,6 +1148,7 @@ class AuthMiddleware:
|
||||
jwt_audience=self._jwt_audience,
|
||||
jwt_version=self._jwt_version,
|
||||
storage=storage,
|
||||
cookie_name=self._cookie_name,
|
||||
)
|
||||
if not allowed:
|
||||
body: dict[str, Any] = {"error": msg}
|
||||
@@ -1154,7 +1175,7 @@ class AuthMiddleware:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def handle_auth_login(request: Request, audience: str) -> Response:
|
||||
async def handle_auth_login(request: Request, audience: str, cookie_name: str) -> Response:
|
||||
"""Shared ``POST /api/auth/login`` handler.
|
||||
|
||||
Authenticates via username:password or legacy token exchange, returning
|
||||
@@ -1257,16 +1278,16 @@ async def handle_auth_login(request: Request, audience: str) -> Response:
|
||||
response = JSONResponse(resp_body)
|
||||
cookie_value = jwt_token if jwt_token else body.get("token", "")
|
||||
if cookie_value:
|
||||
response.headers["Set-Cookie"] = make_set_cookie(cookie_value, secure=secure)
|
||||
response.headers["Set-Cookie"] = make_set_cookie(cookie_value, cookie_name, secure=secure)
|
||||
return response
|
||||
|
||||
|
||||
async def handle_auth_logout(request: Request) -> Response:
|
||||
async def handle_auth_logout(request: Request, cookie_name: str) -> Response:
|
||||
"""Shared ``POST /api/auth/logout`` handler — clear auth cookie."""
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
response = JSONResponse({"status": "ok"})
|
||||
response.headers["Set-Cookie"] = make_clear_cookie()
|
||||
response.headers["Set-Cookie"] = make_clear_cookie(cookie_name)
|
||||
return response
|
||||
|
||||
|
||||
@@ -1300,7 +1321,7 @@ async def handle_auth_status(request: Request) -> Response:
|
||||
return JSONResponse(resp)
|
||||
|
||||
|
||||
async def handle_auth_setup(request: Request, audience: str) -> Response:
|
||||
async def handle_auth_setup(request: Request, audience: str, cookie_name: str) -> Response:
|
||||
"""Shared ``POST /api/auth/setup`` handler — create first admin user.
|
||||
|
||||
Only works when zero users exist. Returns JWT on success.
|
||||
@@ -1401,11 +1422,11 @@ async def handle_auth_setup(request: Request, audience: str) -> Response:
|
||||
secure = is_secure_request(dict(request.headers), request.url.scheme)
|
||||
response = JSONResponse(resp_body)
|
||||
if jwt_token:
|
||||
response.headers["Set-Cookie"] = make_set_cookie(jwt_token, secure=secure)
|
||||
response.headers["Set-Cookie"] = make_set_cookie(jwt_token, cookie_name, secure=secure)
|
||||
return response
|
||||
|
||||
|
||||
async def handle_auth_whoami(request: Request) -> Response:
|
||||
async def handle_auth_whoami(request: Request, cookie_name: str) -> Response:
|
||||
"""Shared ``GET /api/auth/whoami`` handler — return authenticated user info.
|
||||
|
||||
Includes the JWT ``exp`` claim (epoch seconds) so the frontend can
|
||||
@@ -1440,7 +1461,7 @@ async def handle_auth_whoami(request: Request) -> Response:
|
||||
resp["permissions"] = ",".join(sorted(auth_result.permissions))
|
||||
# Surface the cookie/JWT expiry so the client can schedule refresh.
|
||||
# Decoded without re-validating (auth middleware already validated).
|
||||
cookie_token = request.cookies.get(AUTH_COOKIE, "")
|
||||
cookie_token = request.cookies.get(cookie_name, "")
|
||||
if cookie_token:
|
||||
try:
|
||||
import jwt as _jwt
|
||||
@@ -1455,7 +1476,7 @@ async def handle_auth_whoami(request: Request) -> Response:
|
||||
return JSONResponse(resp)
|
||||
|
||||
|
||||
async def handle_auth_refresh(request: Request, audience: str) -> Response:
|
||||
async def handle_auth_refresh(request: Request, audience: str, cookie_name: str) -> Response:
|
||||
"""Shared ``POST /api/auth/refresh`` handler — re-mint the auth cookie.
|
||||
|
||||
Requires a currently-valid auth cookie (auth middleware enforces).
|
||||
@@ -1551,7 +1572,7 @@ async def handle_auth_refresh(request: Request, audience: str) -> Response:
|
||||
|
||||
response = JSONResponse(resp_body)
|
||||
secure = is_secure_request(dict(request.headers), request.url.scheme)
|
||||
response.headers["Set-Cookie"] = make_set_cookie(new_token, secure=secure)
|
||||
response.headers["Set-Cookie"] = make_set_cookie(new_token, cookie_name, secure=secure)
|
||||
return response
|
||||
|
||||
|
||||
@@ -1657,7 +1678,7 @@ async def _refetch_jwks_locked(
|
||||
return fresh
|
||||
|
||||
|
||||
async def handle_oidc_callback(request: Request, audience: str) -> Response:
|
||||
async def handle_oidc_callback(request: Request, audience: str, cookie_name: str) -> Response:
|
||||
"""Shared ``GET /api/auth/oidc/callback`` handler — exchange code, provision user, issue JWT."""
|
||||
from starlette.responses import JSONResponse, RedirectResponse
|
||||
|
||||
@@ -1808,5 +1829,5 @@ async def handle_oidc_callback(request: Request, audience: str) -> Response:
|
||||
response = RedirectResponse("/?oidc_success=1", status_code=302)
|
||||
if jwt_token:
|
||||
secure = is_secure_request(dict(request.headers), request.url.scheme)
|
||||
response.headers["Set-Cookie"] = make_set_cookie(jwt_token, secure=secure)
|
||||
response.headers["Set-Cookie"] = make_set_cookie(jwt_token, cookie_name, secure=secure)
|
||||
return response
|
||||
|
||||
+13
-7
@@ -48,6 +48,7 @@ from turnstone.api.docs import make_docs_handler, make_openapi_handler
|
||||
from turnstone.api.server_spec import build_server_spec
|
||||
from turnstone.core.adapters.interactive_adapter import InteractiveAdapter
|
||||
from turnstone.core.auth import (
|
||||
AUTH_COOKIE_SERVER,
|
||||
DENY_EMPTY_SUB,
|
||||
JWT_AUD_SERVER,
|
||||
AuthMiddleware,
|
||||
@@ -2574,14 +2575,14 @@ async def auth_login(request: Request) -> Response:
|
||||
"""POST /v1/api/auth/login — authenticate and return JWT."""
|
||||
from turnstone.core.auth import handle_auth_login
|
||||
|
||||
return await handle_auth_login(request, JWT_AUD_SERVER)
|
||||
return await handle_auth_login(request, JWT_AUD_SERVER, cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
async def auth_logout(request: Request) -> Response:
|
||||
"""POST /v1/api/auth/logout — clear auth cookie."""
|
||||
from turnstone.core.auth import handle_auth_logout
|
||||
|
||||
return await handle_auth_logout(request)
|
||||
return await handle_auth_logout(request, cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
async def auth_status(request: Request) -> Response:
|
||||
@@ -2595,14 +2596,14 @@ async def auth_setup(request: Request) -> Response:
|
||||
"""POST /v1/api/auth/setup — create first admin user (public, one-time only)."""
|
||||
from turnstone.core.auth import handle_auth_setup
|
||||
|
||||
return await handle_auth_setup(request, JWT_AUD_SERVER)
|
||||
return await handle_auth_setup(request, JWT_AUD_SERVER, cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
async def auth_whoami(request: Request) -> Response:
|
||||
"""GET /v1/api/auth/whoami — return authenticated user info."""
|
||||
from turnstone.core.auth import handle_auth_whoami
|
||||
|
||||
return await handle_auth_whoami(request)
|
||||
return await handle_auth_whoami(request, cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
async def auth_refresh(request: Request) -> Response:
|
||||
@@ -2613,7 +2614,7 @@ async def auth_refresh(request: Request) -> Response:
|
||||
"""
|
||||
from turnstone.core.auth import handle_auth_refresh
|
||||
|
||||
return await handle_auth_refresh(request, JWT_AUD_SERVER)
|
||||
return await handle_auth_refresh(request, JWT_AUD_SERVER, cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
async def oidc_authorize(request: Request) -> Response:
|
||||
@@ -2627,7 +2628,7 @@ async def oidc_callback(request: Request) -> Response:
|
||||
"""GET /v1/api/auth/oidc/callback — OIDC callback, exchange code for JWT."""
|
||||
from turnstone.core.auth import handle_oidc_callback
|
||||
|
||||
return await handle_oidc_callback(request, JWT_AUD_SERVER)
|
||||
return await handle_oidc_callback(request, JWT_AUD_SERVER, cookie_name=AUTH_COOKIE_SERVER)
|
||||
|
||||
|
||||
async def mcp_oauth_authorize(request: Request) -> Response:
|
||||
@@ -3666,7 +3667,12 @@ def _build_middleware(cors_origins: list[str] | None = None) -> list[Middleware]
|
||||
stack.append(cors_middleware(cors_origins))
|
||||
stack.extend(
|
||||
[
|
||||
Middleware(AuthMiddleware, jwt_audience=JWT_AUD_SERVER, jwt_version=jwt_version_slot()),
|
||||
Middleware(
|
||||
AuthMiddleware,
|
||||
jwt_audience=JWT_AUD_SERVER,
|
||||
jwt_version=jwt_version_slot(),
|
||||
cookie_name=AUTH_COOKIE_SERVER,
|
||||
),
|
||||
Middleware(RateLimitMiddleware),
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user