Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Public Key Client Validation Quickstart


This guide will walk you through the steps of implementing Public Key Client Validation. We include sample cURL commands and HTTP requests, and then at the end, we'll detail the steps in Java.


Public key client validation quickstart

public-key-client-validation-quickstart page anchor

To get started quickly, you can follow the Java example at the bottom of the page. It shows how Client Validation can be implemented, along with links to the Twilio Java helper library that supports this feature.


  1. Generate an RSA Key Pair: Create a valid key pair. (This only has to be done once.)
  2. Submit the Public Key: Submit the public key to Twilio via the Credentials Endpoint. (This is a one-time requirement as well.)
  3. Hash the Canonical Request: Every outgoing request needs to be hashed and signed. (This functionality is implemented in the Java helper library and can be seen below.)
  4. Generate JWT: Once the hash is created, it needs to be embedded in the JWT payload and signed with the private key. (This is also handled by the Java helper library.)
  5. Attach JWT to the request header: The last step is to add the JWT to the request header.

1. Generate an RSA Keypair

1-generate-an-rsa-keypair page anchor

A private key is used to sign your requests. It is verified by the public key which you provide to Twilio.

Note: When you generate the private key, be sure to save and protect it as this is the only means to verify your application's identity.

We recommend generating the RSA key pair using the OpenSSL(link takes you to an external page) toolkit.

For Windows Systems

for-windows-systems page anchor

Install and use Cygwin(link takes you to an external page) to run the OpenSSL RSA keypair commands below.

For Mac and Linux/Unix-based Systems

for-mac-and-linuxunix-based-systems page anchor

You can run the OpenSSL commands to generate an RSA Keypair.

Generate a Private Key

generate-a-private-key page anchor

_10
openssl genrsa -aes256 -out private_key.pem 2048

Note: Twilio will only accept keys that have a bit length of 2048 with an exponent of 65537.


_10
openssl rsa -pubout -in private_key.pem -out public_key.pem

Example Public Key Format If properly generated, the RSA public key should look like the example public key below:


_10
-----BEGIN PUBLIC KEY-----
_10
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlRgaHOdjxFVceFucQXkA
_10
0tTT6tY6YDlkWgThv4FLjtbBqzfRcRUkaTqpSJaGgBsTgXeBdLK0DgneTRmPwZzw
_10
...
_10
sD93r4H6ti519kM+u87I6On00S3k4r6pGsWnBCf+1RmJps6xfsDflPIAstyZEpa9
_10
xQIDAQAB
_10
-----END PUBLIC KEY-----

Be sure to include the full header and footer when submitting the key:

  • '-----BEGIN PUBLIC KEY-----' AND
  • '-----END PUBLIC KEY-----'

You can see your Public Key with this command:


_10
cat public_key.pem


2. Submit the Public Key to Twilio

2-submit-the-public-key-to-twilio page anchor

Sample Requests cURL


_10
curl -X POST "https://accounts.twilio.com/v1/Credentials/PublicKeys/" \
_10
-H "Authorization: Basic <token>" \
_10
-F "PublicKey=-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BA….9xQIDAQAB-----END PUBLIC KEY-----" \
_10
-F "FriendlyName=Client Validation"

Note: Line breaks in the PEM format of the key need to be removed when making the cURL request.

Sample Response


_10
{
_10
"date_updated": "2016-10-25T19:54:49Z",
_10
"friendly_name": "Client Validation",
_10
"account_sid": "AC171b8eb…...e737e0ee2cb99ee",
_10
"url": "https://accounts.twilio.com/v1/Credentials/PublicKeys/CR934061….ed833471f596a5b4",
_10
"sid": "CR934061….ed833471f596a5b4",
_10
"date_created": "2016-10-25T19:54:49Z"
_10
}


3. Hash the Canonical Request

3-hash-the-canonical-request page anchor

The following section describes how the request needs to be canonicalized, hashed and attached to the request.

_Note: The Java helper library implements this functionality and will do the work for you. An end-to-end example is at the bottom of this page. _

This approach is loosely based on the approach Amazon is using to sign AWS API requests(link takes you to an external page).

Canonical request pseudocode

canonical-request-pseudocode page anchor

