import os from datetime import UTC, datetime from cryptography.hazmat.primitives.asymmetric import ec from fido2.cose import ES256 from fido2.webauthn import ( Aaguid, AttestedCredentialData, ) from httpx import AsyncClient from fastapi_oidc_op.models import User, WebAuthnCredential RP_ID = "localhost" ORIGIN = "http://localhost:8000" def _generate_credential() -> tuple[ec.EllipticCurvePrivateKey, bytes, AttestedCredentialData]: private_key = ec.generate_private_key(ec.SECP256R1()) cose_key = ES256.from_cryptography_key(private_key.public_key()) credential_id = os.urandom(32) attested = AttestedCredentialData.create(aaguid=Aaguid.NONE, credential_id=credential_id, public_key=cose_key) return private_key, credential_id, attested async def _setup_user_with_webauthn( client: AsyncClient, ) -> tuple[str, ec.EllipticCurvePrivateKey, bytes, AttestedCredentialData]: """Create a user with a WebAuthn credential in the repo.""" app = client._transport.app # type: ignore[union-attr] user_repo = app.state.user_repo cred_repo = app.state.credential_repo private_key, credential_id, attested = _generate_credential() user = User(userid="lusab-bansen", username="alice", created_at=datetime.now(UTC), updated_at=datetime.now(UTC)) await user_repo.create(user) await cred_repo.create_webauthn( WebAuthnCredential( user_id=user.userid, credential_id=credential_id, public_key=bytes(attested), sign_count=0, ) ) return user.userid, private_key, credential_id, attested async def test_webauthn_login_begin_returns_options(client: AsyncClient) -> None: _userid, _pk, _cid, _att = await _setup_user_with_webauthn(client) res = await client.post( "/login/webauthn/begin", data={"username": "alice"}, headers={"HX-Request": "true"}, ) assert res.status_code == 200 data = res.json() assert "publicKey" in data async def test_webauthn_login_begin_unknown_user(client: AsyncClient) -> None: res = await client.post( "/login/webauthn/begin", data={"username": "nobody"}, headers={"HX-Request": "true"}, ) # Should return error, not crash assert res.status_code == 200 assert 'role="alert"' in res.text or "not found" in res.text.lower() or "Invalid" in res.text async def test_webauthn_login_complete_sets_session(client: AsyncClient) -> None: """Test the begin endpoint + verify sign_count can be updated via repo.""" _userid, _private_key, credential_id, _attested = await _setup_user_with_webauthn(client) app = client._transport.app # type: ignore[union-attr] cred_repo = app.state.credential_repo # Verify begin endpoint works and returns valid options res1 = await client.post("/login/webauthn/begin", data={"username": "alice"}) assert res1.status_code == 200 data = res1.json() assert "publicKey" in data # Verify sign_count can be updated via the repo directly # (Full e2e WebAuthn complete testing requires browser interaction) stored = await cred_repo.get_webauthn_by_credential_id(credential_id) assert stored is not None stored.sign_count = 5 await cred_repo.update_webauthn(stored) updated = await cred_repo.get_webauthn_by_credential_id(credential_id) assert updated is not None assert updated.sign_count == 5