Skip to contentSkip to navigationSkip to topbar
On this page

Verify Passkeys Overview


(information)

Info

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(link takes you to an external page) to try it out for free!


What are Passkeys

what-are-passkeys page anchor

Passkeys, also known as FIDO/WebAuthn, is an industry-standard(link takes you to an external page) 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.(link takes you to an external page)

2023-11-21 at 10.49.36 PM.

Twilio Verify's support for Passkeys

twilio-verifys-support-for-passkeys page anchor

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(link takes you to an external page) that created the Passkeys standard.


Sequence diagram

sequence-diagram page anchor

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.

register-user-public-docs-sequence-diagram-Verify_Passkeys_Sequence_Diagram.
verify-user-public-docs-sequence-diagram-Verify_Passkeys_Sequence_Diagram.

Quickstart guide: Verify Passkeys API

quickstart-guide-verify-passkeys-api page anchor

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:

  1. [Server - Twilio] Create a passkey factor
  2. [Client - Browser] Register a passkey with the factor details
  3. [Server - Twilio] Verify passkey registration

1. Start Passkey registration

1-start-passkey-registration page anchor

Initiate the process of creating a new Passkey registration.

1
POST https:https://comms.twilio.com/preview/Factors
2
NameTypeDescription
friendly_nameStringA human-readable description of the Factor. Maximum length is 255 characters. (🏢 not PII )
toObjectThe end user that is enrolling the factor. (🏢 PII )
to.user_identifierStringA 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 )
contentObjectThe configuration options of the Factor according to the Factor type.
content.user.display_name (required)StringA human-palatable name for the user account, intended only for display.
content.relying_party.id (required)StringThe relying party identifier. This should generally be the origin without a scheme and port.
content.relying_party.nameStringThe relying party name that the authenticator will show during the registration/authentication process.
content.relying_party.originsArray[String]List of Relying Party Server Origins or App IDs that are permitted.
content.authenticator_criteria.authenticator_attachmentStringDefault: "any" Enum: "platform" "cross-platform" "any" A flag indicating a requirement to attach only to a certain type of authenticator.
content.authenticator_criteria.discoverable_credentialsStringDefault: "preferred" Enum: "required" "preferred" "discouraged" A flag indicating the level of preference for discoverable credentials.
content.authenticator_criteria.user_verificationStringDefault: "preferred" Enum: "required" "preferred" "discouraged" Whether user identity verification (via biometrics or PIN) is required.
1
curl --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
}

2. Create Passkey from the browser

2-create-passkey-from-the-browser page anchor

Use the next_step response to create a passkey in the browser with navigator.credentials.create()(link takes you to an external page). The navigator.credentials.create method requires certain parameters to be encoded.

We recommend using the create() wrapper provided by the webauthn-json library(link takes you to an external page) 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:

1
const { create, get, parseCreationOptionsFromJSON, parseRequestOptionsFromJSON, supported } = await
2
import('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:

1
let { 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 ArrayBuffers
2
let creationOptions = parseCreationOptionsFromJSON({publicKey});
3
// wrapper for navigator.credentials.create
4
let 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:

1
copy({ "content" : credential });
2

3. Approve Passkey Registration

3-approve-passkey-registration page anchor

Complete the Passkey registration by passing the response from the navigator.credentials.create()(link takes you to an external page) request to below endpoint with the content parameter.

POST https://comms.twilio.com/preview/Factors/Approve
NameTypeDescription
factor_idStringA reference to a Factor. (🏢 not PII )
content (required)objectThe payload required to approve a Factor
content.typeStringValue: "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.idStringA base64url encoded representation of rawId.
content.rawIdStringThe globally unique identifier for this PublicKeyCredential.
content.authenticatorAttachmentStringEnum: "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.responseobjectThe result of a WebAuthn credential registration via navigator.credentials.create(), as specified in AuthenticatorAttestationResponse.
content.response.clientDataJSON (required)StringThis 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)StringThe authenticator data and an attestation statement for a new key pair generated by the authenticator.
content.response.transportsStringItems 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.
1
curl --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": 0
15
},
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
}

Authenticate with Passkey

authenticate-with-passkey page anchor

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.

1. Create a passkey authentication challenge

1-create-a-passkey-authentication-challenge page anchor
POST https://comms.twilio.com/preview/Verifications
NameTypeDescription
to (required)objectThe user to Verify. (🏢 PII )
to.user_identifierStringA 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)objectThe content of the Verification communication.
content.rp_idStringThe relying party identifier.
content.user_verificationStringEnum: "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.
1
curl --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
}

2. Fetch the passkey in the browser

2-fetch-the-passkey-in-the-browser page anchor

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.

1
let { 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 ArrayBuffers
2
let requestOptions = parseRequestOptionsFromJSON({publicKey});
3
// wrapper for navigator.credentials.create
4
let 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 });

3. Validate the passkey

3-validate-the-passkey page anchor

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
NameTypeDescription
content (required)objectThe set of properties to check against the Verification, according to the Verification channel or factor.
content.idStringA base64url encoded representation of rawId.
content.rawIdStringThe globally unique identifier for this PublicKeyCredential.
content.authenticatorAttachmentString (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.typeStringValue: "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)objectThe result of a WebAuthn authentication via a navigator.credentials.get() request, as specified in AuthenticatorAttestationResponse.
content.response.authenticatorDataStringA CBOR-encoded string, in CTAP2 canonical CBOR encoding form.
content.response.clientDataJSONStringThis property contains the JSON-compatible serialization of the data passed from the browser to the authenticator in order to generate this credential.
content.response.signatureStringAn 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.userHandleStringThe 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.
1
curl --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
}

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.