_10
Canonical HTTP Method + '\n' +
_10
Canonical URI + '\n' +
_10
Canonical Query String + '\n' +
_10
Canonical Headers + '\n' +
_10
Signed Headers + '\n' +
_10
HexEncode(Hash(Request Body))


_10
POST /2010-04-01/Accounts/AC00000000000000000000000000000000
_10
HTTP/1.1
_10
Host: api.twilio.com
_10
Content-Type: application/x-www-form-urlencoded; charset=utf-8
_10
Content-Length: 33
_10
Authorization: Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=
_10
_10
FriendlyName=my new friendly name

Canonicalize the HTTP Method

canonicalize-the-http-method page anchor

The HTTP method is canonicalized by doing the following operations:

  1. Uppercase
  2. Trim

In the Example Request, this results in:


_10
POST

Canonicalize the Resource Path

canonicalize-the-resource-path page anchor

To canonicalize the resource path:

  1. Remove redundant path elements, for example:
    • '/foobar/./barfoo' becomes '/foobar/barfoo' AND
    • '/foobar/../barfoo' becomes '/barfoo'
  2. URL-encode the remaining path using the UTF-8 character set in accordance with RFC 3986(link takes you to an external page) with the following caveats:
    • ' ' (space) should always be '%20'
    • '*' (asterisk) should always be '%2A'
    • '%7E' should always be '~' (tilde)
  3. Empty string path should always result in '/'

In the Example Request, this results in:


_10
/2010-04-01/Accounts/AC00000000000000000000000000000000

Canonicalize the Query String

canonicalize-the-query-string page anchor

The query-string is canonicalized by the following operations:

  1. Remove the query-string from the URI (not-including the '?')
  2. Construct a collection of key/value pairs by splitting the query string on '&' ASCII Sort the combined "key=value" strings (not just the 'keys')
  3. URL encode each key and value following the Resource Path ( RFC 3986(link takes you to an external page) ) with our caveats from above
  4. Concatenate each key/value pair like this: {key}={value}
    • If the key has no accompanying value, should result in '{key}=' Join all key/value pairs with a '&' in between

In the example request, this results in an empty string.


_10
/2010-04-01/Accounts/AC00000000000000000000000000000000

If a request contains the following query parameter,


_10
?from=4151234567&to=4157654321&message=Thanks for your order

The canonicalized query string would be the following:


_10
from=4151234567&message=Thanks%20for%20your%20order%20&to=4157654321

Canonicalize the Headers

canonicalize-the-headers page anchor

The headers are canonicalized by the following operations:

  1. Filter the complete list of headers against the 'hrh' (hashed request headers) value in the enclosing JWT
  2. Lower-case and trim each header key
  3. Trim each header value and reduce continuous whitespace into a since space
  4. Sort header values that correspond to the same key
  5. Combine the key/values like this: "{key}:{values}\n"
  6. ASCII sort
  7. Note that because each header line is terminated with a '\n'. When the entire canonical request is combined, there should be a blank line between the canonical-headers and the canonical-hashed-headers

In the Example Request, this results in:


_10
authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=
_10
host:api.twilio.com

Canonicalize the Hashed Headers

canonicalize-the-hashed-headers page anchor

The hashed-headers are canonicalized by the following operations:

  1. Split on ';' (semi-colon)
  2. Lowercase and trim
  3. 1ASCII Sort
  4. Join with ';" (semi-colon)

In the Example Request assume they want to include 'Host' and 'Authorization' in the list of hashed-headers, this results in:


_10
authorization;host

If the request body is empty, omit hashing it.

To encode the request body:

  1. Hash the request body using SHA-256
  2. Hex-encode the resulting hash

In the Example Request, this results in:


_10
b8e20591615abc52293f088c87be6df8e9b7b40c3da573f134c9132add851e2d

In the example below, the first blank line is due to not having any query parameters. The second blank line is due to every canonicalized header being terminated with a '\n'.


_10
POST
_10
/2010-04-01/Accounts/AC00000000000000000000000000000000
_10
_10
authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=
_10
host:api.twilio.com
_10
_10
authorization;host
_10
b8e20591615abc52293f088c87be6df8e9b7b40c3da573f134c9132add851e2d

When the final canonical request string is created it must be hashed in a similar manner to the request body.

To encode the canonical request:

  1. Hash the request body using SHA-256
  2. Hex-encode the resulting hash

In the Example Request, this results in:


