This Verify feature is currently in Pilot, which means that we're actively looking for early adopters to try it out and give feedback. Contact Twilio Sales to try it out for free!
Passkeys, also known as FIDO/WebAuthn, is an industry-standard authentication method that is more seamless and secure than passwords. Many consumer apps are adding support for Passkeys, including Google making Passkeys its default sign-in option.
Verify enables developers to easily add Passkeys into their existing authentication flows. The Verify API supports passkey registration, public key storage, and auth flows.
Verify Passkeys also offers client-side supported SDKs for iOS and Android that helps you verify users by adding a low-friction, secure, cost-effective, device approval factor into your own mobile application. Get early access to the SDKs.
Twilio is a member of the FIDO alliance that created the Passkeys standard.
The system follows a client-server architecture where the client generates or receives Passkeys and communicates with the server for verification.
Verify Passkeys involves two main sequences that are shown in the diagrams below:
Passkeys creation and registration: Register (sign up) a user by creating a unique passkey.
Passkeys Authentication: Verify (Sign In) the user by authenticating the registered passkey.
This quick tutorial will cover how to register and authenticate with Passkeys. There are two main components for working with Passkeys:
Browser (Client) APIs. These are standardized and available in modern browsers.
Server APIs. Twilio Verify is providing the server side passkey storage and validation.
The steps to register a passkey are:
Initiate the process of creating a new Passkey registration.
1POST https:https://comms.twilio.com/preview/Factors2
Name | Type | Description | |
---|---|---|---|
friendly_name | String | A human-readable description of the Factor. Maximum length is 255 characters. (🏢 not PII ) | |
to | Object | The end user that is enrolling the factor. (🏢 PII ) | |
to.user_identifier | String | A custom string that uniquely identifies a person or end-user. For example, this can be the end-user's username or a UUID4 string or an SHA256 hash. (🏢 PII ) | |
content | Object | The configuration options of the Factor according to the Factor type. | |
content.user.display_name (required) | String | A human-palatable name for the user account, intended only for display. | |
content.relying_party.id (required) | String | The relying party identifier. This should generally be the origin without a scheme and port. | |
content.relying_party.name | String | The relying party name that the authenticator will show during the registration/authentication process. | |
content.relying_party.origins | Array[String] | List of Relying Party Server Origins or App IDs that are permitted. | |
content.authenticator_criteria.authenticator_attachment | String | Default: "any" Enum: "platform" "cross-platform" "any" A flag indicating a requirement to attach only to a certain type of authenticator. | |
content.authenticator_criteria.discoverable_credentials | String | Default: "preferred" Enum: "required" "preferred" "discouraged" A flag indicating the level of preference for discoverable credentials. | |
content.authenticator_criteria.user_verification | String | Default: "preferred" Enum: "required" "preferred" "discouraged" Whether user identity verification (via biometrics or PIN) is required. |
1curl --location 'https://comms.twilio.com/preview/Factors' \2--header 'Content-Type: application/json' \3--header 'Authorization: Basic ***' \4--data '{5"friendly_name": "Twilio Passkeys Quickstart",6"to": {7"user_identifier": "passkeyuser001"8},9"content": {10"relying_party": {11"id": "twilio.com",12"name": "Twilio Passkeys Quickstart",13"origins": ["http://twilio.com", "https://twilio.com", "https://www.twilio.com"]14},15"authenticator_criteria": {16"authenticator_attachment": "platform",17"discoverable_credentials": "preferred",18"user_verification": "preferred"19},20"user": {21"display_name": "User 001"22}23}24}
1{2"contact_id": "comms_contact_751yj9086a8ddjer531ancqdnr",3"content": {4"authenticator_criteria": {5"authenticator_attachment": "platform",6"discoverable_credentials": "preferred",7"user_verification": "preferred"8},9"credential": {10"authenticator_metadata": null,11"credential_id": null,12"credential_public_key": null,13"flags": [],14"transports": []15},16"relying_party": {17"id": "acme.com",18"name": "ACME",19"origins": ["https://acme.com"]20}21},22"created_at": "2024-05-24T09:23:20.385660582Z",23"deleted_at": null,24"friendly_name": "ACME",25"id": "comms_factor_04x93pexgrjmjn1zsx6f9r2d1t",26"next_step": {27"attestation": "none",28"authenticatorSelection": {29"authenticatorAttachment": "platform",30"requireResidentKey": false,31"residentKey": "preferred",32"userVerification": "preferred"33},34"challenge": "WUYwNGVhNDc2Nzc2MTg5NTI1NTBmZjNkMzNkMzgxMzQzYQ",35"excludeCredentials": [],36"pubKeyCredParams": [37{38"alg": -7,39"type": "public-key"40}41],42"rp": {43"id": "acme.com",44"name": "ACME"45},46"timeout": 600000,47"user": {48"displayName": "User 001",49"id": "WUVlNTBmYTQ5MDIwY2E0MzViMjc2MGEzMGFhYWNiYjZiOA",50"name": "passkeyuser001"51}52},53"related": [],54"status": "pending",55"tags": {},56"type": "passkey",57"updated_at": "2024-05-24T09:23:20.385660582Z",58"user_identifier": "passkeyuser001"59}
Use the next_step
response to create a passkey in the browser with navigator.credentials.create(). The navigator.credentials.create method requires certain parameters to be encoded.
We recommend using the create() wrapper provided by the webauthn-json library for easier encoding and decoding. The capability provided by this library will eventually be browser native. Open the developer console for your browser while on a twilio.com page (domain matching is part of the Passkeys spec) and copy the following code:
1const { create, get, parseCreationOptionsFromJSON, parseRequestOptionsFromJSON, supported } = await2import('https://cdn.jsdelivr.net/npm/@github/webauthn-json/dist/esm/webauthn-json.browser-ponyfill.js')3
This loads the webauthn-json library into your browser console. Assign the JSON response from step 1 to a variable:
1let { next_step: publicKey } = <paste JSON response from step 1>2
Next, copy the following code to the browser console create a passkey credential:
1// converts challenge and user.id to ArrayBuffers2let creationOptions = parseCreationOptionsFromJSON({publicKey});3// wrapper for navigator.credentials.create4let credential = await create(creationOptions);5
Follow the prompts to register a passkey with the password manager of your choice then copy the credential to use in the next request. In the browser console run the following to copy the JSON request body to your clipboard:
1copy({ "content" : credential });2
Complete the Passkey registration by passing the response from the navigator.credentials.create() request to below endpoint with the content parameter.
POST https://comms.twilio.com/preview/Factors/Approve
Name | Type | Description |
---|---|---|
factor_id | String | A reference to a Factor. (🏢 not PII ) |
content (required) | object | The payload required to approve a Factor |
content.type | String | Value: "public-key" The valid credential types supported by the API. The values of this enumeration are used for versioning the AuthenticatorAssertion and AuthenticatorAttestation structures according to the type of the authenticator. |
content.id | String | A base64url encoded representation of rawId. |
content.rawId | String | The globally unique identifier for this PublicKeyCredential. |
content.authenticatorAttachment | String | Enum: "platform" "cross-platform" A string that indicates the mechanism by which the WebAuthn implementation is attached to the authenticator at the time the associated navigator.credentials.create() or navigator.credentials.get() call completes. |
content.response | object | The result of a WebAuthn credential registration via navigator.credentials.create(), as specified in AuthenticatorAttestationResponse. |
content.response.clientDataJSON (required) | String | This property contains the JSON-compatible serialization of the data passed from the browser to the authenticator in order to generate this credential. |
content.response.attestationObject (required) | String | The authenticator data and an attestation statement for a new key pair generated by the authenticator. |
content.response.transports | String | Items Enum: "usb" "nfc" "ble" "smart-card" "internal" "hybrid" An array of strings providing hints as to the methods the client could use to communicate with the relevant authenticator of the public key credential to retrieve. |
1curl --location 'https://comms.twilio.com/preview/Factors/Approve' \2--header 'Content-Type: application/json' \3--header 'Authorization: Basic ***' \4--data '{5"factor_id": "comms_factor_04nwt37vgrb7j21yn7g55z98qw",6"content": {7"type": "public-key",8"id": "4-O54pnhw12mMAz8rvDcZ3pvEWwEZzSluVVK-cHjbXs",9"rawId": "4-O54pnhw12mMAz8rvDcZ3pvEWwEZzSluVVK-cHjbXs",10"authenticatorAttachment": "platform",11"response": {12"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiV1VZd05HRm1NelF6TTJWbE1UZzFPV1UwTWpCbVlXRTNPREUwWW1ZMFlUSm1ZdyIsIm9yaWdpbiI6Imh0dHBzOi8vNTdmOTJhZGI1YzAzLm5ncm9rLmFwcCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",13"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikw7izEZUWvBs_gwvj5FDoWndf0jzEJpSCztwEIi9HgONFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIOPjueKZ4cNdpjAM_K7w3Gd6bxFsBGc0pblVSvnB4217pQECAyYgASFYIP_2j2eHQVMka1OtAibT6LtNJPbRmTMX0bOXocijGFWOIlggCOnyYGFzN-yCLwTn9se3xreIBHRv6HipD4QKs4N9MkY",14"transports": [15"internal"16]17}18}19}'
1{2"contact_id": "comms_contact_5hq9cznnc8dnvrrhz1m1pbfmv5",3"content": {4"authenticator_criteria": {5"authenticator_attachment": "platform",6"discoverable_credentials": "preferred",7"user_verification": "preferred"8},9"credential": {10"authenticator_metadata": {11"AAGUID": "adce0002-35bc-c60a-648b-0b25f1f05503",12"authenticator_attachment": "platform",13"clone_warning": false,14"sign_count": 015},16"credential_id": "4-O54pnhw12mMAz8rvDcZ3pvEWwEZzSluVVK-cHjbXs",17"credential_public_key": "pQECAyYgASFYIP_2j2eHQVMka1OtAibT6LtNJPbRmTMX0bOXocijGFWOIlggCOnyYGFzN-yCLwTn9se3xreIBHRv6HipD4QKs4N9MkY",18"flags": [19"user-present",20"user-verified",21"attested-credential-data"22],23"transports": [24"internal"25]26},27"relying_party": {28"id": "acme.com",29"name": "ACME",30"origins": [31"https://acme.com"32]33}34},35"created_at": "2024-05-24T09:43:42.281167662Z",36"deleted_at": null,37"friendly_name": "ACME",38"id": "comms_factor_04nwt37vgrb7j21yn7g55z98qw",39"next_step": {40"attestation": "none",41"authenticatorSelection": {42"authenticatorAttachment": "platform",43"requireResidentKey": false,44"residentKey": "preferred",45"userVerification": "preferred"46},47"challenge": "WUYwNGFmMzQzM2VlMTg1OWU0MjBmYWE3ODE0YmY0YTJmYw",48"excludeCredentials": [],49"pubKeyCredParams": [50{51"alg": -7,52"type": "public-key"53}54],55"rp": {56"id": "acme.com",57"name": "ACME Corporation"58},59"timeout": 600000,60"user": {61"displayName": "ACME",62"id": "WUViMWJhNTlmYWQ1ODg2ZDc3OGM0N2UxYTA2Y2I3ZDM2NQ",63"name": "ACME"64}65},66"related": [],67"status": "approved",68"tags": {},69"type": "passkey",70"updated_at": "2024-05-24T09:43:42.281167662Z",71"user_identifier": "passkeysuser001"72}
The steps to authenticate with a Passkey are:
[Server - Twilio] Create an authentication challenge.
[Client - Browser] Fetch the passkey with the challenge data. The browser will sign the challenge with the passkey.
[Server - Twilio] Validate the signature and approve the authentication challenge.
POST https://comms.twilio.com/preview/Verifications
Name | Type | Description |
---|---|---|
to (required) | object | The user to Verify. (🏢 PII ) |
to.user_identifier | String | A custom string that uniquely identifies a person or end-user. For example, this can be the end-user's username or a UUID4 string or an SHA256 hash. (🏢 PII ) |
content (required) | object | The content of the Verification communication. |
content.rp_id | String | The relying party identifier. |
content.user_verification | String | Enum: "required" "preferred" "discouraged" A string that specifies the extent to which the relying party desires to authenticate the user to the client, and the extent to which the client should request that the user be authenticated. |
1curl --location 'https://comms.dev.twilio.com/preview/Verifications' \2--header 'Content-Type: application/json' \3--header 'Authorization: Basic ***'4--data '{5"to": {6"factor_id": "comms_factor_04j1end14yz8m3XXXXX"7},8"content": {9"rp_id": "acme.com",10"user_verification": "preferred"11},12}'
1{2"created_at": "2024-07-15T09:46:10.217023412Z",3"deleted_at": null,4"id": "comms_verification_04zdtzzx9tj1gXXXXXX",5"next_step": {6"publicKey": {7"allowCredentials": [8{9"id": "wmDTDMVE6qzrTlM8sG7vgTfDPZ2lnT0AURGXXXXX",10"transports": [],11"type": "public-key"12}13],14"challenge": "WUMwNGZiNzVmZmY1M2E5MDYxN2MyOWIyMmJkNzQ0YzU0Nw",15"extensions": {},16"rpId": "acme.com",17"timeout": 300000,18"userVerification": "preferred"19}20},21"related": [],22"status": "pending",23"tags": {},24"to": {25"address": null,26"address_extension": null,27"channel": "passkey",28"contact_id": "comms_contact_01j0k8sf0se8n9dpjxXXXX",29"device_ip": null,30"factor_id": "comms_factor_04j1end14yz8m3jse90XXXX",31"otp_type": null,32"user_identifier": "testuser001"33},34"updated_at": "2024-07-15T09:46:10.217023412Z"35}
Use the next_step
response to get the passkey in the browser with navigator.credentials.get().
Like registration, we recommend using the get() wrapper provided by the webauthn-json library for easier encoding and decoding. In the developer console, paste the following code to assign the JSON response from step 1 to a variable.
1let { next_step: { publicKey } } = <paste JSON response from step 1>2
Next, add the following code to the browser console to get the passkey credential:
1// converts challenge and user.id to ArrayBuffers2let requestOptions = parseRequestOptionsFromJSON({publicKey});3// wrapper for navigator.credentials.create4let credential = await get(requestOptions);
Follow the prompts to register a passkey with the password manager of your choice then copy the credential to use in the next request. In the browser console run the following to copy the JSON request body to your clipboard:
copy({ "content" : credential });
Complete the authentication of a Passkey registration by passing the response from the get() request to the passkey verification check endpoint with the content parameter.
POST https://comms.twilio.com/preview/Verifications/Check
Name | Type | Description |
---|---|---|
content (required) | object | The set of properties to check against the Verification, according to the Verification channel or factor. |
content.id | String | A base64url encoded representation of rawId. |
content.rawId | String | The globally unique identifier for this PublicKeyCredential. |
content.authenticatorAttachment | String (Optional) | Enum: "platform" "cross-platform" A string that indicates the mechanism by which the WebAuthn implementation is attached to the authenticator at the time the associated navigator.credentials.create() or navigator.credentials.get() call completes. |
content.type | String | Value: "public-key" The valid credential types supported by the API. The values of this enumeration are used for versioning the AuthenticatorAssertion and AuthenticatorAttestation structures according to the type of the authenticator. |
content.response (required) | object | The result of a WebAuthn authentication via a navigator.credentials.get() request, as specified in AuthenticatorAttestationResponse. |
content.response.authenticatorData | String | A CBOR-encoded string, in CTAP2 canonical CBOR encoding form. |
content.response.clientDataJSON | String | This property contains the JSON-compatible serialization of the data passed from the browser to the authenticator in order to generate this credential. |
content.response.signature | String | An assertion signature over authenticatorData and clientDataJSON. The assertion signature is created with the private key of the key pair that was created during the originating navigator.credentials.create() call and verified using the public key of that same key pair. |
content.response.userHandle | String | The user handle stored in the authenticator, specified as user.id in the options passed to the originating navigator.credentials.create() call. This property should contain a base64url-encoded contact ID. |
1curl --location 'https://comms.dev.twilio.com/preview/Verifications/Check' \2--header 'Content-Type: application/json' \3--header 'Authorization: Basic ***'4--data '{5"verification_id": "comms_verification_04zdtzzx9tj1gqradj5fbm9ha7",6"content": {7"id": "wmDTDMVE6qzrTlM8sG7vgTfDPZ2lnT0AURGlAHG9B7k",8"rawId": "wmDTDMVE6qzrTlM8sG7vgTfDPZ2lnT0AURGlAHG9B7k",9"authenticatorAttachment": "platform",10"type": "public-key",11"clientExtensionResults": {},12"response": {13"authenticatorData": "PsiNdKDyTjYfuluswFpAJmUc71Ul9PPRGf7ZtyMXERIFAAAAAg",14"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiV1VNd05HWmlOelZtWm1ZMU0yRTVNRFl4TjJNeU9XSXlNbUprTnpRMFl6VTBOdyIsIm9yaWdpbiI6Imh0dHBzOi8vdmlydHVhbC1hdXRoZW50aWNhdG9yLWUyZS10ZXN0LnR3aWxpby5jb20iLCJjcm9zc09yaWdpbiI6ZmFsc2V9",15"signature": "MEUCIF3dZrka0ahPSylH_tRif9L0pZh_jBJlpzSE5T0feGevAiEA-q5KSFAt-5YTdXQP4PbrnRu4a7ux6v55go-wtK94P0E",16"userHandle": "WUU2MDg1MzU1MzVkYzA1NDMwMTY2MDljNDExODAwMWQyYg"17}18}19}'
1{2"created_at": "2024-07-15T09:46:10.217023412Z",3"deleted_at": null,4"id": "comms_verification_04zdtzzx9tj1gqradXXXX",5"next_step": {6"publicKey": {7"allowCredentials": [8{9"id": "wmDTDMVE6qzrTlM8sG7vgTfDPZ2lnT0AURGlAHG9B7k",10"transports": [],11"type": "public-key"12}13],14"challenge": "WUMwNGZiNzVmZmY1M2E5MDYxN2MyOWIyMmJkNzQ0YzU0Nw",15"extensions": {},16"rpId": "acme.com",17"timeout": 300000,18"userVerification": "preferred"19}20},21"related": [],22"status": "approved",23"tags": {},24"to": {25"address": null,26"address_extension": null,27"channel": "passkey",28"contact_id": "comms_contact_01j0k8sf0se8n9dpjx9db3gjrd",29"device_ip": null,30"factor_id": "comms_factor_04j1end14yz8m3jse90zhcd4vn",31"otp_type": null,32"user_identifier": "testuser"33},34"updated_at": "2024-07-15T09:46:10.445005034Z"35}