|
23 | 23 | InvalidArgumentError, |
24 | 24 | IssuerValidationError, |
25 | 25 | MfaRequiredError, |
| 26 | + MfaVerifyError, |
26 | 27 | MissingRequiredArgumentError, |
27 | 28 | PasswordlessStartError, |
28 | 29 | PasswordlessVerifyError, |
@@ -662,6 +663,31 @@ async def test_verify_invalid_audience_maps_to_typed_error(self, mocker): |
662 | 663 | assert exc.value.code == "invalid_audience" |
663 | 664 | client._state_store.set.assert_not_awaited() |
664 | 665 |
|
| 666 | + @pytest.mark.asyncio |
| 667 | + async def test_verify_expired_id_token_maps_to_typed_error(self, mocker): |
| 668 | + client = _make_client() |
| 669 | + mocker.patch.object(client, "_get_oidc_metadata_cached", return_value=METADATA) |
| 670 | + mocker.patch.object( |
| 671 | + client, |
| 672 | + "_get_jwks_cached", |
| 673 | + return_value={"keys": [{"kty": "RSA", "kid": "k1"}]}, |
| 674 | + ) |
| 675 | + mocker.patch.object( |
| 676 | + client, |
| 677 | + "_verify_and_decode_jwt", |
| 678 | + side_effect=jwt.ExpiredSignatureError("signature has expired"), |
| 679 | + ) |
| 680 | + _mock_http(client, 200, {"access_token": "at", "id_token": "idt", "expires_in": 3600}) |
| 681 | + |
| 682 | + with pytest.raises(PasswordlessVerifyError) as exc: |
| 683 | + await client.passwordless.verify( |
| 684 | + VerifyPasswordlessOtpOptions( |
| 685 | + connection="email", email="user@example.com", verification_code="123456" |
| 686 | + ) |
| 687 | + ) |
| 688 | + assert exc.value.code == "token_expired" |
| 689 | + client._state_store.set.assert_not_awaited() |
| 690 | + |
665 | 691 | @pytest.mark.asyncio |
666 | 692 | async def test_client_ip_forwarded_on_verify(self, mocker): |
667 | 693 | client = _make_client() |
@@ -919,6 +945,60 @@ async def test_passwordless_mfa_verify_persist_creates_session(self, mocker): |
919 | 945 | assert saved_state.internal.sid == "SID-MFA" |
920 | 946 | assert saved_state.token_sets[0].access_token == "mfa_at" |
921 | 947 |
|
| 948 | + @pytest.mark.asyncio |
| 949 | + async def test_mfa_verify_persist_expired_id_token_maps_to_typed_error(self, mocker): |
| 950 | + client = _make_client() |
| 951 | + mocker.patch.object(client, "_get_oidc_metadata_cached", return_value=METADATA) |
| 952 | + mocker.patch.object( |
| 953 | + client, |
| 954 | + "_get_jwks_cached", |
| 955 | + return_value={"keys": [{"kty": "RSA", "kid": "k1"}]}, |
| 956 | + ) |
| 957 | + mocker.patch.object( |
| 958 | + client, |
| 959 | + "_verify_and_decode_jwt", |
| 960 | + side_effect=jwt.ExpiredSignatureError("signature has expired"), |
| 961 | + ) |
| 962 | + _mock_http( |
| 963 | + client, |
| 964 | + 403, |
| 965 | + { |
| 966 | + "error": "mfa_required", |
| 967 | + "error_description": "Additional factor required", |
| 968 | + "mfa_token": "raw_server_mfa_token", |
| 969 | + }, |
| 970 | + ) |
| 971 | + |
| 972 | + with pytest.raises(MfaRequiredError) as exc: |
| 973 | + await client.passwordless.verify( |
| 974 | + VerifyPasswordlessOtpOptions( |
| 975 | + connection="email", email="user@example.com", verification_code="123456" |
| 976 | + ), |
| 977 | + store_options={}, |
| 978 | + ) |
| 979 | + |
| 980 | + client._state_store.get = AsyncMock(return_value=None) |
| 981 | + mfa_response = AsyncMock() |
| 982 | + mfa_response.status_code = 200 |
| 983 | + mfa_response.headers = {} |
| 984 | + mfa_response.json = MagicMock( |
| 985 | + return_value={ |
| 986 | + "access_token": "mfa_at", |
| 987 | + "id_token": "mfa_idt", |
| 988 | + "token_type": "Bearer", |
| 989 | + "expires_in": 3600, |
| 990 | + "scope": "openid profile email", |
| 991 | + } |
| 992 | + ) |
| 993 | + mocker.patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mfa_response) |
| 994 | + |
| 995 | + with pytest.raises(MfaVerifyError, match="expired"): |
| 996 | + await client.mfa.verify( |
| 997 | + {"mfa_token": exc.value.mfa_token, "otp": "654321", "persist": True}, |
| 998 | + store_options={}, |
| 999 | + ) |
| 1000 | + client._state_store.set.assert_not_awaited() |
| 1001 | + |
922 | 1002 | @pytest.mark.asyncio |
923 | 1003 | async def test_verify_mfa_required_without_token_falls_through(self, mocker): |
924 | 1004 | # Third-party-strict / flex-commands-with-FF-off: 403 mfa_required with |
|
0 commit comments