From dbf6d0092f0a64da415a3d10cf040e291763c233 Mon Sep 17 00:00:00 2001 From: BinoyOza-okta Date: Wed, 3 Jun 2026 14:05:30 +0530 Subject: [PATCH 1/2] fix: Add UserFactorSignedNonce schema and discriminator mapping for Okta FastPass factor This commit fixes deserialization failures in `list_factors()` (GET `/api/v1/users/{userId}/factors`) for users enrolled with the Okta FastPass `signed_nonce` factor type. Problem: - `UserApi.list_factors()` raised a Pydantic ValidationError whenever the response contained a factor with `factorType: "signed_nonce"`. - The `UserFactorType` enum already included `signed_nonce`, but the `UserFactor` schema in the OpenAPI spec lacked both a discriminator mapping for it and a dedicated `UserFactorSignedNonce` subschema. - As a result, the generated SDK had no class to route the payload to, and discriminator validation rejected the response: Discriminator property name: factorType, mapping: {"call": ..., "email": ..., "push": ..., "question": ..., "sms": ..., "token": ..., "token:hardware": ..., "token:hotp": ..., "token:software:totp": ..., "u2f": ..., "web": ..., "webauthn": ...} (no entry for `signed_nonce`). Root Cause: - Missing `signed_nonce: '#/components/schemas/UserFactorSignedNonce'` entry under `UserFactor.discriminator.mapping` in `openapi/api.yaml`. - Missing `UserFactorSignedNonce`, `UserFactorSignedNonceProfile`, and `UserFactorSignedNonceProfileKey` schema definitions describing the Okta FastPass payload (device metadata + JWK-style key material). Solution: 1. Added `signed_nonce` to the `UserFactor` discriminator mapping in `openapi/api.yaml`. 2. Defined three new component schemas in `openapi/api.yaml`: - `UserFactorSignedNonce` (allOf `UserFactor` + `provider: OKTA`, `factorType: signed_nonce`, `profile`) - `UserFactorSignedNonceProfile` (device metadata: `credentialId`, `deviceType`, `name`, `platform`, `version`, `keys[]`) - `UserFactorSignedNonceProfileKey` (JWK fields: `kty`, `use`, `kid`, `jwkType`, `e`, `n`, `crv`, `x`, `y`, `x5c`) 3. Regenerated model files: - `okta/models/user_factor_signed_nonce.py` - `okta/models/user_factor_signed_nonce_profile.py` - `okta/models/user_factor_signed_nonce_profile_key.py` 4. Updated `okta/models/user_factor.py` to register the new subclass in the discriminator map, the `oneOf` union, and `from_dict()` routing. 5. Wired the new models into the lazy-import maps in `okta/__init__.py` and `okta/models/__init__.py` (added to the `user_factor` model group). 6. Added API docs: `docs/UserFactorSignedNonce.md`, `docs/UserFactorSignedNonceProfile.md`, `docs/UserFactorSignedNonceProfileKey.md`. Testing: - Verified against a live Okta org with a user enrolled in Okta FastPass (`signed_nonce`). `client.list_factors(user_id)` now returns the full factor list, including `UserFactorSignedNonce` instances correctly deserialized with populated `profile` and `profile.keys` fields. - No breaking changes to existing factor types; all other discriminator mappings remain intact. Related: - okta/okta-sdk-python#311 - okta/okta-sdk-python#387 Fixes: OKTA-1152856 --- docs/UserFactorSignedNonce.md | 32 ++++ docs/UserFactorSignedNonceProfile.md | 35 ++++ docs/UserFactorSignedNonceProfileKey.md | 39 +++++ okta/__init__.py | 3 + okta/models/__init__.py | 4 + okta/models/user_factor.py | 9 + okta/models/user_factor_signed_nonce.py | 163 ++++++++++++++++++ .../user_factor_signed_nonce_profile.py | 142 +++++++++++++++ .../user_factor_signed_nonce_profile_key.py | 163 ++++++++++++++++++ openapi/api.yaml | 94 ++++++++++ 10 files changed, 684 insertions(+) create mode 100644 docs/UserFactorSignedNonce.md create mode 100644 docs/UserFactorSignedNonceProfile.md create mode 100644 docs/UserFactorSignedNonceProfileKey.md create mode 100644 okta/models/user_factor_signed_nonce.py create mode 100644 okta/models/user_factor_signed_nonce_profile.py create mode 100644 okta/models/user_factor_signed_nonce_profile_key.py diff --git a/docs/UserFactorSignedNonce.md b/docs/UserFactorSignedNonce.md new file mode 100644 index 00000000..4e3db39e --- /dev/null +++ b/docs/UserFactorSignedNonce.md @@ -0,0 +1,32 @@ +# UserFactorSignedNonce + +`signed_nonce` is the factor type for [Okta FastPass](https://help.okta.com/oie/en-us/content/topics/identity-engine/devices/fp/fp-main.htm). You can't use the Factors API to enroll or activate Okta FastPass (`signed_nonce`) for a user. Use the [Okta Verify](https://help.okta.com/en-us/content/topics/mobile/okta-verify-overview.htm) authenticator enrollment flow instead. You can use the Factors API to list and delete `signed_nonce` factors. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**factor_type** | **object** | | [optional] +**profile** | [**UserFactorSignedNonceProfile**](UserFactorSignedNonceProfile.md) | | [optional] +**provider** | **str** | | [optional] + +## Example + +```python +from okta.models.user_factor_signed_nonce import UserFactorSignedNonce + +# TODO update the JSON string below +json = "{}" +# create an instance of UserFactorSignedNonce from a JSON string +user_factor_signed_nonce_instance = UserFactorSignedNonce.from_json(json) +# print the JSON string representation of the object +print(UserFactorSignedNonce.to_json()) + +# convert the object into a dict +user_factor_signed_nonce_dict = user_factor_signed_nonce_instance.to_dict() +# create an instance of UserFactorSignedNonce from a dict +user_factor_signed_nonce_from_dict = UserFactorSignedNonce.from_dict(user_factor_signed_nonce_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/UserFactorSignedNonceProfile.md b/docs/UserFactorSignedNonceProfile.md new file mode 100644 index 00000000..1ebfa388 --- /dev/null +++ b/docs/UserFactorSignedNonceProfile.md @@ -0,0 +1,35 @@ +# UserFactorSignedNonceProfile + +Profile for the Okta FastPass (signed nonce) factor + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**credential_id** | **str** | ID for the factor credential | [optional] +**device_type** | **str** | Type of device | [optional] +**name** | **str** | Name of the device | [optional] +**platform** | **str** | OS platform of the associated device | [optional] +**version** | **str** | OS version of the associated device | [optional] +**keys** | [**List[UserFactorSignedNonceProfileKey]**](UserFactorSignedNonceProfileKey.md) | Cryptographic keys associated with the signed nonce factor | [optional] + +## Example + +```python +from okta.models.user_factor_signed_nonce_profile import UserFactorSignedNonceProfile + +# TODO update the JSON string below +json = "{}" +# create an instance of UserFactorSignedNonceProfile from a JSON string +user_factor_signed_nonce_profile_instance = UserFactorSignedNonceProfile.from_json(json) +# print the JSON string representation of the object +print(UserFactorSignedNonceProfile.to_json()) + +# convert the object into a dict +user_factor_signed_nonce_profile_dict = user_factor_signed_nonce_profile_instance.to_dict() +# create an instance of UserFactorSignedNonceProfile from a dict +user_factor_signed_nonce_profile_from_dict = UserFactorSignedNonceProfile.from_dict(user_factor_signed_nonce_profile_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/UserFactorSignedNonceProfileKey.md b/docs/UserFactorSignedNonceProfileKey.md new file mode 100644 index 00000000..bade2661 --- /dev/null +++ b/docs/UserFactorSignedNonceProfileKey.md @@ -0,0 +1,39 @@ +# UserFactorSignedNonceProfileKey + +JSON Web Key (JWK) for signed nonce verification + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**kty** | **str** | Key type | [optional] +**use** | **str** | Key usage | [optional] +**kid** | **str** | Key ID | [optional] +**jwk_type** | **str** | Purpose of the key | [optional] +**e** | **str** | RSA public exponent (present only for RSA keys) | [optional] +**n** | **str** | RSA modulus (present only for RSA keys) | [optional] +**crv** | **str** | EC curve name (present only for EC keys) | [optional] +**x** | **str** | EC x-coordinate (present only for EC keys) | [optional] +**y** | **str** | EC y-coordinate (present only for EC keys) | [optional] +**x5c** | **List[str]** | X.509 certificate chain | [optional] + +## Example + +```python +from okta.models.user_factor_signed_nonce_profile_key import UserFactorSignedNonceProfileKey + +# TODO update the JSON string below +json = "{}" +# create an instance of UserFactorSignedNonceProfileKey from a JSON string +user_factor_signed_nonce_profile_key_instance = UserFactorSignedNonceProfileKey.from_json(json) +# print the JSON string representation of the object +print(UserFactorSignedNonceProfileKey.to_json()) + +# convert the object into a dict +user_factor_signed_nonce_profile_key_dict = user_factor_signed_nonce_profile_key_instance.to_dict() +# create an instance of UserFactorSignedNonceProfileKey from a dict +user_factor_signed_nonce_profile_key_from_dict = UserFactorSignedNonceProfileKey.from_dict(user_factor_signed_nonce_profile_key_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/okta/__init__.py b/okta/__init__.py index aeddde3a..f211e496 100644 --- a/okta/__init__.py +++ b/okta/__init__.py @@ -1803,6 +1803,9 @@ "UserFactorSMSProfile": "okta.models.user_factor_sms_profile", "UserFactorSecurityQuestion": "okta.models.user_factor_security_question", "UserFactorSecurityQuestionProfile": "okta.models.user_factor_security_question_profile", + "UserFactorSignedNonce": "okta.models.user_factor_signed_nonce", + "UserFactorSignedNonceProfile": "okta.models.user_factor_signed_nonce_profile", + "UserFactorSignedNonceProfileKey": "okta.models.user_factor_signed_nonce_profile_key", "UserFactorStatus": "okta.models.user_factor_status", "UserFactorSupported": "okta.models.user_factor_supported", "UserFactorToken": "okta.models.user_factor_token", diff --git a/okta/models/__init__.py b/okta/models/__init__.py index e15c879f..2aaaac47 100644 --- a/okta/models/__init__.py +++ b/okta/models/__init__.py @@ -232,6 +232,7 @@ 'UserFactorEmail', 'UserFactorPush', 'UserFactorSecurityQuestion', + "UserFactorSignedNonce", 'UserFactorSMS', 'UserFactorToken', 'UserFactorTokenHardware', @@ -2004,6 +2005,9 @@ "UserFactorSMSProfile": "okta.models.user_factor_sms_profile", "UserFactorSecurityQuestion": "okta.models.user_factor_security_question", "UserFactorSecurityQuestionProfile": "okta.models.user_factor_security_question_profile", + "UserFactorSignedNonce": "okta.models.user_factor_signed_nonce", + "UserFactorSignedNonceProfile": "okta.models.user_factor_signed_nonce_profile", + "UserFactorSignedNonceProfileKey": "okta.models.user_factor_signed_nonce_profile_key", "UserFactorStatus": "okta.models.user_factor_status", "UserFactorSupported": "okta.models.user_factor_supported", "UserFactorToken": "okta.models.user_factor_token", diff --git a/okta/models/user_factor.py b/okta/models/user_factor.py index 03ba4997..bc023f3a 100644 --- a/okta/models/user_factor.py +++ b/okta/models/user_factor.py @@ -42,6 +42,7 @@ from okta.models.user_factor_email import UserFactorEmail from okta.models.user_factor_push import UserFactorPush from okta.models.user_factor_security_question import UserFactorSecurityQuestion + from okta.models.user_factor_signed_nonce import UserFactorSignedNonce from okta.models.user_factor_sms import UserFactorSMS from okta.models.user_factor_token import UserFactorToken from okta.models.user_factor_token_hardware import UserFactorTokenHardware @@ -113,6 +114,7 @@ class UserFactor(BaseModel): "email": "UserFactorEmail", "push": "UserFactorPush", "question": "UserFactorSecurityQuestion", + "signed_nonce": "UserFactorSignedNonce", "sms": "UserFactorSMS", "token": "UserFactorToken", "token:hardware": "UserFactorTokenHardware", @@ -148,6 +150,7 @@ def from_json(cls, json_str: str) -> Optional[ UserFactorEmail, UserFactorPush, UserFactorSecurityQuestion, + UserFactorSignedNonce, UserFactorSMS, UserFactorToken, UserFactorTokenHardware, @@ -207,6 +210,7 @@ def from_dict(cls, obj: Dict[str, Any]) -> Optional[ UserFactorEmail, UserFactorPush, UserFactorSecurityQuestion, + UserFactorSignedNonce, UserFactorSMS, UserFactorToken, UserFactorTokenHardware, @@ -242,6 +246,11 @@ def from_dict(cls, obj: Dict[str, Any]) -> Optional[ if object_type == cls.__name__: return cls.model_validate(obj) return models.UserFactorSecurityQuestion.from_dict(obj) + if object_type == "UserFactorSignedNonce": + # Check if the discriminator maps to the same class to avoid infinite recursion + if object_type == cls.__name__: + return cls.model_validate(obj) + return models.UserFactorSignedNonce.from_dict(obj) if object_type == "UserFactorSMS": # Check if the discriminator maps to the same class to avoid infinite recursion if object_type == cls.__name__: diff --git a/okta/models/user_factor_signed_nonce.py b/okta/models/user_factor_signed_nonce.py new file mode 100644 index 00000000..f07d36ba --- /dev/null +++ b/okta/models/user_factor_signed_nonce.py @@ -0,0 +1,163 @@ +# The Okta software accompanied by this notice is provided pursuant to the following terms: +# Copyright © 2025-Present, Okta, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +# License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS +# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and limitations under the License. +# coding: utf-8 + +""" +Okta Admin Management + +Allows customers to easily access the Okta Management APIs + +The version of the OpenAPI document: 5.1.0 +Contact: devex-public@okta.com +Generated by OpenAPI Generator (https://openapi-generator.tech) + +Do not edit the class manually. +""" # noqa: E501 + +from __future__ import annotations + +import json +import pprint +import re # noqa: F401 +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set + +from pydantic import ConfigDict, Field, StrictStr, field_validator +from typing_extensions import Self + +from okta.models.user_factor import UserFactor +from okta.models.user_factor_links import UserFactorLinks +from okta.models.user_factor_signed_nonce_profile import UserFactorSignedNonceProfile + + +class UserFactorSignedNonce(UserFactor): + """ + `signed_nonce` is the factor type for [Okta FastPass]( + https://help.okta.com/oie/en-us/content/topics/identity-engine/devices/fp/fp-main.htm). You can't use the Factors API to + enroll or activate Okta FastPass (`signed_nonce`) for a user. Use the [Okta Verify]( + https://help.okta.com/en-us/content/topics/mobile/okta-verify-overview.htm) authenticator enrollment flow instead. You + can use the Factors API to list and delete `signed_nonce` factors. + """ # noqa: E501 + + factor_type: Optional[Any] = Field(default=None, alias="factorType") + profile: Optional[UserFactorSignedNonceProfile] = None + provider: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = [ + "created", + "factorType", + "id", + "lastUpdated", + "profile", + "provider", + "status", + "vendorName", + "_embedded", + "_links", + ] + + @field_validator("provider") + def provider_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(["OKTA"]): + raise ValueError("must be one of enum values ('OKTA')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UserFactorSignedNonce from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of profile + if self.profile: + if not isinstance(self.profile, dict): + _dict["profile"] = self.profile.to_dict() + else: + _dict["profile"] = self.profile + + # override the default output from pydantic by calling `to_dict()` of links + if self.links: + if not isinstance(self.links, dict): + _dict["_links"] = self.links.to_dict() + else: + _dict["_links"] = self.links + + # set to None if factor_type (nullable) is None + # and model_fields_set contains the field + if self.factor_type is None and "factor_type" in self.model_fields_set: + _dict["factorType"] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UserFactorSignedNonce from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate( + { + "created": obj.get("created"), + "factorType": obj.get("factorType"), + "id": obj.get("id"), + "lastUpdated": obj.get("lastUpdated"), + "profile": ( + UserFactorSignedNonceProfile.from_dict(obj["profile"]) + if obj.get("profile") is not None + else None + ), + "provider": obj.get("provider"), + "status": obj.get("status"), + "vendorName": obj.get("vendorName"), + "_embedded": obj.get("_embedded"), + "_links": ( + UserFactorLinks.from_dict(obj["_links"]) + if obj.get("_links") is not None + else None + ), + } + ) + return _obj diff --git a/okta/models/user_factor_signed_nonce_profile.py b/okta/models/user_factor_signed_nonce_profile.py new file mode 100644 index 00000000..8d0cc239 --- /dev/null +++ b/okta/models/user_factor_signed_nonce_profile.py @@ -0,0 +1,142 @@ +# The Okta software accompanied by this notice is provided pursuant to the following terms: +# Copyright © 2025-Present, Okta, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +# License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS +# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and limitations under the License. +# coding: utf-8 + +""" +Okta Admin Management + +Allows customers to easily access the Okta Management APIs + +The version of the OpenAPI document: 5.1.0 +Contact: devex-public@okta.com +Generated by OpenAPI Generator (https://openapi-generator.tech) + +Do not edit the class manually. +""" # noqa: E501 + +from __future__ import annotations + +import json +import pprint +import re # noqa: F401 +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing_extensions import Self + +from okta.models.user_factor_signed_nonce_profile_key import ( + UserFactorSignedNonceProfileKey, +) + + +class UserFactorSignedNonceProfile(BaseModel): + """ + Profile for the Okta FastPass (signed nonce) factor + """ # noqa: E501 + + credential_id: Optional[StrictStr] = Field( + default=None, description="ID for the factor credential", alias="credentialId" + ) + device_type: Optional[StrictStr] = Field( + default=None, description="Type of device", alias="deviceType" + ) + name: Optional[StrictStr] = Field(default=None, description="Name of the device") + platform: Optional[StrictStr] = Field( + default=None, description="OS platform of the associated device" + ) + version: Optional[StrictStr] = Field( + default=None, description="OS version of the associated device" + ) + keys: Optional[List[UserFactorSignedNonceProfileKey]] = Field( + default=None, + description="Cryptographic keys associated with the signed nonce factor", + ) + __properties: ClassVar[List[str]] = [ + "credentialId", + "deviceType", + "name", + "platform", + "version", + "keys", + ] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UserFactorSignedNonceProfile from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in keys (list) + _items = [] + if self.keys: + for _item in self.keys: + if _item: + _items.append(_item.to_dict()) + _dict["keys"] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UserFactorSignedNonceProfile from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate( + { + "credentialId": obj.get("credentialId"), + "deviceType": obj.get("deviceType"), + "name": obj.get("name"), + "platform": obj.get("platform"), + "version": obj.get("version"), + "keys": ( + [ + UserFactorSignedNonceProfileKey.from_dict(_item) + for _item in obj["keys"] + ] + if obj.get("keys") is not None + else None + ), + } + ) + return _obj diff --git a/okta/models/user_factor_signed_nonce_profile_key.py b/okta/models/user_factor_signed_nonce_profile_key.py new file mode 100644 index 00000000..99c4c7e9 --- /dev/null +++ b/okta/models/user_factor_signed_nonce_profile_key.py @@ -0,0 +1,163 @@ +# The Okta software accompanied by this notice is provided pursuant to the following terms: +# Copyright © 2025-Present, Okta, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +# License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS +# IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and limitations under the License. +# coding: utf-8 + +""" +Okta Admin Management + +Allows customers to easily access the Okta Management APIs + +The version of the OpenAPI document: 5.1.0 +Contact: devex-public@okta.com +Generated by OpenAPI Generator (https://openapi-generator.tech) + +Do not edit the class manually. +""" # noqa: E501 + +from __future__ import annotations + +import json +import pprint +import re # noqa: F401 +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing_extensions import Self + + +class UserFactorSignedNonceProfileKey(BaseModel): + """ + JSON Web Key (JWK) for signed nonce verification + """ # noqa: E501 + + kty: Optional[StrictStr] = Field(default=None, description="Key type") + use: Optional[StrictStr] = Field(default=None, description="Key usage") + kid: Optional[StrictStr] = Field(default=None, description="Key ID") + jwk_type: Optional[StrictStr] = Field( + default=None, description="Purpose of the key", alias="jwkType" + ) + e: Optional[StrictStr] = Field( + default=None, description="RSA public exponent (present only for RSA keys)" + ) + n: Optional[StrictStr] = Field( + default=None, description="RSA modulus (present only for RSA keys)" + ) + crv: Optional[StrictStr] = Field( + default=None, description="EC curve name (present only for EC keys)" + ) + x: Optional[StrictStr] = Field( + default=None, description="EC x-coordinate (present only for EC keys)" + ) + y: Optional[StrictStr] = Field( + default=None, description="EC y-coordinate (present only for EC keys)" + ) + x5c: Optional[List[StrictStr]] = Field( + default=None, description="X.509 certificate chain" + ) + __properties: ClassVar[List[str]] = [ + "kty", + "use", + "kid", + "jwkType", + "e", + "n", + "crv", + "x", + "y", + "x5c", + ] + + @field_validator("kty") + def kty_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(["RSA", "EC"]): + raise ValueError("must be one of enum values ('RSA', 'EC')") + return value + + @field_validator("jwk_type") + def jwk_type_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set( + ["proofOfPossession", "userVerification", "userVerificationBioOrPin"] + ): + raise ValueError( + "must be one of enum values ('proofOfPossession', 'userVerification', 'userVerificationBioOrPin')" + ) + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of UserFactorSignedNonceProfileKey from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of UserFactorSignedNonceProfileKey from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate( + { + "kty": obj.get("kty"), + "use": obj.get("use"), + "kid": obj.get("kid"), + "jwkType": obj.get("jwkType"), + "e": obj.get("e"), + "n": obj.get("n"), + "crv": obj.get("crv"), + "x": obj.get("x"), + "y": obj.get("y"), + "x5c": obj.get("x5c"), + } + ) + return _obj diff --git a/openapi/api.yaml b/openapi/api.yaml index d07ef21e..e2a95e0b 100644 --- a/openapi/api.yaml +++ b/openapi/api.yaml @@ -81003,6 +81003,7 @@ components: email: '#/components/schemas/UserFactorEmail' push: '#/components/schemas/UserFactorPush' question: '#/components/schemas/UserFactorSecurityQuestion' + signed_nonce: '#/components/schemas/UserFactorSignedNonce' sms: '#/components/schemas/UserFactorSMS' token: '#/components/schemas/UserFactorToken' token:hardware: '#/components/schemas/UserFactorTokenHardware' @@ -81771,6 +81772,99 @@ components: description: ID for the factor credential example: dade.murphy@example.com type: string + UserFactorSignedNonce: + title: signed_nonce + description: |- + `signed_nonce` is the factor type for [Okta FastPass](https://help.okta.com/oie/en-us/content/topics/identity-engine/devices/fp/fp-main.htm). You can't use the Factors API to enroll or activate Okta FastPass (`signed_nonce`) for a user. Use the [Okta Verify](https://help.okta.com/en-us/content/topics/mobile/okta-verify-overview.htm) authenticator enrollment flow instead. + + You can use the Factors API to list and delete `signed_nonce` factors. + allOf: + - $ref: '#/components/schemas/UserFactor' + - type: object + properties: + factorType: + example: signed_nonce + profile: + $ref: '#/components/schemas/UserFactorSignedNonceProfile' + provider: + enum: + - OKTA + UserFactorSignedNonceProfile: + description: Profile for the Okta FastPass (signed nonce) factor + type: object + readOnly: true + properties: + credentialId: + description: ID for the factor credential + example: dade.murphy@example.com + type: string + deviceType: + description: Type of device + example: SmartPhone_IPhone + type: string + name: + description: Name of the device + example: My Phone + type: string + platform: + description: OS platform of the associated device + example: IOS + type: string + version: + description: OS version of the associated device + example: '17.2' + type: string + keys: + description: Cryptographic keys associated with the signed nonce factor + type: array + items: + $ref: '#/components/schemas/UserFactorSignedNonceProfileKey' + UserFactorSignedNonceProfileKey: + description: JSON Web Key (JWK) for signed nonce verification + type: object + properties: + kty: + description: Key type + type: string + enum: + - RSA + - EC + use: + description: Key usage + type: string + example: sig + kid: + description: Key ID + type: string + example: default + jwkType: + description: Purpose of the key + type: string + enum: + - proofOfPossession + - userVerification + - userVerificationBioOrPin + e: + description: RSA public exponent (present only for RSA keys) + type: string + n: + description: RSA modulus (present only for RSA keys) + type: string + crv: + description: EC curve name (present only for EC keys) + type: string + example: P-256 + x: + description: EC x-coordinate (present only for EC keys) + type: string + y: + description: EC y-coordinate (present only for EC keys) + type: string + x5c: + description: X.509 certificate chain + type: array + items: + type: string UserFactorYubikeyOtpToken: type: object properties: From 4496394e951a00082ed24a87150afba1b9c5657c Mon Sep 17 00:00:00 2001 From: BinoyOza-okta Date: Tue, 9 Jun 2026 13:51:18 +0530 Subject: [PATCH 2/2] - Added integration tests for the changes implemented. --- ...source.test_list_factors_signed_nonce.yaml | 150 ++++++++++++++++++ tests/integration/test_factors_it.py | 63 ++++++++ 2 files changed, 213 insertions(+) create mode 100644 tests/integration/cassettes/test_factors_it/TestFactorsResource.test_list_factors_signed_nonce.yaml diff --git a/tests/integration/cassettes/test_factors_it/TestFactorsResource.test_list_factors_signed_nonce.yaml b/tests/integration/cassettes/test_factors_it/TestFactorsResource.test_list_factors_signed_nonce.yaml new file mode 100644 index 00000000..72247945 --- /dev/null +++ b/tests/integration/cassettes/test_factors_it/TestFactorsResource.test_list_factors_signed_nonce.yaml @@ -0,0 +1,150 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Authorization: + - SSWS myAPIToken + User-Agent: + - OpenAPI-Generator/1.0.0/python + method: GET + uri: https://test.okta.com/api/v1/users + response: + body: + string: '[{"id":"00unwlw0tbo8E6aVj5d7","status":"ACTIVE","created":"2025-03-19T08:01:48.000Z","activated":null,"statusChanged":"2025-03-19T09:20:48.000Z","lastLogin":"2026-06-04T06:38:19.000Z","lastUpdated":"2025-03-19T09:20:48.000Z","passwordChanged":"2025-03-19T09:20:48.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Binoy","lastName":"Oza","mobilePhone":null,"secondEmail":null,"login":"binoy.oza@okta.com","email":"binoy.oza@okta.com"},"credentials":{"password":{},"emails":[{"value":"binoy.oza@okta.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7"}}},{"id":"00upynhrehMaqXPgU5d7","status":"ACTIVE","created":"2025-08-07T13:11:36.000Z","activated":"2025-08-07T13:11:36.000Z","statusChanged":"2025-08-07T13:11:36.000Z","lastLogin":null,"lastUpdated":"2025-08-07T13:11:36.000Z","passwordChanged":"2025-08-07T13:11:36.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"John","lastName":"Doe-Assign-User-Role","mobilePhone":null,"secondEmail":null,"login":"John.Doe-Assign-User-Role@example.com","email":"John.Doe-Assign-User-Role@example.com"},"credentials":{"password":{},"emails":[{"value":"John.Doe-Assign-User-Role@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00upynhrehMaqXPgU5d7"}}},{"id":"00urimiriwkS090165d7","status":"ACTIVE","created":"2025-11-17T11:41:28.000Z","activated":"2025-11-17T11:41:28.000Z","statusChanged":"2025-11-20T07:32:46.000Z","lastLogin":"2025-11-25T10:47:16.000Z","lastUpdated":"2025-11-20T07:32:46.000Z","passwordChanged":"2025-11-20T07:32:46.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"bob","lastName":"tables","mobilePhone":null,"secondEmail":null,"login":"bob@tables.fake","email":"bob@tables.fake"},"credentials":{"password":{},"emails":[{"value":"bob@tables.fake","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00urimiriwkS090165d7"}}},{"id":"00uron9tssJTT01de5d7","status":"ACTIVE","created":"2025-11-27T19:26:19.000Z","activated":"2025-11-28T04:17:25.000Z","statusChanged":"2025-11-28T04:20:27.000Z","lastLogin":"2025-12-08T14:05:06.000Z","lastUpdated":"2025-11-28T04:20:27.000Z","passwordChanged":"2025-11-28T04:20:27.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Aniket","lastName":"Anil + Kumar","mobilePhone":null,"secondEmail":null,"login":"aniket@okta.com","email":"aniket@okta.com"},"credentials":{"password":{},"emails":[{"value":"aniket@okta.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uron9tssJTT01de5d7"}}},{"id":"00usr26thxUMNcwqC5d7","status":"ACTIVE","created":"2026-01-31T16:40:03.000Z","activated":"2026-01-31T16:40:04.000Z","statusChanged":"2026-01-31T16:41:07.000Z","lastLogin":"2026-01-31T16:41:07.000Z","lastUpdated":"2026-01-31T16:41:07.000Z","passwordChanged":"2026-01-31T16:41:07.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Test","lastName":"Client + sdk","mobilePhone":null,"secondEmail":null,"login":"test@clientsdk.com","email":"test@clientsdk.com"},"credentials":{"password":{},"emails":[{"value":"test@clientsdk.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00usr26thxUMNcwqC5d7"}}},{"id":"00utrpra8lYbNAGHR5d7","status":"ACTIVE","created":"2026-03-25T06:44:24.000Z","activated":"2026-03-25T06:44:25.000Z","statusChanged":"2026-03-25T06:46:57.000Z","lastLogin":"2026-03-25T06:54:46.000Z","lastUpdated":"2026-03-25T06:46:57.000Z","passwordChanged":"2026-03-25T06:46:57.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Prachi","lastName":"Pandey","mobilePhone":null,"secondEmail":null,"login":"prachi.pandey@okta.com","email":"prachi.pandey@okta.com"},"credentials":{"password":{},"emails":[{"value":"prachi.pandey@okta.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00utrpra8lYbNAGHR5d7"}}},{"id":"00uuyg8e0tP0FcSQo5d7","status":"ACTIVE","created":"2026-06-02T10:38:43.000Z","activated":"2026-06-02T10:38:44.000Z","statusChanged":"2026-06-02T10:38:44.000Z","lastLogin":"2026-06-02T10:42:40.000Z","lastUpdated":"2026-06-02T10:38:44.000Z","passwordChanged":"2026-06-02T10:38:44.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Sagar","lastName":"V","mobilePhone":null,"secondEmail":null,"login":"sagar.viswanatha@okta.com","email":"sagar.viswanatha@okta.com"},"credentials":{"password":{},"emails":[{"value":"sagar.viswanatha@okta.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uuyg8e0tP0FcSQo5d7"}}},{"id":"00uowwcs8cTcBPGMc5d7","status":"PASSWORD_EXPIRED","created":"2025-05-27T08:13:06.000Z","activated":"2025-05-27T08:13:06.000Z","statusChanged":"2025-05-27T08:13:06.000Z","lastLogin":null,"lastUpdated":"2025-05-27T08:13:06.000Z","passwordChanged":"2025-05-27T08:13:06.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Isaac","lastName":"Brock","mobilePhone":"555-415-1337","secondEmail":null,"login":"isaac.brock@example.com","email":"isaac.brock@example.com"},"credentials":{"password":{},"emails":[{"value":"isaac.brock@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uowwcs8cTcBPGMc5d7"}}},{"id":"00uowxt3ozKl39Oex5d7","status":"PASSWORD_EXPIRED","created":"2025-05-27T09:22:32.000Z","activated":"2025-05-27T09:22:32.000Z","statusChanged":"2025-05-27T09:22:32.000Z","lastLogin":null,"lastUpdated":"2025-05-27T09:22:32.000Z","passwordChanged":"2025-05-27T09:22:32.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Sample","lastName":"Sample","mobilePhone":"555-415-1337","secondEmail":null,"login":"sample.sample@example.com","email":"sample.sample@example.com"},"credentials":{"password":{},"emails":[{"value":"sample.sample@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uowxt3ozKl39Oex5d7"}}},{"id":"00uowylh4548byr6w5d7","status":"PASSWORD_EXPIRED","created":"2025-05-27T10:16:29.000Z","activated":"2025-05-27T10:16:30.000Z","statusChanged":"2025-05-27T10:16:30.000Z","lastLogin":null,"lastUpdated":"2025-05-27T10:16:30.000Z","passwordChanged":"2025-05-27T10:16:30.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Sample1","lastName":"Sample1","mobilePhone":"555-415-1337","secondEmail":null,"login":"sample1.sample1@example.com","email":"sample1.sample1@example.com"},"credentials":{"password":{},"emails":[{"value":"sample1.sample1@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uowylh4548byr6w5d7"}}},{"id":"00uozv8kk7SGP9IKF5d7","status":"PASSWORD_EXPIRED","created":"2025-06-02T04:36:18.000Z","activated":"2025-06-02T04:36:18.000Z","statusChanged":"2025-06-02T04:36:18.000Z","lastLogin":null,"lastUpdated":"2025-06-02T04:36:18.000Z","passwordChanged":"2025-06-02T04:36:18.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Sample12","lastName":"Sample12","mobilePhone":"555-415-1337","secondEmail":null,"login":"sample12.sample12@example.com","email":"sample12.sample12@example.com"},"credentials":{"password":{},"emails":[{"value":"sample12.sample12@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uozv8kk7SGP9IKF5d7"}}},{"id":"00uqh31ktvIde3jcq5d7","status":"PASSWORD_EXPIRED","created":"2025-09-09T13:35:11.000Z","activated":"2025-09-09T13:35:11.000Z","statusChanged":"2025-09-09T13:35:11.000Z","lastLogin":null,"lastUpdated":"2025-09-09T13:35:11.000Z","passwordChanged":"2025-09-09T13:35:11.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Sample121","lastName":"Sample121","mobilePhone":"555-415-1337","secondEmail":null,"login":"sample12.sample121@example.com","email":"sample12.sample121@example.com"},"credentials":{"password":{},"emails":[{"value":"sample12.sample121@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uqh31ktvIde3jcq5d7"}}},{"id":"00ushjf1hmpOzKEqR5d7","status":"PASSWORD_EXPIRED","created":"2026-01-16T15:30:07.000Z","activated":"2026-01-16T15:30:08.000Z","statusChanged":"2026-01-16T15:30:08.000Z","lastLogin":null,"lastUpdated":"2026-01-16T15:30:08.000Z","passwordChanged":"2026-01-16T15:30:07.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Manmohan","lastName":"Shaw","mobilePhone":null,"secondEmail":null,"login":"manmohan.shaw@okta.com","email":"manmohan.shaw@okta.com"},"credentials":{"password":{},"emails":[{"value":"manmohan.shaw@okta.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00ushjf1hmpOzKEqR5d7"}}},{"id":"00utbu9afurlD9N3o5d7","status":"PASSWORD_EXPIRED","created":"2026-02-26T19:20:45.000Z","activated":"2026-02-26T19:20:45.000Z","statusChanged":"2026-02-26T19:20:45.000Z","lastLogin":null,"lastUpdated":"2026-02-26T19:20:45.000Z","passwordChanged":"2026-02-26T19:20:45.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"TestUserdceaae85","lastName":"LastNamedceaae85","mobilePhone":"555-415-1337","secondEmail":null,"login":"testuserdceaae85@example.com","email":"testuserdceaae85@example.com"},"credentials":{"password":{},"emails":[{"value":"testuserdceaae85@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00utbu9afurlD9N3o5d7"}}},{"id":"00utwqn7x6aISME9M5d7","status":"PASSWORD_EXPIRED","created":"2026-04-02T07:40:49.000Z","activated":"2026-04-02T07:40:49.000Z","statusChanged":"2026-04-02T07:40:49.000Z","lastLogin":null,"lastUpdated":"2026-04-02T07:40:49.000Z","passwordChanged":"2026-04-02T07:40:49.000Z","type":{"id":"otynwlw0pkLLUgtNG5d7"},"profile":{"firstName":"Isaac121","lastName":"Brock121","mobilePhone":"555-415-1337","secondEmail":"","login":"isaac.brock121@example.com","email":"isaac.brock121@example.com"},"credentials":{"password":{},"emails":[{"value":"isaac.brock121@example.com","status":"VERIFIED","type":"PRIMARY"},{"value":"","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00utwqn7x6aISME9M5d7"}}}]' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 09 Jun 2026 06:04:03 GMT + Expires: + - '0' + Link: + - ; rel="self" + Pragma: + - no-cache + Server: + - nginx + Set-Cookie: + - sid="";Version=1;Path=/;Max-Age=0 + - xids="";Version=1;Path=/;Max-Age=0 + - autolaunch_triggered=""; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/ + - activate_ca_modal_triggered=""; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/ + - JSESSIONID=1792FD6002309367151DC18D2370A461; Path=/; Secure; HttpOnly + Strict-Transport-Security: + - max-age=315360000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + accept-ch: + - Sec-CH-UA-Platform-Version + content-security-policy: frame-ancestors 'self' + p3p: + - CP="HONK" + referrer-policy: + - strict-origin-when-cross-origin + x-content-type-options: + - nosniff + x-okta-request-id: + - 59d84181d42748b70ee6e18811b24a7f + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '49' + x-rate-limit-reset: + - '1780985103' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Authorization: + - SSWS myAPIToken + User-Agent: + - OpenAPI-Generator/1.0.0/python + method: GET + uri: https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7/factors + response: + body: + string: '[{"id":"crpnwmp5pcpELVg0Y5d7","factorType":"signed_nonce","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2025-03-19T09:21:27.000Z","lastUpdated":"2025-03-19T09:21:27.000Z","profile":{"credentialId":"binoy.oza@okta.com","deviceType":"SmartPhone_IPhone","name":"CVG3GF7X5P","platform":"IOS","version":"18.3.2","keys":[{"kty":"EC","use":"sig","kid":"default","jwkType":"proofOfPossession","x":"0M18P_3jzp58KAYDy41cB1N6JwhkDnXxaqbBVscZxVA","y":"4AeD4KOHGhN1P0uSyO170-tU0ZIuC_K2eC72olTPuZw","crv":"P-256"},{"kty":"EC","use":"sig","kid":"default","jwkType":"userVerification","x":"R0iBIuMUR6wA4JKShqIke4FxcapHHnKE1WPkNT_8P2I","y":"qjEXEEU99DixvuLEF9q3rQYYT4LRYQ022q6GTW4Ye-E","crv":"P-256"},{"kty":"EC","use":"sig","kid":"default","jwkType":"userVerificationBioOrPin","x":"dyjq_GhQu-drEwxp3bq4X_vdpEheUKCz37TO13BC5CU","y":"8x6G_DIXtWoNfblyhDLpWzP5a6FF7zVaejcXDLsQaqw","crv":"P-256"}]},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7/factors/crpnwmp5pcpELVg0Y5d7","hints":{"allow":["GET","DELETE"]}},"user":{"href":"https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7","hints":{"allow":["GET"]}}}},{"id":"ostnwmp5pabaPon1M5d7","factorType":"token:software:totp","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2025-03-19T09:21:27.000Z","lastUpdated":"2025-03-19T09:21:27.000Z","profile":{"credentialId":"binoy.oza@okta.com"},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7/factors/ostnwmp5pabaPon1M5d7","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7/factors/ostnwmp5pabaPon1M5d7/verify","hints":{"allow":["POST"]}},"user":{"href":"https://test.okta.com/api/v1/users/00unwlw0tbo8E6aVj5d7","hints":{"allow":["GET"]}}}}]' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 09 Jun 2026 06:04:04 GMT + Expires: + - '0' + Pragma: + - no-cache + Server: + - nginx + Set-Cookie: + - sid="";Version=1;Path=/;Max-Age=0 + - xids="";Version=1;Path=/;Max-Age=0 + - autolaunch_triggered=""; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/ + - activate_ca_modal_triggered=""; Expires=Thu, 01 Jan 1970 00:00:10 GMT; Path=/ + - JSESSIONID=E41E7C3B95A85FE876768F5E9A824183; Path=/; Secure; HttpOnly + Strict-Transport-Security: + - max-age=315360000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + accept-ch: + - Sec-CH-UA-Platform-Version + content-security-policy: 'default-src ''self'' test.okta.com *.oktacdn.com; + connect-src ''self'' test.okta.com test-admin.okta.com *.oktacdn.com *.mixpanel.com + *.mapbox.com test.kerberos.okta.com *.authenticatorlocalprod.com:8769 http://localhost:8769 + http://127.0.0.1:8769 *.authenticatorlocalprod.com:65111 http://localhost:65111 + http://127.0.0.1:65111 *.authenticatorlocalprod.com:65121 http://localhost:65121 + http://127.0.0.1:65121 *.authenticatorlocalprod.com:65131 http://localhost:65131 + http://127.0.0.1:65131 *.authenticatorlocalprod.com:65141 http://localhost:65141 + http://127.0.0.1:65141 *.authenticatorlocalprod.com:65151 http://localhost:65151 + http://127.0.0.1:65151 https://oinmanager.okta.com data: *.ingest.sentry.io; + script-src ''unsafe-inline'' ''self'' ''report-sample'' test.okta.com *.oktacdn.com; + style-src ''unsafe-inline'' ''self'' ''report-sample'' test.okta.com *.oktacdn.com; + frame-src ''self'' test.okta.com test-admin.okta.com login.okta.com *.vidyard.com + com-okta-authenticator:; img-src ''self'' test.okta.com *.oktacdn.com *.tiles.mapbox.com + *.mapbox.com *.vidyard.com data: blob:; font-src ''self'' test.okta.com data: + *.oktacdn.com fonts.gstatic.com; frame-ancestors ''self''' + p3p: + - CP="HONK" + referrer-policy: + - strict-origin-when-cross-origin + x-content-type-options: + - nosniff + x-okta-request-id: + - 38faa7a94db82b5870dc55895684d60d + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '48' + x-rate-limit-reset: + - '1780985103' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/integration/test_factors_it.py b/tests/integration/test_factors_it.py index 8b893a9a..5fac2a53 100644 --- a/tests/integration/test_factors_it.py +++ b/tests/integration/test_factors_it.py @@ -219,6 +219,69 @@ async def test_list_factors_new_user(self, fs): errors.append(exc) assert len(errors) == 0 + @pytest.mark.vcr() + @pytest.mark.asyncio + async def test_list_factors_signed_nonce(self, fs): + """ + Regression test for OKTA-1152856. + + Verifies that `list_factors()` correctly deserializes the Okta + FastPass `signed_nonce` factor type into a `UserFactorSignedNonce` + instance (with a populated `UserFactorSignedNonceProfile`) instead + of raising a Pydantic discriminator ValidationError. + + Note: `signed_nonce` cannot be enrolled via the Factors API (it is + provisioned through the Okta Verify authenticator enrollment + flow). This test therefore relies on a pre-existing FastPass- + enrolled user in the test org; it discovers that user by + iterating `list_users()` and inspecting each user's factors. + """ + # Instantiate Mock Client + client = MockOktaClient(fs) + + users, _, err = await client.list_users() + assert err is None + assert users is not None and len(users) > 0 + + signed_nonce_user = None + signed_nonce_factor = None + for user in users: + factors, _, factors_err = await client.list_factors(user.id) + assert factors_err is None + if not factors: + continue + for factor in factors: + if isinstance(factor, models.UserFactorSignedNonce): + signed_nonce_user = user + signed_nonce_factor = factor + break + if signed_nonce_factor is not None: + break + + assert signed_nonce_user is not None, ( + "No user enrolled with the signed_nonce (Okta FastPass) factor was " + "found in the test org. Enroll Okta Verify for a test user before " + "re-recording this cassette." + ) + + # Discriminator routed to the correct subclass + assert isinstance(signed_nonce_factor, models.UserFactorSignedNonce) + assert signed_nonce_factor.factor_type == models.UserFactorType.SIGNED_NONCE + assert signed_nonce_factor.provider == "OKTA" + assert signed_nonce_factor.id is not None + + # Nested profile deserialized into the new generated model + assert signed_nonce_factor.profile is not None + assert isinstance( + signed_nonce_factor.profile, models.UserFactorSignedNonceProfile + ) + + # If the profile carries JWK key material, ensure each key entry + # is also routed to the dedicated profile-key model. + if signed_nonce_factor.profile.keys: + for key in signed_nonce_factor.profile.keys: + assert isinstance(key, models.UserFactorSignedNonceProfileKey) + @pytest.mark.skip(reason="SDK bug: user_factor_links.py from_dict expects dict but API returns list for resend field") @pytest.mark.vcr() @pytest.mark.asyncio