""" Tests for authorization endpoint route Tests the /auth/authorization endpoint for IndieAuth client authorization. """ import pytest from starpunk.auth import create_session from urllib.parse import urlparse, parse_qs def create_admin_session(client, app): """Helper to create an authenticated admin session""" with app.test_request_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') session_token = create_session(admin_me) client.set_cookie('starpunk_session', session_token) return session_token def test_authorization_endpoint_get_not_logged_in(client, app): """Test authorization endpoint redirects to login when not authenticated""" response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create' }) # Should redirect to login assert response.status_code == 302 assert '/auth/login' in response.location def test_authorization_endpoint_get_logged_in(client, app): """Test authorization endpoint shows consent form when authenticated""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create' }) assert response.status_code == 200 assert b'Authorization Request' in response.data assert b'https://client.example' in response.data assert b'create' in response.data def test_authorization_endpoint_missing_response_type(client, app): """Test authorization endpoint rejects missing response_type""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123' }) assert response.status_code == 400 assert b'Missing response_type' in response.data def test_authorization_endpoint_invalid_response_type(client, app): """Test authorization endpoint rejects unsupported response_type""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'token', # Only 'code' is supported 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123' }) assert response.status_code == 400 assert b'Unsupported response_type' in response.data def test_authorization_endpoint_missing_client_id(client, app): """Test authorization endpoint rejects missing client_id""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123' }) assert response.status_code == 400 assert b'Missing client_id' in response.data def test_authorization_endpoint_missing_redirect_uri(client, app): """Test authorization endpoint rejects missing redirect_uri""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'state': 'random_state_123' }) assert response.status_code == 400 assert b'Missing redirect_uri' in response.data def test_authorization_endpoint_missing_state(client, app): """Test authorization endpoint rejects missing state""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback' }) assert response.status_code == 400 assert b'Missing state' in response.data def test_authorization_endpoint_empty_scope(client, app): """Test authorization endpoint allows empty scope""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': '' # Empty scope allowed per IndieAuth spec }) assert response.status_code == 200 assert b'Authorization Request' in response.data def test_authorization_endpoint_filters_unsupported_scopes(client, app): """Test authorization endpoint filters to supported scopes only""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create update delete' # Only 'create' is supported in V1 }) assert response.status_code == 200 # Should only show 'create' scope assert b'create' in response.data def test_authorization_endpoint_post_approve(client, app): """Test authorization approval generates code and redirects""" create_admin_session(client, app) with app.app_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') response = client.post('/auth/authorization', data={ 'approve': 'yes', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create', 'me': admin_me, 'response_type': 'code' }) # Should redirect to client's redirect_uri assert response.status_code == 302 assert response.location.startswith('https://client.example/callback') # Parse redirect URL parsed = urlparse(response.location) params = parse_qs(parsed.query) # Should include code and state assert 'code' in params assert 'state' in params assert params['state'][0] == 'random_state_123' assert len(params['code'][0]) > 0 def test_authorization_endpoint_post_deny(client, app): """Test authorization denial redirects with error""" create_admin_session(client, app) with app.app_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') response = client.post('/auth/authorization', data={ 'approve': 'no', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create', 'me': admin_me, 'response_type': 'code' }) # Should redirect to client's redirect_uri with error assert response.status_code == 302 assert response.location.startswith('https://client.example/callback') # Parse redirect URL parsed = urlparse(response.location) params = parse_qs(parsed.query) # Should include error assert 'error' in params assert params['error'][0] == 'access_denied' assert 'state' in params assert params['state'][0] == 'random_state_123' def test_authorization_endpoint_post_not_logged_in(client, app): """Test authorization POST requires authentication""" with app.app_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') response = client.post('/auth/authorization', data={ 'approve': 'yes', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create', 'me': admin_me, 'response_type': 'code' }) # Should redirect to login assert response.status_code == 302 assert '/auth/login' in response.location def test_authorization_endpoint_with_pkce(client, app): """Test authorization endpoint accepts PKCE parameters""" create_admin_session(client, app) response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create', 'code_challenge': 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', 'code_challenge_method': 'S256' }) assert response.status_code == 200 assert b'Authorization Request' in response.data def test_authorization_endpoint_post_with_pkce(client, app): """Test authorization approval preserves PKCE parameters""" create_admin_session(client, app) with app.app_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') response = client.post('/auth/authorization', data={ 'approve': 'yes', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create', 'me': admin_me, 'response_type': 'code', 'code_challenge': 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', 'code_challenge_method': 'S256' }) assert response.status_code == 302 assert response.location.startswith('https://client.example/callback') # Parse redirect URL parsed = urlparse(response.location) params = parse_qs(parsed.query) # Should have code and state assert 'code' in params assert 'state' in params def test_authorization_endpoint_preserves_me_parameter(client, app): """Test authorization endpoint uses ADMIN_ME as identity""" create_admin_session(client, app) with app.app_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') response = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create' }) assert response.status_code == 200 # Should show admin's identity in the form assert admin_me.encode() in response.data def test_authorization_flow_end_to_end(client, app): """Test complete authorization flow from consent to token exchange""" create_admin_session(client, app) with app.app_context(): admin_me = app.config.get('ADMIN_ME', 'https://test.example.com') # Step 1: Get authorization form response1 = client.get('/auth/authorization', query_string={ 'response_type': 'code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create' }) assert response1.status_code == 200 # Step 2: Approve authorization response2 = client.post('/auth/authorization', data={ 'approve': 'yes', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'state': 'random_state_123', 'scope': 'create', 'me': admin_me, 'response_type': 'code' }) assert response2.status_code == 302 # Extract authorization code parsed = urlparse(response2.location) params = parse_qs(parsed.query) code = params['code'][0] # Step 3: Exchange code for token response3 = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': admin_me }, content_type='application/x-www-form-urlencoded') assert response3.status_code == 200 token_data = response3.get_json() assert 'access_token' in token_data assert token_data['token_type'] == 'Bearer' assert token_data['scope'] == 'create' assert token_data['me'] == admin_me