"""Discord OAuth2 flow. SEC-AUTH-004. The authorization URL carried client_id, redirect_uri, response_type and scope — and nothing else. With no `state`, the callback accepted any authorization code presented to it, so an attacker could have a victim's browser consume a code issued for the attacker's own Discord account (RFC 6749 §10.12, "login CSRF"). """ from urllib.parse import parse_qs, urlparse import pytest from app.routes import auth as auth_module @pytest.fixture def discord_configured(monkeypatch): """Pretend the Discord application credentials are present. They are read into module-level constants at import time, so they are patched on the module rather than in the environment. """ monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_ID', '123456789012345678') monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_SECRET', 'not-a-real-secret') monkeypatch.setattr( auth_module, 'DISCORD_REDIRECT_URI', 'https://example.test/auth/discord/callback', ) def _authorize_params(response): return parse_qs(urlparse(response.headers['Location']).query) class TestAuthorizationRequest: def test_the_request_carries_a_state(self, client, discord_configured): response = client.get('/auth/discord/login', follow_redirects=False) assert response.status_code in (301, 302) params = _authorize_params(response) assert 'state' in params assert len(params['state'][0]) >= 32 def test_the_state_is_stored_in_the_session(self, client, discord_configured): response = client.get('/auth/discord/login', follow_redirects=False) sent = _authorize_params(response)['state'][0] with client.session_transaction() as sess: assert sess[auth_module.DISCORD_STATE_KEY] == sent def test_two_requests_get_different_states(self, client, discord_configured): first = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))[ 'state' ][0] second = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))[ 'state' ][0] assert first != second def test_scopes_and_redirect_are_preserved(self, client, discord_configured): params = _authorize_params(client.get('/auth/discord/login', follow_redirects=False)) assert params['scope'][0] == 'identify connections' assert params['response_type'][0] == 'code' assert params['redirect_uri'][0] == 'https://example.test/auth/discord/callback' def test_a_missing_redirect_uri_is_reported_not_raised(self, client, monkeypatch): """Quoting an unset redirect_uri used to raise inside the query builder instead of reporting a configuration problem.""" monkeypatch.setattr(auth_module, 'DISCORD_CLIENT_ID', '123456789012345678') monkeypatch.setattr(auth_module, 'DISCORD_REDIRECT_URI', None) response = client.get('/auth/discord/login', follow_redirects=False) assert response.status_code in (301, 302) assert '/auth/register' in response.headers['Location'] class TestCallbackStateValidation: """The callback must reject anything it did not itself initiate. None of these reach Discord: the state check happens before the token exchange, so a rejected callback performs no network call. """ def test_a_callback_without_state_is_rejected(self, client, discord_configured): client.get('/auth/discord/login', follow_redirects=False) response = client.get('/auth/discord/callback?code=attacker-code', follow_redirects=False) assert '/auth/register' in response.headers['Location'] def test_a_callback_with_a_wrong_state_is_rejected(self, client, discord_configured): client.get('/auth/discord/login', follow_redirects=False) response = client.get( '/auth/discord/callback?code=attacker-code&state=forged', follow_redirects=False ) assert '/auth/register' in response.headers['Location'] with client.session_transaction() as sess: assert 'discord_oauth' not in sess def test_a_callback_without_a_prior_request_is_rejected(self, client, discord_configured): """No /discord/login beforehand: nothing to match against.""" response = client.get( '/auth/discord/callback?code=x&state=anything', follow_redirects=False ) assert '/auth/register' in response.headers['Location'] def test_the_state_is_single_use(self, client, discord_configured): """Consumed on the first callback, valid or not, so it cannot be replayed.""" state = _authorize_params(client.get('/auth/discord/login', follow_redirects=False))[ 'state' ][0] client.get(f'/auth/discord/callback?code=x&state={state}', follow_redirects=False) with client.session_transaction() as sess: assert auth_module.DISCORD_STATE_KEY not in sess