@@ -129,7 +129,7 @@ recovery read the [account recovery documentation](../self-service/flows/account
129129
130130## Importing credentials
131131
132- Ory supports importing credentials for identities including passwords and social sign-in connections.
132+ Ory supports importing credentials for identities including passwords, social sign-in connections, TOTP, WebAuthn, and passkeys .
133133
134134### Clear text password
135135
@@ -787,6 +787,138 @@ must be the ID of the user on the given platform. The subject should not be an e
787787}
788788```
789789
790+ ### TOTP credentials
791+
792+ To import an identity with an existing TOTP (time-based one-time password) credential, provide the TOTP secret as an `otpauth://`
793+ URL following the [Google Authenticator Key URI format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format).
794+
795+ This allows users to continue using their existing authenticator app without re-enrollment.
796+
797+ ```json {6-12}
798+ {
799+ "schema_id": "preset://email",
800+ "traits": {
801+ "email": "docs-totp@example.org"
802+ },
803+ "credentials": {
804+ "totp": {
805+ "config": {
806+ "totp_url": "otpauth://totp/MyApp:docs-totp@example.org?secret=JBSWY3DPEHPK3PXP&issuer=MyApp"
807+ }
808+ }
809+ }
810+ }
811+ ```
812+
813+ The `totp_url` must be a valid `otpauth://` URL. The URL contains the shared secret, issuer, and account name:
814+
815+ ```
816+ otpauth://totp/{Issuer}:{AccountName}?secret={SECRET}&issuer={Issuer}
817+ ```
818+
819+ - `secret` (required): Base32-encoded shared secret.
820+ - `issuer` (required): Service or company name.
821+ - `digits` (optional): Number of digits, default is 6.
822+ - `period` (optional): Time step in seconds, default is 30.
823+ - `algorithm` (optional): Hash algorithm (`SHA1`, `SHA256`, `SHA512`), default is `SHA1`.
824+
825+ Each identity supports one TOTP credential. Importing a new TOTP credential replaces the previous one.
826+
827+ ### Passkey credentials
828+
829+ To import an identity with existing passkey credentials, provide the WebAuthn credential data including the credential ID, public
830+ key, and authenticator metadata. All binary fields must be base64-encoded.
831+
832+ ```json {6-24}
833+ {
834+ "schema_id": "preset://email",
835+ "traits": {
836+ "email": "docs-passkey@example.org"
837+ },
838+ "credentials": {
839+ "passkey": {
840+ "config": {
841+ "user_handle": "dXNlci1oYW5kbGUtMQ==",
842+ "credentials": [
843+ {
844+ "id": "Y3JlZGVudGlhbC1pZC0x",
845+ "public_key": "cHVibGljLWtleS0x",
846+ "attestation_type": "none",
847+ "display_name": "Work Laptop",
848+ "is_passwordless": true,
849+ "authenticator": {
850+ "aaguid": "YWFndWlkLTE=",
851+ "sign_count": 0,
852+ "clone_warning": false
853+ }
854+ }
855+ ]
856+ }
857+ }
858+ }
859+ }
860+ ```
861+
862+ Each credential requires the following fields:
863+
864+ - `id` (base64, required): The credential ID assigned during registration.
865+ - `public_key` (base64, required): The COSE-encoded public key from the authenticator.
866+ - `attestation_type` (string, required): The attestation type. Use `"none"` if the original attestation is not available.
867+ - `authenticator.aaguid` (base64, required): The Authenticator Attestation GUID identifying the authenticator model.
868+ - `authenticator.sign_count` (integer, required): The signature counter from the last authentication.
869+ - `authenticator.clone_warning` (boolean, required): Set to `true` if the credential may have been cloned.
870+ - `user_handle` (base64, required on create): The WebAuthn user handle. Must be unique per identity.
871+
872+ Optional fields include `display_name`, `is_passwordless`, `added_at`, `flags` (`user_present`, `user_verified`,
873+ `backup_eligible`, `backup_state`), and `transport` (`"usb"`, `"nfc"`, `"ble"`, `"internal"`).
874+
875+ An identity can have multiple passkey credentials. When updating an identity, existing credentials with the same `id` are updated
876+ in place and new credentials are appended.
877+
878+ :::warning
879+
880+ Passkeys are bound to a relying party (RP) ID, typically your domain. If the RP ID changes during migration, existing passkeys
881+ will not work. Make sure the RP ID in your Ory configuration matches the one used when the passkeys were originally registered.
882+
883+ :::
884+
885+ ### WebAuthn credentials
886+
887+ WebAuthn credentials used for second-factor authentication (FIDO2 security keys) use the same format as passkeys. Use the
888+ `webauthn` key instead of `passkey`:
889+
890+ ```json {6-24}
891+ {
892+ "schema_id": "preset://email",
893+ "traits": {
894+ "email": "docs-webauthn@example.org"
895+ },
896+ "credentials": {
897+ "webauthn": {
898+ "config": {
899+ "user_handle": "dXNlci1oYW5kbGUtMQ==",
900+ "credentials": [
901+ {
902+ "id": "Y3JlZGVudGlhbC1pZC0x",
903+ "public_key": "cHVibGljLWtleS0x",
904+ "attestation_type": "none",
905+ "display_name": "YubiKey",
906+ "is_passwordless": false,
907+ "authenticator": {
908+ "aaguid": "YWFndWlkLTE=",
909+ "sign_count": 10,
910+ "clone_warning": false
911+ }
912+ }
913+ ]
914+ }
915+ }
916+ }
917+ }
918+ ```
919+
920+ Set `is_passwordless` to `false` for second-factor WebAuthn credentials and `true` for passkeys.
921+
790922## Organization-specific SAML and OIDC connections
791923
792924When importing SAML or OIDC connections that are only available for certain [organizations](../organizations/organizations.mdx)
0 commit comments