_10
245eece1e638d9b0081ca0621183cd417fc97a1818bd822aa26697f9aa70c792


Once you have created the hash, you can generate a JWT with the hash embedded.

Every JWT assertion is composed of three components, the header, the payload, and the signature.

  • The header specifies the algorithm used for the JWT signature.
  • The payload contains the hash and additional metadata
  • The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way.

To construct the JWT assertion, these three components must be base64 encoded and concatenated using a "." separator:


_10
<base64URLencoded header>.<base64URLencoded claims>.<base64URLencoded signature>

Note: For additional details on JWT go to: https://jwt.io/introduction/(link takes you to an external page)

Let's have a closer look at the different parts of the JWT Assertion:

The header consists of four parts: the content type, the type of the token, the hashing algorithm being used, and the reference to the public key Twilio should use to validate the message.

FieldValue(s)RequiredDescription
ctytwilio-pkrv;v=1yesContentType = Twilio Public Key Request Validation - Version 1
typJWTNo (Default: 'JWT')Media Type = JSON Web Token, other values rejected
algRS256 or PS256yesOne of RS256 or PS256. These are the only algorithms supported at the moment. RS256 = RSASSA-PKCS-v1_5 using SHA-256 hash algorithm. PS256 = RSASSA-PSS using SHA-SHA 256 hash algorithm.
kidCredentialSidyesKey ID = Identifier of the public key credential associated with the private key used to sign the JWT

Example header:


_10
{
_10
"cty": "twilio-pkrv;v=1",
_10
"typ": "JWT",
_10
"alg": "RS256",
_10
"kid": "CR00000000000000000000000000000000"
_10
}

The second part of the token is the payload, which contains the claims. Claims are statements about an entity and additional metadata. For the issuer field, you can create a Main type API key from the Twilio Console or with the REST API.

FieldValue(s)RequiredDescription
issAPIKeySidyesIssuer = APIKey Sid used to match against request credentials
subAccountSidyesSubject = AccountSid
expexpiration timeyesToken Expiry Time: token received after exp +- clock skew will be rejected. Max exp - nbf is 300 seconds
nbfnot before timeNo(Default: 'now') Not Before Time
hrhlist of headers to hashyesA ';' (semicolon) delimited list of lowercase headers to include in the request hash calculation. At a minimum, you must include 'Host' and 'Authorization'
rqhrequest hashyesPlease refer to '3. Create a Hash of the Canonical Request' above.

Example Payload:


_10
{
_10
"iss": "SK00000000000000000000000000000000",
_10
"sub": "AC00000000000000000000000000000000",
_10
"exp": 1471827354,
_10
"hrh": "authorization;host",
_10
"rqh": "245eece1e638d9b0081ca0621183cd417fc97a1818bd822aa26697f9aa70c792"
_10
}

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

Signature Creation Example


_10
RS256(
_10
base64UrlEncode(header) + "." +
_10
base64UrlEncode(payload),
_10
secret)

To validate the signature Twilio needs the public key. This public key needs to be uploaded to Twilio. The public key must be:

  • Algorithm: RSA
  • Modulus::bitLength: 2048
  • Format: X.509

Public key to successfully validate the Example JWT (below):


_10
-----BEGIN PUBLIC KEY-----
_10
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAum6dAjx7jM1GTYOcIo1x
_10
b+KvO/FsKUMd4xLiDeKNd5DZ1sVKoJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAK
_10
uGEGZotiVR7Zmbq7l+NuR+pR3KhYJagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O19
_10
8eNAMS3sRwNnVlyPwtvIamwN8iDxEr+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdw
_10
vwGSHlXh/sr91o7fy/thWxwzM9Dp+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y
_10
/g6KGo7b0ehR241pV0cmqFm0ebF0m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7
_10
IQIDAQAB
_10
-----END PUBLIC KEY-----

The request has to be signed with a private key. The private key must match the public key uploaded to Twilio.

There are no limitations on the private key (as opposed to the public key, enumerated above) other than it needs to match the public key. It can be either PKCS#1 or PKCS#8 (whichever the signing library supports).

The private key used to sign the Example JWT:


