diff --git a/docs/UserFactorSignedNonce.md b/docs/UserFactorSignedNonce.md new file mode 100644 index 000000000..4e3db39ec --- /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 000000000..1ebfa3887 --- /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 000000000..bade26611 --- /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 aeddde3af..f211e4961 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 e15c879f1..c40a589d5 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 03ba4997c..bc023f3aa 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 000000000..f07d36baa --- /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 000000000..8d0cc2391 --- /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 000000000..99c4c7e96 --- /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 d07ef21ed..e2a95e0bc 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: 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 000000000..fd72b8015 --- /dev/null +++ b/tests/integration/cassettes/test_factors_it/TestFactorsResource.test_list_factors_signed_nonce.yaml @@ -0,0 +1,100 @@ +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":"00uFAKEFASTPASS000001","status":"ACTIVE","created":"2025-01-01T00:00:00.000Z","activated":null,"statusChanged":"2025-01-01T00:00:00.000Z","lastLogin":"2025-01-01T00:00:00.000Z","lastUpdated":"2025-01-01T00:00:00.000Z","passwordChanged":"2025-01-01T00:00:00.000Z","type":{"id":"otyFAKEUSERTYPE000001"},"profile":{"firstName":"Jane","lastName":"Doe-Signed-Nonce","mobilePhone":null,"secondEmail":null,"login":"jane.doe-signed-nonce@example.com","email":"jane.doe-signed-nonce@example.com"},"credentials":{"password":{},"emails":[{"value":"jane.doe-signed-nonce@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uFAKEFASTPASS000001"}}}]' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Tue, 09 Jun 2026 06:04:03 GMT + Expires: + - '0' + Link: + - ; rel="self" + Pragma: + - no-cache + Server: + - nginx + Strict-Transport-Security: + - max-age=315360000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + x-content-type-options: + - nosniff + 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/00uFAKEFASTPASS000001/factors + response: + body: + string: '[{"id":"crpFAKESIGNEDNONCE0001","factorType":"signed_nonce","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2025-01-01T00:00:00.000Z","lastUpdated":"2025-01-01T00:00:00.000Z","profile":{"credentialId":"jane.doe-signed-nonce@example.com","deviceType":"SmartPhone_IPhone","name":"FAKEDEVICE01","platform":"IOS","version":"18.0.0","keys":[{"kty":"EC","use":"sig","kid":"default","jwkType":"proofOfPossession","x":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","y":"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","crv":"P-256"},{"kty":"EC","use":"sig","kid":"default","jwkType":"userVerification","x":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","y":"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD","crv":"P-256"},{"kty":"EC","use":"sig","kid":"default","jwkType":"userVerificationBioOrPin","x":"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE","y":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF","crv":"P-256"}]},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uFAKEFASTPASS000001/factors/crpFAKESIGNEDNONCE0001","hints":{"allow":["GET","DELETE"]}},"user":{"href":"https://test.okta.com/api/v1/users/00uFAKEFASTPASS000001","hints":{"allow":["GET"]}}}},{"id":"ostFAKETOTP000000001","factorType":"token:software:totp","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2025-01-01T00:00:00.000Z","lastUpdated":"2025-01-01T00:00:00.000Z","profile":{"credentialId":"jane.doe-signed-nonce@example.com"},"_links":{"self":{"href":"https://test.okta.com/api/v1/users/00uFAKEFASTPASS000001/factors/ostFAKETOTP000000001","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://test.okta.com/api/v1/users/00uFAKEFASTPASS000001/factors/ostFAKETOTP000000001/verify","hints":{"allow":["POST"]}},"user":{"href":"https://test.okta.com/api/v1/users/00uFAKEFASTPASS000001","hints":{"allow":["GET"]}}}}]' + headers: + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Tue, 09 Jun 2026 06:04:04 GMT + Expires: + - '0' + Pragma: + - no-cache + Server: + - nginx + Strict-Transport-Security: + - max-age=315360000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + x-content-type-options: + - nosniff + 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 8b893a9a0..5fac2a53c 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