Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,179 @@
|
||||
"""/auth/login, /auth/callback, /auth/logout: cookies, rotation and CSRF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ragcore.application.auth import SessionService, SignedTokenCodec, decode_session
|
||||
from ragcore.application.auth.ports import AuthorizationChallenge
|
||||
from ragcore.domain.auth import OidcClaims
|
||||
from ragcore.domain.control import Role
|
||||
from ragcore.main import create_app
|
||||
from ragcore.web.auth.dependencies import (
|
||||
PENDING_LOGIN_COOKIE_NAME,
|
||||
SESSION_COOKIE_NAME,
|
||||
get_session_codec,
|
||||
get_session_service,
|
||||
)
|
||||
|
||||
SECRET = b"0" * 32
|
||||
|
||||
|
||||
class FakeOidcProvider:
|
||||
def __init__(self, claims: OidcClaims) -> None:
|
||||
self.claims = claims
|
||||
|
||||
async def build_authorization_challenge(self, *, redirect_uri: str) -> AuthorizationChallenge:
|
||||
return AuthorizationChallenge(
|
||||
redirect_url="https://idp.example.test/authorize?mock=1",
|
||||
state="fixed-state-for-tests",
|
||||
nonce="fixed-nonce-for-tests",
|
||||
code_verifier="fixed-verifier-for-tests",
|
||||
)
|
||||
|
||||
async def complete_login(
|
||||
self, *, code: str, code_verifier: str, nonce: str, redirect_uri: str
|
||||
) -> OidcClaims:
|
||||
return self.claims
|
||||
|
||||
|
||||
def _fixture(*, https: bool = False) -> tuple[TestClient, SignedTokenCodec]:
|
||||
provider = FakeOidcProvider(
|
||||
OidcClaims(subject="alice", email="alice@example.test", groups=("ragcore-editors",))
|
||||
)
|
||||
service = SessionService(
|
||||
provider=provider, group_roles={"ragcore-editors": Role.EDITOR}, workspace_id=uuid4()
|
||||
)
|
||||
codec = SignedTokenCodec(secret=SECRET)
|
||||
api = create_app()
|
||||
api.dependency_overrides[get_session_service] = lambda: service
|
||||
api.dependency_overrides[get_session_codec] = lambda: codec
|
||||
base_url = "https://testserver" if https else "http://testserver"
|
||||
return TestClient(api, base_url=base_url, follow_redirects=False), codec
|
||||
|
||||
|
||||
def test_login_redirects_and_sets_a_pending_login_cookie() -> None:
|
||||
client, _ = _fixture()
|
||||
response = client.get("/auth/login")
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.headers["location"].startswith("https://idp.example.test/")
|
||||
set_cookie = response.headers.get("set-cookie", "")
|
||||
assert PENDING_LOGIN_COOKIE_NAME in set_cookie
|
||||
assert "HttpOnly" in set_cookie
|
||||
assert "samesite=lax" in set_cookie.lower()
|
||||
|
||||
|
||||
def test_pending_login_cookie_is_marked_secure_over_https() -> None:
|
||||
client, _ = _fixture(https=True)
|
||||
response = client.get("/auth/login")
|
||||
assert "Secure" in response.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
def test_pending_login_cookie_is_not_marked_secure_over_plain_http() -> None:
|
||||
client, _ = _fixture(https=False)
|
||||
response = client.get("/auth/login")
|
||||
assert "Secure" not in response.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
def test_full_login_then_callback_sets_a_rotated_session_cookie() -> None:
|
||||
client, _ = _fixture()
|
||||
login_response = client.get("/auth/login")
|
||||
|
||||
callback_response = client.get(
|
||||
"/auth/callback",
|
||||
params={"code": "auth-code", "state": "fixed-state-for-tests"},
|
||||
)
|
||||
|
||||
assert callback_response.status_code == 302
|
||||
assert callback_response.headers["location"] == "/admin/dashboard"
|
||||
set_cookie = callback_response.headers.get("set-cookie", "")
|
||||
assert SESSION_COOKIE_NAME in set_cookie
|
||||
# The pending-login cookie is cleared once login completes.
|
||||
assert f"{PENDING_LOGIN_COOKIE_NAME}=" in set_cookie or any(
|
||||
PENDING_LOGIN_COOKIE_NAME in header
|
||||
for header in callback_response.headers.get_list("set-cookie")
|
||||
)
|
||||
session_cookie_value = client.cookies.get(SESSION_COOKIE_NAME)
|
||||
pending_cookie_value = client.cookies.get(PENDING_LOGIN_COOKIE_NAME)
|
||||
assert session_cookie_value
|
||||
assert not pending_cookie_value
|
||||
assert login_response.headers["set-cookie"] != callback_response.headers["set-cookie"]
|
||||
|
||||
|
||||
def test_callback_with_mismatched_state_is_rejected() -> None:
|
||||
client, _ = _fixture()
|
||||
client.get("/auth/login")
|
||||
|
||||
response = client.get(
|
||||
"/auth/callback",
|
||||
params={"code": "auth-code", "state": "an-attacker-supplied-state"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["code"] == "INVALID_STATE"
|
||||
|
||||
|
||||
def test_callback_without_a_pending_login_cookie_is_rejected() -> None:
|
||||
client, _ = _fixture()
|
||||
|
||||
response = client.get(
|
||||
"/auth/callback",
|
||||
params={"code": "auth-code", "state": "fixed-state-for-tests"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["code"] == "LOGIN_EXPIRED"
|
||||
|
||||
|
||||
def test_logout_without_csrf_token_is_rejected() -> None:
|
||||
client, _ = _fixture()
|
||||
client.get("/auth/login")
|
||||
client.get("/auth/callback", params={"code": "auth-code", "state": "fixed-state-for-tests"})
|
||||
|
||||
response = client.post("/auth/logout")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["code"] == "CSRF_TOKEN_INVALID"
|
||||
|
||||
|
||||
def test_logout_with_a_forged_csrf_token_is_rejected() -> None:
|
||||
client, _ = _fixture()
|
||||
client.get("/auth/login")
|
||||
client.get("/auth/callback", params={"code": "auth-code", "state": "fixed-state-for-tests"})
|
||||
|
||||
response = client.post("/auth/logout", headers={"x-csrf-token": "not-the-real-token"})
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["code"] == "CSRF_TOKEN_INVALID"
|
||||
|
||||
|
||||
def test_logout_with_the_correct_csrf_token_succeeds_and_clears_the_session() -> None:
|
||||
client, codec = _fixture()
|
||||
client.get("/auth/login")
|
||||
client.get("/auth/callback", params={"code": "auth-code", "state": "fixed-state-for-tests"})
|
||||
|
||||
session = decode_session(client.cookies[SESSION_COOKIE_NAME], codec=codec)
|
||||
|
||||
response = client.post("/auth/logout", headers={"x-csrf-token": session.csrf_token})
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.headers["location"] == "/login"
|
||||
assert not client.cookies.get(SESSION_COOKIE_NAME)
|
||||
|
||||
|
||||
def test_logout_requires_authentication_before_csrf_is_even_checked() -> None:
|
||||
client, _ = _fixture()
|
||||
response = client.post("/auth/logout", headers={"x-csrf-token": "anything"})
|
||||
assert response.status_code == 401
|
||||
assert response.json()["code"] == "AUTHENTICATION_REQUIRED"
|
||||
|
||||
|
||||
def test_auth_routes_are_503_when_oidc_is_not_configured() -> None:
|
||||
api = create_app()
|
||||
client = TestClient(api, follow_redirects=False)
|
||||
response = client.get("/auth/login")
|
||||
assert response.status_code == 503
|
||||
assert response.json()["code"] == "AUTH_UNAVAILABLE"
|
||||
Reference in new issue
Block a user