_27
-----BEGIN RSA PRIVATE KEY-----
_27
MIIEpAIBAAKCAQEAum6dAjx7jM1GTYOcIo1xb+KvO/FsKUMd4xLiDeKNd5DZ1sVK
_27
oJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAKuGEGZotiVR7Zmbq7l+NuR+pR3KhY
_27
JagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O198eNAMS3sRwNnVlyPwtvIamwN8iDx
_27
Er+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdwvwGSHlXh/sr91o7fy/thWxwzM9Dp
_27
+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y/g6KGo7b0ehR241pV0cmqFm0ebF0
_27
m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7IQIDAQABAoIBAQCIFvbGCyClR7Nq
_27
Igh3sIh+BBumxjUOadAHUmFxgU+DWFmsTZiMX+BI1pxeWYYdXIATx2EP6FK7yNii
_27
5dkOGge5UBo8AMNnH334mjcWSQ7XsFTRnpG5625wFkh7AT2bMXqiT7+kV/L2B1mk
_27
lla1eCfXyuuw+rTfobxtbmQC+izygW6pri4KbmIBxhlTMPcgns3dTADL0eoH0po6
_27
u2mKHBaLP9GZpxR+pbZE0y4e6qDJt4M3nwUpm1zDkJGVuyAQebTbMxtsP4VHQ/0t
_27
wKKi73bnD62CanRf+bqt+FJWEIPI6yOBVbxcvLVLStRRkOVwugZlP0seDOlLrWVo
_27
YnwIReABAoGBAOfUlV9xgTwYdokaZKVOh8uJewAc4qqE1e3dh4epLm9jLiUul2bQ
_27
dFxL/dtAur76Th9kpRbbNQizGKKKjDOD4r0qF+aNbsRpNhx9OqaTaK40CH3g6zlc
_27
i8HmW5kjoRTJTBoFtp+8U6OdeiksUZ1Xbm5yR8395Wm7Y4p4LmCOGIcRAoGBAM3e
_27
YB5tNM6U0FgRFRc6R8UMrgo4SLXNlqvzMyKC/eHPziJP2PKAvBAasqZwEISlK4D6
_27
T6Fqbb6eFh0XNYJEQq/3JkuC5HNfBIMZ81X5gxGq/pMQPiPbr6QfY3hzgUljKyky
_27
xkYiQdcu9E6KiMJXWpz2GNmctlQT1b0cpQW3GNMRAoGARGN00RwFuLmqthVAHXfG
_27
HWfoDgd3YkAfb7ULFxz0Ys2KPlO5PA5AVT3hnD1DGbVzOFWTUePGiFN07/YZF9VP
_27
HOh+9ndAdtZmrQ7QL3WKyuD0pFWmblx7qe6PlORqz1v2hDKtRf/jWH/LGrxFMzoo
_27
jJJP1leQxpkN6zo6zCb+21ECgYAsoYk1D3fjUV/Zt9parsfgcF9K1+jrgSapIJB1
_27
avCfg+2sgqMF7+LVmvQgIStzlltYGuwokmo4aQ1iQSXYl/PdMjebJ0Vfvbm8smOO
_27
wAkqS2fleh/+piHt8uAdvOzKfDVfOSLDEao0fHl6jY4Yk9eRL8kzZEYi9CniVdNw
_27
6cD4AQKBgQDPluvF2FmQiEPR0to4rcpfa3IznO2uC8V7fjSUBAZQ38zQhFbsL+DR
_27
7SbJbloHv1K5HzcAwkNuKQJeJ7WKGjGgtm4ScuLJbkTWQF2BJTcZA6cuqQ0RVRq4
_27
LGJ+GQyvLu2JUtZj+gL9Aab0mbB/pL/zw3vzg9bdYgVtN0rA2nF7jQ==
_27
-----END RSA PRIVATE KEY-----

The following JWT is composed of the example blocks from above. The JWT is signed with the private key above. This JWT can be validated with the public key above.


