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.
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.
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 toolkit.
Install and use Cygwin to run the OpenSSL RSA keypair commands below.
You can run the OpenSSL commands to generate an RSA Keypair.
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-----2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlRgaHOdjxFVceFucQXkA30tTT6tY6YDlkWgThv4FLjtbBqzfRcRUkaTqpSJaGgBsTgXeBdLK0DgneTRmPwZzw4...5sD93r4H6ti519kM+u87I6On00S3k4r6pGsWnBCf+1RmJps6xfsDflPIAstyZEpa96xQIDAQAB7-----END PUBLIC KEY-----
Be sure to include the full header and footer when submitting the key:
You can see your Public Key with this command:
cat public_key.pem
Sample Requests cURL
1curl -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}
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.
1Canonical HTTP Method + '\n' +2Canonical URI + '\n' +3Canonical Query String + '\n' +4Canonical Headers + '\n' +5Signed Headers + '\n' +6HexEncode(Hash(Request Body))
1POST /2010-04-01/Accounts/AC000000000000000000000000000000002HTTP/1.13Host: api.twilio.com4Content-Type: application/x-www-form-urlencoded; charset=utf-85Content-Length: 336Authorization: Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=78FriendlyName=my new friendly name
The HTTP method is canonicalized by doing the following operations:
In the Example Request, this results in:
POST
To canonicalize the resource path:
In the Example Request, this results in:
/2010-04-01/Accounts/AC00000000000000000000000000000000
The query-string is canonicalized by the following operations:
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
The headers are canonicalized by the following operations:
In the Example Request, this results in:
1authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=2host:api.twilio.com
The hashed-headers are canonicalized by the following operations:
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:
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'.
1POST2/2010-04-01/Accounts/AC0000000000000000000000000000000034authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=5host:api.twilio.com67authorization;host8b8e20591615abc52293f088c87be6df8e9b7b40c3da573f134c9132add851e2d
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:
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.
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/
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.
Field | Value(s) | Required | Description |
---|---|---|---|
cty | twilio-pkrv;v=1 | yes | ContentType = Twilio Public Key Request Validation - Version 1 |
typ | JWT | No (Default: 'JWT') | Media Type = JSON Web Token, other values rejected |
alg | RS256 or PS256 | yes | One 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. |
kid | CredentialSid | yes | Key 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.
Field | Value(s) | Required | Description |
---|---|---|---|
iss | APIKeySid | yes | Issuer = APIKey Sid used to match against request credentials |
sub | AccountSid | yes | Subject = AccountSid |
exp | expiration time | yes | Token Expiry Time: token received after exp +- clock skew will be rejected. Max exp - nbf is 300 seconds |
nbf | not before time | No | (Default: 'now') Not Before Time |
hrh | list of headers to hash | yes | A ';' (semicolon) delimited list of lowercase headers to include in the request hash calculation. At a minimum, you must include 'Host' and 'Authorization' |
rqh | request hash | yes | Please 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
1RS256(2base64UrlEncode(header) + "." +3base64UrlEncode(payload),4secret)
To validate the signature Twilio needs the public key. This public key needs to be uploaded to Twilio. The public key must be:
Public key to successfully validate the Example JWT (below):
1-----BEGIN PUBLIC KEY-----2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAum6dAjx7jM1GTYOcIo1x3b+KvO/FsKUMd4xLiDeKNd5DZ1sVKoJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAK4uGEGZotiVR7Zmbq7l+NuR+pR3KhYJagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O1958eNAMS3sRwNnVlyPwtvIamwN8iDxEr+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdw6vwGSHlXh/sr91o7fy/thWxwzM9Dp+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y7/g6KGo7b0ehR241pV0cmqFm0ebF0m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v78IQIDAQAB9-----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-----2MIIEpAIBAAKCAQEAum6dAjx7jM1GTYOcIo1xb+KvO/FsKUMd4xLiDeKNd5DZ1sVK3oJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAKuGEGZotiVR7Zmbq7l+NuR+pR3KhY4JagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O198eNAMS3sRwNnVlyPwtvIamwN8iDx5Er+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdwvwGSHlXh/sr91o7fy/thWxwzM9Dp6+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y/g6KGo7b0ehR241pV0cmqFm0ebF07m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7IQIDAQABAoIBAQCIFvbGCyClR7Nq8Igh3sIh+BBumxjUOadAHUmFxgU+DWFmsTZiMX+BI1pxeWYYdXIATx2EP6FK7yNii95dkOGge5UBo8AMNnH334mjcWSQ7XsFTRnpG5625wFkh7AT2bMXqiT7+kV/L2B1mk10lla1eCfXyuuw+rTfobxtbmQC+izygW6pri4KbmIBxhlTMPcgns3dTADL0eoH0po611u2mKHBaLP9GZpxR+pbZE0y4e6qDJt4M3nwUpm1zDkJGVuyAQebTbMxtsP4VHQ/0t12wKKi73bnD62CanRf+bqt+FJWEIPI6yOBVbxcvLVLStRRkOVwugZlP0seDOlLrWVo13YnwIReABAoGBAOfUlV9xgTwYdokaZKVOh8uJewAc4qqE1e3dh4epLm9jLiUul2bQ14dFxL/dtAur76Th9kpRbbNQizGKKKjDOD4r0qF+aNbsRpNhx9OqaTaK40CH3g6zlc15i8HmW5kjoRTJTBoFtp+8U6OdeiksUZ1Xbm5yR8395Wm7Y4p4LmCOGIcRAoGBAM3e16YB5tNM6U0FgRFRc6R8UMrgo4SLXNlqvzMyKC/eHPziJP2PKAvBAasqZwEISlK4D617T6Fqbb6eFh0XNYJEQq/3JkuC5HNfBIMZ81X5gxGq/pMQPiPbr6QfY3hzgUljKyky18xkYiQdcu9E6KiMJXWpz2GNmctlQT1b0cpQW3GNMRAoGARGN00RwFuLmqthVAHXfG19HWfoDgd3YkAfb7ULFxz0Ys2KPlO5PA5AVT3hnD1DGbVzOFWTUePGiFN07/YZF9VP20HOh+9ndAdtZmrQ7QL3WKyuD0pFWmblx7qe6PlORqz1v2hDKtRf/jWH/LGrxFMzoo21jJJP1leQxpkN6zo6zCb+21ECgYAsoYk1D3fjUV/Zt9parsfgcF9K1+jrgSapIJB122avCfg+2sgqMF7+LVmvQgIStzlltYGuwokmo4aQ1iQSXYl/PdMjebJ0Vfvbm8smOO23wAkqS2fleh/+piHt8uAdvOzKfDVfOSLDEao0fHl6jY4Yk9eRL8kzZEYi9CniVdNw246cD4AQKBgQDPluvF2FmQiEPR0to4rcpfa3IznO2uC8V7fjSUBAZQ38zQhFbsL+DR257SbJbloHv1K5HzcAwkNuKQJeJ7WKGjGgtm4ScuLJbkTWQF2BJTcZA6cuqQ0RVRq426LGJ+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
The JWT needs to be added to the request via the Twilio-Client-Validation header.
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.
1package com.twilio.example;234import com.twilio.http.TwilioRestClient;5import com.twilio.http.ValidationClient;6import com.twilio.rest.accounts.v1.credential.PublicKey;7import com.twilio.rest.api.v2010.account.Message;8import com.twilio.rest.api.v2010.account.NewSigningKey;9import com.twilio.twiml.TwiMLException;10import com.twilio.type.PhoneNumber;1112import java.security.KeyPair;13import java.security.KeyPairGenerator;14import java.util.Base64;1516import io.jsonwebtoken.SignatureAlgorithm;1718public class ValidationExample {1920public static final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID");21public static final String AUTH_TOKEN = System.getenv("TWILIO_AUTH_TOKEN");2223/**24* Example Twilio usage.25*26* @param args command line args27* @throws TwiMLException if unable to generate TwiML28*/29public static void main(String[] args) throws Exception {30// Generate public/private key pair31KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");32keyGen.initialize(2048);33KeyPair pair = keyGen.generateKeyPair();34java.security.PublicKey pk = pair.getPublic();3536// Use the default rest client37TwilioRestClient client =38new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN)39.build();4041// Create a public key and signing key using the default client42PublicKey key = PublicKey.creator(43Base64.getEncoder().encodeToString(pk.getEncoded())44).setFriendlyName("Public Key").create(client);4546NewSigningKey signingKey = NewSigningKey.creator().create(client);4748// Switch to validation client as the default client49TwilioRestClient 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();5455// Make REST API requests56Iterable<Message> messages = Message.reader().read(validationClient);57for (Message message : messages) {58System.out.println(message.getBody());59}6061Message message = Message.creator(62new PhoneNumber("+1XXXXXXXXXX"),63new PhoneNumber("+1XXXXXXXXXX"),64"Public Key Client Validation Test"65).create(validationClient);66System.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 for it to take effect.