""" Tests for token endpoint route Tests the /auth/token endpoint for IndieAuth token exchange. """ import pytest from starpunk.tokens import create_authorization_code import hashlib def test_token_endpoint_success(client, app): """Test successful token exchange""" with app.app_context(): # Create authorization code code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create" ) # Exchange for token response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 200 data = response.get_json() assert 'access_token' in data assert data['token_type'] == 'Bearer' assert data['scope'] == 'create' assert data['me'] == 'https://user.example' def test_token_endpoint_with_pkce(client, app): """Test token exchange with PKCE""" with app.app_context(): # Generate PKCE verifier and challenge code_verifier = "test_verifier_with_sufficient_entropy_12345" code_challenge = hashlib.sha256(code_verifier.encode()).hexdigest() # Create authorization code with PKCE code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create", code_challenge=code_challenge, code_challenge_method="S256" ) # Exchange with correct verifier response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example', 'code_verifier': code_verifier }, content_type='application/x-www-form-urlencoded') assert response.status_code == 200 data = response.get_json() assert 'access_token' in data def test_token_endpoint_missing_grant_type(client, app): """Test token endpoint rejects missing grant_type""" with app.app_context(): response = client.post('/auth/token', data={ 'code': 'some_code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' assert 'grant_type' in data['error_description'] def test_token_endpoint_invalid_grant_type(client, app): """Test token endpoint rejects invalid grant_type""" with app.app_context(): response = client.post('/auth/token', data={ 'grant_type': 'password', 'code': 'some_code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'unsupported_grant_type' def test_token_endpoint_missing_code(client, app): """Test token endpoint rejects missing code""" with app.app_context(): response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' assert 'code' in data['error_description'] def test_token_endpoint_missing_client_id(client, app): """Test token endpoint rejects missing client_id""" with app.app_context(): response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': 'some_code', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' assert 'client_id' in data['error_description'] def test_token_endpoint_missing_redirect_uri(client, app): """Test token endpoint rejects missing redirect_uri""" with app.app_context(): response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': 'some_code', 'client_id': 'https://client.example', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' assert 'redirect_uri' in data['error_description'] def test_token_endpoint_missing_me(client, app): """Test token endpoint rejects missing me parameter""" with app.app_context(): response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': 'some_code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' assert 'me' in data['error_description'] def test_token_endpoint_invalid_code(client, app): """Test token endpoint rejects invalid authorization code""" with app.app_context(): response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': 'invalid_code_12345', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_grant' def test_token_endpoint_code_replay(client, app): """Test token endpoint prevents code replay attacks""" with app.app_context(): # Create authorization code code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create" ) # First exchange succeeds response1 = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response1.status_code == 200 # Second exchange fails (replay attack) response2 = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response2.status_code == 400 data = response2.get_json() assert data['error'] == 'invalid_grant' assert 'already been used' in data['error_description'] def test_token_endpoint_client_id_mismatch(client, app): """Test token endpoint rejects mismatched client_id""" with app.app_context(): code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create" ) response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://different-client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_grant' assert 'client_id' in data['error_description'] def test_token_endpoint_redirect_uri_mismatch(client, app): """Test token endpoint rejects mismatched redirect_uri""" with app.app_context(): code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create" ) response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/different-callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_grant' assert 'redirect_uri' in data['error_description'] def test_token_endpoint_me_mismatch(client, app): """Test token endpoint rejects mismatched me parameter""" with app.app_context(): code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create" ) response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://different-user.example' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_grant' assert 'me parameter' in data['error_description'] def test_token_endpoint_empty_scope(client, app): """Test token endpoint rejects authorization code with empty scope""" with app.app_context(): # Create authorization code with empty scope code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="" # Empty scope ) response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }, content_type='application/x-www-form-urlencoded') # IndieAuth spec: MUST NOT issue token if no scope assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_scope' def test_token_endpoint_wrong_content_type(client, app): """Test token endpoint rejects non-form-encoded requests""" with app.app_context(): response = client.post('/auth/token', json={ 'grant_type': 'authorization_code', 'code': 'some_code', 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' }) assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_request' assert 'Content-Type' in data['error_description'] def test_token_endpoint_pkce_missing_verifier(client, app): """Test token endpoint rejects PKCE exchange without verifier""" with app.app_context(): # Create authorization code with PKCE code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create", code_challenge="some_challenge", code_challenge_method="S256" ) # Exchange without verifier response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example' # Missing code_verifier }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_grant' assert 'code_verifier' in data['error_description'] def test_token_endpoint_pkce_wrong_verifier(client, app): """Test token endpoint rejects PKCE exchange with wrong verifier""" with app.app_context(): code_verifier = "correct_verifier" code_challenge = hashlib.sha256(code_verifier.encode()).hexdigest() # Create authorization code with PKCE code = create_authorization_code( me="https://user.example", client_id="https://client.example", redirect_uri="https://client.example/callback", scope="create", code_challenge=code_challenge, code_challenge_method="S256" ) # Exchange with wrong verifier response = client.post('/auth/token', data={ 'grant_type': 'authorization_code', 'code': code, 'client_id': 'https://client.example', 'redirect_uri': 'https://client.example/callback', 'me': 'https://user.example', 'code_verifier': 'wrong_verifier' }, content_type='application/x-www-form-urlencoded') assert response.status_code == 400 data = response.get_json() assert data['error'] == 'invalid_grant' assert 'code_verifier' in data['error_description']