Skip to content

Commit 22d2997

Browse files
committed
Changes for MyAccount Factors Schema
1 parent 7e9225f commit 22d2997

3 files changed

Lines changed: 38 additions & 11 deletions

File tree

src/auth0_server_python/auth_server/my_account_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,10 @@ async def get_factors(
412412
validation_errors=error_data.get("validation_errors", None),
413413
)
414414

415-
return GetFactorsResponse.model_validate(response.json())
415+
# Auth0 /me/v1/factors returns a plain array, not {"factors":[...]}
416+
raw = response.json()
417+
payload = raw if isinstance(raw, dict) else {"factors": raw}
418+
return GetFactorsResponse.model_validate(payload)
416419

417420
except Exception as e:
418421
if isinstance(e, (MyAccountApiError, ApiError)):

src/auth0_server_python/auth_types/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ class EnrollAuthenticationMethodRequest(BaseModel):
717717
email: Optional[str] = None
718718
phone_number: Optional[str] = None
719719
preferred_authentication_method: Optional[PreferredAuthMethod] = None
720-
user_identity_id: Optional[str] = None
720+
identity_user_id: Optional[str] = None # OAS: IdentityAuthenticationMethodBase.identity_user_id
721721
connection: Optional[str] = None
722722

723723

@@ -790,9 +790,8 @@ class ListAuthenticationMethodsResponse(BaseModel):
790790

791791
class Factor(BaseModel):
792792
model_config = ConfigDict(extra="allow")
793-
name: str
794-
enabled: Optional[bool] = None
795-
trial_expired: Optional[bool] = None
793+
type: str
794+
usage: Optional[list[str]] = None
796795

797796

798797
class GetFactorsResponse(BaseModel):

src/auth0_server_python/tests/test_my_account_client.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -525,15 +525,17 @@ async def test_get_factors_success(mocker):
525525
client = MyAccountClient(domain="auth0.local")
526526
response = AsyncMock()
527527
response.status_code = 200
528-
response.json = MagicMock(return_value={"factors": [{"name": "sms", "enabled": True}]})
528+
response.json = MagicMock(
529+
return_value={"factors": [{"type": "phone", "usage": ["primary"]}]}
530+
)
529531
mocker.patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=response)
530532

531533
result = await client.get_factors(access_token="token123")
532534

533535
assert isinstance(result, GetFactorsResponse)
534536
assert len(result.factors) == 1
535-
assert result.factors[0].name == "sms"
536-
assert result.factors[0].enabled is True
537+
assert result.factors[0].type == "phone"
538+
assert result.factors[0].usage == ["primary"]
537539

538540

539541
@pytest.mark.asyncio
@@ -596,12 +598,13 @@ async def test_get_factors_extra_fields(mocker):
596598
response = AsyncMock()
597599
response.status_code = 200
598600
response.json = MagicMock(return_value={
599-
"factors": [{"name": "webauthn-roaming", "enabled": True, "future_field": "value"}]
601+
"factors": [{"type": "webauthn-roaming", "usage": ["secondary"], "future_field": "value"}]
600602
})
601603
mocker.patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=response)
602604

603605
result = await client.get_factors(access_token="token123")
604-
assert result.factors[0].name == "webauthn-roaming"
606+
assert result.factors[0].type == "webauthn-roaming"
607+
assert result.factors[0].model_extra["future_field"] == "value"
605608

606609

607610
@pytest.mark.asyncio
@@ -804,6 +807,26 @@ async def test_enroll_authentication_method_success(mocker):
804807
assert result.authn_params_public_key.user.display_name == "Test User"
805808

806809

810+
@pytest.mark.asyncio
811+
async def test_enroll_authentication_method_sends_identity_user_id(mocker):
812+
"""identity_user_id must serialize to the request body under its OAS wire key."""
813+
client = MyAccountClient(domain="auth0.local")
814+
response = AsyncMock()
815+
response.status_code = 202
816+
response.headers = {"location": "/me/v1/authentication-methods/passkey|new"}
817+
response.json = MagicMock(return_value={"auth_session": "session_abc"})
818+
mock_post = mocker.patch(
819+
"httpx.AsyncClient.post", new_callable=AsyncMock, return_value=response
820+
)
821+
822+
req = EnrollAuthenticationMethodRequest(type="passkey", identity_user_id="auth0|abc123")
823+
await client.enroll_authentication_method(access_token="token123", request=req)
824+
825+
sent_body = mock_post.call_args[1]["json"]
826+
assert sent_body["identity_user_id"] == "auth0|abc123"
827+
assert "user_identity_id" not in sent_body
828+
829+
807830
@pytest.mark.asyncio
808831
async def test_enroll_authentication_method_public_key_extra_fields_preserved(mocker):
809832
"""Unknown WebAuthn fields (excludeCredentials, attestation, extensions) must not be dropped."""
@@ -1012,7 +1035,9 @@ async def test_get_factors_with_dpop_key(mocker):
10121035
client = MyAccountClient(domain="auth0.local")
10131036
response = AsyncMock()
10141037
response.status_code = 200
1015-
response.json = MagicMock(return_value={"factors": [{"name": "sms", "enabled": True}]})
1038+
response.json = MagicMock(
1039+
return_value={"factors": [{"type": "phone", "usage": ["primary"]}]}
1040+
)
10161041
mock_get = mocker.patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=response)
10171042

10181043
dpop_key = jwk_module.JWK.generate(kty="EC", crv="P-256")

0 commit comments

Comments
 (0)