Files
demo-oidc/tests/test_main.py
T
herel baef35115a fix(oidc): Add state parameter and validate nonce against login CSRF
The OIDC implicit-flow authorize request carried no state parameter and
the nonce was generated with weak Math.random() and never validated
client- or server-side, enabling login CSRF / forced authentication /
replay (AUTHZ-VULN-05).

Generate state and nonce with the Web Crypto CSPRNG, send state in the
authorize request, and validate both state and the id_token nonce claim
in the callback before storing tokens. Forward the nonce to /validate-
token so the backend also binds the id_token to the login flow.

Fixes the forged-callback fragment attack where a planted id_token with
a mismatched nonce was accepted, stored, and used by the SPA.
2026-07-08 13:34:31 +02:00

188 lines
6.3 KiB
Python

import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from main import app
# Create a test client
client = TestClient(app)
def test_health_check():
"""Test the health check endpoint."""
response = client.get("/health")
assert response.status_code == 200
assert "status" in response.json()
assert response.json()["status"] == "healthy"
def test_favicon():
"""Test the favicon endpoint."""
response = client.get("/favicon.ico")
# The favicon endpoint serves the actual favicon file, so we expect a 200
# If the file doesn't exist, it would return a 404, but in our case it should exist
assert response.status_code == 200
def test_get_client_config():
"""Test the client configuration endpoint."""
response = client.get("/config")
assert response.status_code == 200
data = response.json()
assert "client_id" in data
assert "issuer" in data
# Just check that we get a client_id, not necessarily the test value
assert isinstance(data["client_id"], str)
assert len(data["client_id"]) > 0
@patch('main.verify_token')
def test_validate_token_success(mock_verify_token):
"""Test successful token validation."""
# Mock the verify_token function to return a valid payload
mock_verify_token.return_value = {
"email": "rene.luria@infomaniak.com",
"iss": "https://login.infomaniak.com",
"aud": "test-client-id"
}
response = client.post("/validate-token", json={
"id_token": "valid.token.here"
})
assert response.status_code == 200
data = response.json()
assert data["valid"]
assert data["user"] is not None
assert data["user"]["email"] == "rene.luria@infomaniak.com"
assert data["secret_phrase"] == "Welcome to the secret club!"
@patch('main.verify_token')
def test_validate_token_unauthorized_user(mock_verify_token):
"""Test token validation for unauthorized user."""
# Mock the verify_token function to return a payload with unauthorized user
mock_verify_token.return_value = {
"email": "unauthorized@example.com",
"iss": "https://login.infomaniak.com",
"aud": "test-client-id"
}
response = client.post("/validate-token", json={
"id_token": "valid.token.here"
})
assert response.status_code == 200
data = response.json()
assert data["valid"]
assert data["user"] is not None
assert data["user"]["email"] == "unauthorized@example.com"
assert data["secret_phrase"] is None
@patch('main.verify_token')
def test_validate_token_missing_email(mock_verify_token):
"""Test token validation when email is missing from token."""
# Mock the verify_token function to return a payload without email
mock_verify_token.return_value = {
"iss": "https://login.infomaniak.com",
"aud": "test-client-id"
}
response = client.post("/validate-token", json={
"id_token": "token.without.email"
})
assert response.status_code == 200
data = response.json()
assert not data["valid"]
assert data["error"] == "Email not found in token"
@patch('main.verify_token')
def test_validate_token_invalid(mock_verify_token):
"""Test validation with an invalid token."""
# Mock the verify_token function to raise an exception
mock_verify_token.side_effect = ValueError("Invalid token")
response = client.post("/validate-token", json={
"id_token": "invalid.token.here"
})
assert response.status_code == 200
data = response.json()
assert not data["valid"]
assert data["error"] == "Invalid token"
@patch('main.verify_token')
def test_validate_token_passes_nonce_to_verify(mock_verify_token):
"""The endpoint must forward the supplied nonce to verify_token (anti-replay binding)."""
mock_verify_token.return_value = {
"email": "rene.luria@infomaniak.com",
"iss": "https://login.infomaniak.com",
"aud": "test-client-id",
"nonce": "abc123",
}
response = client.post("/validate-token", json={
"id_token": "valid.token.here",
"nonce": "abc123"
})
assert response.status_code == 200
assert response.json()["valid"]
# verify_token must have been called with the nonce as the second positional arg
assert mock_verify_token.call_args.args[1] == "abc123"
@patch('main.verify_token')
def test_validate_token_rejects_nonce_mismatch(mock_verify_token):
"""A mismatched nonce (forced-auth / replay token) is rejected by the backend."""
mock_verify_token.side_effect = ValueError("Nonce mismatch: token is not bound to this login")
response = client.post("/validate-token", json={
"id_token": "forged.token.here",
"nonce": "abc123"
})
assert response.status_code == 200
data = response.json()
assert not data["valid"]
assert "Nonce mismatch" in data["error"]
def test_validate_token_empty_request():
"""Test validation with empty token."""
response = client.post("/validate-token", json={
"id_token": ""
})
assert response.status_code == 200
data = response.json()
assert not data["valid"]
@patch('main.refresh_token')
def test_refresh_token_success(mock_refresh_token):
"""Test successful token refresh."""
# Mock the refresh_token function
mock_refresh_token.return_value = {
"access_token": "new-access-token",
"id_token": "new-id-token",
"expires_in": 3600,
"token_type": "Bearer"
}
response = client.post("/refresh-token", json={
"refresh_token": "valid-refresh-token"
})
assert response.status_code == 200
data = response.json()
assert data["access_token"] == "new-access-token"
assert data["id_token"] == "new-id-token"
assert data["expires_in"] == 3600
assert data["token_type"] == "Bearer"
@patch('main.refresh_token')
def test_refresh_token_failure(mock_refresh_token):
"""Test token refresh failure."""
# Mock the refresh_token function to raise an exception
mock_refresh_token.side_effect = ValueError("Invalid refresh token")
response = client.post("/refresh-token", json={
"refresh_token": "invalid-refresh-token"
})
assert response.status_code == 200
data = response.json()
assert data["error"] == "Invalid refresh token"