_10
eyJjdHkiOiJ0d2lsaW8tcGtydjt2PTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkNSMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifQ.eyJpc3MiOiJTSzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3ViIjoiQUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsImV4cCI6MTQ3MTgyNzM1NCwiaHJoIjoiYXV0aG9yaXphdGlvbjtob3N0IiwicnFoIjoiMjQ1ZWVjZTFlNjM4ZDliMDA4MWNhMDYyMTE4M2NkNDE3ZmM5N2ExODE4YmQ4MjJhYTI2Njk3ZjlhYTcwYzc5MiJ9.a8Z-NXPEf8FrfEpxYBF8kIdn_1VAoa4H6t_X_CmtT7YksKkLMsQl6X00Hx0zEItgu64Z-qeaANxmwme6Y7nRRVz2AV8ZPTv5sWPhXOHVevyEDf2QfPpteDd0gpoPA4KjaklJtnNR8iSAd68DBaUvVE6bnAsop6dM4vowYNOMCe4PUe_W8AXu6iIzHmQxm5AVatyPoRY4dR-Il1tswbUr5FlVGzJJsw7JLNd46FYp2gIfhDM52cgBMeH5qNQw9inUm-BUybT1rB-kB1UCNq_3WenGoTGZsJ32QSBXAS9pbjOYNHIrylR51GV2foxqcOpsgIBFt_udnWlsqkezRun7TQ


5. Attach JWT to the Request Header

5-attach-jwt-to-the-request-header page anchor

The JWT needs to be added to the request via the Twilio-Client-Validation header.


Client Validation Java Example

client-validation-java-example page anchor

The functionality is currently only supported in the latest Java helper library.

The following example covers all five steps of making a successful Client Validation request. This sample is also available on GitHub(link takes you to an external page).


_68
package com.twilio.example;
_68
_68
_68
import com.twilio.http.TwilioRestClient;
_68
import com.twilio.http.ValidationClient;
_68
import com.twilio.rest.accounts.v1.credential.PublicKey;
_68
import com.twilio.rest.api.v2010.account.Message;
_68
import com.twilio.rest.api.v2010.account.NewSigningKey;
_68
import com.twilio.twiml.TwiMLException;
_68
import com.twilio.type.PhoneNumber;
_68
_68
import java.security.KeyPair;
_68
import java.security.KeyPairGenerator;
_68
import java.util.Base64;
_68
_68
import io.jsonwebtoken.SignatureAlgorithm;
_68
_68
public class ValidationExample {
_68
_68
public static final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID");
_68
public static final String AUTH_TOKEN = System.getenv("TWILIO_AUTH_TOKEN");
_68
_68
/**
_68
* Example Twilio usage.
_68
*
_68
* @param args command line args
_68
* @throws TwiMLException if unable to generate TwiML
_68
*/
_68
public static void main(String[] args) throws Exception {
_68
// Generate public/private key pair
_68
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
_68
keyGen.initialize(2048);
_68
KeyPair pair = keyGen.generateKeyPair();
_68
java.security.PublicKey pk = pair.getPublic();
_68
_68
// Use the default rest client
_68
TwilioRestClient client =
_68
new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN)
_68
.build();
_68
_68
// Create a public key and signing key using the default client
_68
PublicKey key = PublicKey.creator(
_68
Base64.getEncoder().encodeToString(pk.getEncoded())
_68
).setFriendlyName("Public Key").create(client);
_68
_68
NewSigningKey signingKey = NewSigningKey.creator().create(client);
_68
_68
// Switch to validation client as the default client
_68
TwilioRestClient validationClient = new TwilioRestClient.Builder(signingKey.getSid(), signingKey.getSecret())
_68
.accountSid(ACCOUNT_SID)
_68
// Validation client supports RS256 or PS256 algorithm. The default is RS256.
_68
.httpClient(new ValidationClient(ACCOUNT_SID, key.getSid(), signingKey.getSid(), pair.getPrivate(), SignatureAlgorithm.PS256))
_68
.build();
_68
_68
// Make REST API requests
_68
Iterable<Message> messages = Message.reader().read(validationClient);
_68
for (Message message : messages) {
_68
System.out.println(message.getBody());
_68
}
_68
_68
Message message = Message.creator(
_68
new PhoneNumber("+1XXXXXXXXXX"),
_68
new PhoneNumber("+1XXXXXXXXXX"),
_68
"Public Key Client Validation Test"
_68
).create(validationClient);
_68
System.out.println(message.getSid());
_68
}
_68
}

Notes:

Standard API Keys are not permitted to manage Accounts (e.g. create subaccounts) and other API Keys. If you require this functionality please refer to this page for additional details.

It may take a few minutes after Enforcing Public Key Client Validation from Settings(link takes you to an external page) for it to take effect.


Rate this page: