Skip to contentSkip to navigationSkip to topbar
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
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.

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:

1
-----BEGIN PUBLIC KEY-----
2
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlRgaHOdjxFVceFucQXkA
3
0tTT6tY6YDlkWgThv4FLjtbBqzfRcRUkaTqpSJaGgBsTgXeBdLK0DgneTRmPwZzw
4
...
5
sD93r4H6ti519kM+u87I6On00S3k4r6pGsWnBCf+1RmJps6xfsDflPIAstyZEpa9
6
xQIDAQAB
7
-----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:

cat public_key.pem

2. Submit the Public Key to Twilio

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

Sample Requests cURL

1
curl -X POST "https://accounts.twilio.com/v1/Credentials/PublicKeys/" \
2
-H "Authorization: Basic <token>" \
3
-F "PublicKey=-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BA….9xQIDAQAB-----END PUBLIC KEY-----" \
4
-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

1
{
2
"date_updated": "2016-10-25T19:54:49Z",
3
"friendly_name": "Client Validation",
4
"account_sid": "AC171b8eb…...e737e0ee2cb99ee",
5
"url": "https://accounts.twilio.com/v1/Credentials/PublicKeys/CR934061….ed833471f596a5b4",
6
"sid": "CR934061….ed833471f596a5b4",
7
"date_created": "2016-10-25T19:54:49Z"
8
}

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
1
Canonical HTTP Method + '\n' +
2
Canonical URI + '\n' +
3
Canonical Query String + '\n' +
4
Canonical Headers + '\n' +
5
Signed Headers + '\n' +
6
HexEncode(Hash(Request Body))
1
POST /2010-04-01/Accounts/AC00000000000000000000000000000000
2
HTTP/1.1
3
Host: api.twilio.com
4
Content-Type: application/x-www-form-urlencoded; charset=utf-8
5
Content-Length: 33
6
Authorization: Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=
7
8
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:

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:

/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.

/2010-04-01/Accounts/AC00000000000000000000000000000000

If a request contains the following query parameter,

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

The canonicalized query string would be the following:

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:

1
authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=
2
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:

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:

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'.

1
POST
2
/2010-04-01/Accounts/AC00000000000000000000000000000000
3
4
authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=
5
host:api.twilio.com
6
7
authorization;host
8
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:

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:

<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:

1
{
2
"cty": "twilio-pkrv;v=1",
3
"typ": "JWT",
4
"alg": "RS256",
5
"kid": "CR00000000000000000000000000000000"
6
}

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:

1
{
2
"iss": "SK00000000000000000000000000000000",
3
"sub": "AC00000000000000000000000000000000",
4
"exp": 1471827354,
5
"hrh": "authorization;host",
6
"rqh": "245eece1e638d9b0081ca0621183cd417fc97a1818bd822aa26697f9aa70c792"
7
}

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

1
RS256(
2
base64UrlEncode(header) + "." +
3
base64UrlEncode(payload),
4
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):

1
-----BEGIN PUBLIC KEY-----
2
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAum6dAjx7jM1GTYOcIo1x
3
b+KvO/FsKUMd4xLiDeKNd5DZ1sVKoJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAK
4
uGEGZotiVR7Zmbq7l+NuR+pR3KhYJagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O19
5
8eNAMS3sRwNnVlyPwtvIamwN8iDxEr+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdw
6
vwGSHlXh/sr91o7fy/thWxwzM9Dp+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y
7
/g6KGo7b0ehR241pV0cmqFm0ebF0m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7
8
IQIDAQAB
9
-----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:

1
-----BEGIN RSA PRIVATE KEY-----
2
MIIEpAIBAAKCAQEAum6dAjx7jM1GTYOcIo1xb+KvO/FsKUMd4xLiDeKNd5DZ1sVK
3
oJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAKuGEGZotiVR7Zmbq7l+NuR+pR3KhY
4
JagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O198eNAMS3sRwNnVlyPwtvIamwN8iDx
5
Er+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdwvwGSHlXh/sr91o7fy/thWxwzM9Dp
6
+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y/g6KGo7b0ehR241pV0cmqFm0ebF0
7
m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7IQIDAQABAoIBAQCIFvbGCyClR7Nq
8
Igh3sIh+BBumxjUOadAHUmFxgU+DWFmsTZiMX+BI1pxeWYYdXIATx2EP6FK7yNii
9
5dkOGge5UBo8AMNnH334mjcWSQ7XsFTRnpG5625wFkh7AT2bMXqiT7+kV/L2B1mk
10
lla1eCfXyuuw+rTfobxtbmQC+izygW6pri4KbmIBxhlTMPcgns3dTADL0eoH0po6
11
u2mKHBaLP9GZpxR+pbZE0y4e6qDJt4M3nwUpm1zDkJGVuyAQebTbMxtsP4VHQ/0t
12
wKKi73bnD62CanRf+bqt+FJWEIPI6yOBVbxcvLVLStRRkOVwugZlP0seDOlLrWVo
13
YnwIReABAoGBAOfUlV9xgTwYdokaZKVOh8uJewAc4qqE1e3dh4epLm9jLiUul2bQ
14
dFxL/dtAur76Th9kpRbbNQizGKKKjDOD4r0qF+aNbsRpNhx9OqaTaK40CH3g6zlc
15
i8HmW5kjoRTJTBoFtp+8U6OdeiksUZ1Xbm5yR8395Wm7Y4p4LmCOGIcRAoGBAM3e
16
YB5tNM6U0FgRFRc6R8UMrgo4SLXNlqvzMyKC/eHPziJP2PKAvBAasqZwEISlK4D6
17
T6Fqbb6eFh0XNYJEQq/3JkuC5HNfBIMZ81X5gxGq/pMQPiPbr6QfY3hzgUljKyky
18
xkYiQdcu9E6KiMJXWpz2GNmctlQT1b0cpQW3GNMRAoGARGN00RwFuLmqthVAHXfG
19
HWfoDgd3YkAfb7ULFxz0Ys2KPlO5PA5AVT3hnD1DGbVzOFWTUePGiFN07/YZF9VP
20
HOh+9ndAdtZmrQ7QL3WKyuD0pFWmblx7qe6PlORqz1v2hDKtRf/jWH/LGrxFMzoo
21
jJJP1leQxpkN6zo6zCb+21ECgYAsoYk1D3fjUV/Zt9parsfgcF9K1+jrgSapIJB1
22
avCfg+2sgqMF7+LVmvQgIStzlltYGuwokmo4aQ1iQSXYl/PdMjebJ0Vfvbm8smOO
23
wAkqS2fleh/+piHt8uAdvOzKfDVfOSLDEao0fHl6jY4Yk9eRL8kzZEYi9CniVdNw
24
6cD4AQKBgQDPluvF2FmQiEPR0to4rcpfa3IznO2uC8V7fjSUBAZQ38zQhFbsL+DR
25
7SbJbloHv1K5HzcAwkNuKQJeJ7WKGjGgtm4ScuLJbkTWQF2BJTcZA6cuqQ0RVRq4
26
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.

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).

1
package com.twilio.example;
2
3
4
import com.twilio.http.TwilioRestClient;
5
import com.twilio.http.ValidationClient;
6
import com.twilio.rest.accounts.v1.credential.PublicKey;
7
import com.twilio.rest.api.v2010.account.Message;
8
import com.twilio.rest.api.v2010.account.NewSigningKey;
9
import com.twilio.twiml.TwiMLException;
10
import com.twilio.type.PhoneNumber;
11
12
import java.security.KeyPair;
13
import java.security.KeyPairGenerator;
14
import java.util.Base64;
15
16
import io.jsonwebtoken.SignatureAlgorithm;
17
18
public class ValidationExample {
19
20
public static final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID");
21
public static final String AUTH_TOKEN = System.getenv("TWILIO_AUTH_TOKEN");
22
23
/**
24
* Example Twilio usage.
25
*
26
* @param args command line args
27
* @throws TwiMLException if unable to generate TwiML
28
*/
29
public static void main(String[] args) throws Exception {
30
// Generate public/private key pair
31
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
32
keyGen.initialize(2048);
33
KeyPair pair = keyGen.generateKeyPair();
34
java.security.PublicKey pk = pair.getPublic();
35
36
// Use the default rest client
37
TwilioRestClient client =
38
new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN)
39
.build();
40
41
// Create a public key and signing key using the default client
42
PublicKey key = PublicKey.creator(
43
Base64.getEncoder().encodeToString(pk.getEncoded())
44
).setFriendlyName("Public Key").create(client);
45
46
NewSigningKey signingKey = NewSigningKey.creator().create(client);
47
48
// Switch to validation client as the default client
49
TwilioRestClient validationClient = new TwilioRestClient.Builder(signingKey.getSid(), signingKey.getSecret())
50
.accountSid(ACCOUNT_SID)
51
// Validation client supports RS256 or PS256 algorithm. The default is RS256.
52
.httpClient(new ValidationClient(ACCOUNT_SID, key.getSid(), signingKey.getSid(), pair.getPrivate(), SignatureAlgorithm.PS256))
53
.build();
54
55
// Make REST API requests
56
Iterable<Message> messages = Message.reader().read(validationClient);
57
for (Message message : messages) {
58
System.out.println(message.getBody());
59
}
60
61
Message message = Message.creator(
62
new PhoneNumber("+1XXXXXXXXXX"),
63
new PhoneNumber("+1XXXXXXXXXX"),
64
"Public Key Client Validation Test"
65
).create(validationClient);
66
System.out.println(message.getSid());
67
}
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.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.