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.
_10openssl 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.
_10openssl 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-----_10MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlRgaHOdjxFVceFucQXkA_100tTT6tY6YDlkWgThv4FLjtbBqzfRcRUkaTqpSJaGgBsTgXeBdLK0DgneTRmPwZzw_10..._10sD93r4H6ti519kM+u87I6On00S3k4r6pGsWnBCf+1RmJps6xfsDflPIAstyZEpa9_10xQIDAQAB_10-----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:
_10cat public_key.pem
Sample Requests cURL
_10curl -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}
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.
_10Canonical 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))
_10POST /2010-04-01/Accounts/AC00000000000000000000000000000000_10HTTP/1.1_10Host: api.twilio.com_10Content-Type: application/x-www-form-urlencoded; charset=utf-8_10Content-Length: 33_10Authorization: Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=_10_10FriendlyName=my new friendly name
The HTTP method is canonicalized by doing the following operations:
In the Example Request, this results in:
_10POST
To canonicalize the resource path:
In the Example Request, this results in:
_10/2010-04-01/Accounts/AC00000000000000000000000000000000
The query-string is canonicalized by the following operations:
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:
_10from=4151234567&message=Thanks%20for%20your%20order%20&to=4157654321
The headers are canonicalized by the following operations:
In the Example Request, this results in:
_10authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=_10host: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:
_10authorization;host
If the request body is empty, omit hashing it.
To encode the request body:
In the Example Request, this results in:
_10b8e20591615abc52293f088c87be6df8e9b7b40c3da573f134c9132add851e2d
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'.
_10POST_10/2010-04-01/Accounts/AC00000000000000000000000000000000_10_10authorization:Basic QUMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmb29iYXI=_10host:api.twilio.com_10_10authorization;host_10b8e20591615abc52293f088c87be6df8e9b7b40c3da573f134c9132add851e2d
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:
_10245eece1e638d9b0081ca0621183cd417fc97a1818bd822aa26697f9aa70c792
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:
_10<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:
_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.
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:
_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
_10RS256(_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:
Public key to successfully validate the Example JWT (below):
_10-----BEGIN PUBLIC KEY-----_10MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAum6dAjx7jM1GTYOcIo1x_10b+KvO/FsKUMd4xLiDeKNd5DZ1sVKoJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAK_10uGEGZotiVR7Zmbq7l+NuR+pR3KhYJagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O19_108eNAMS3sRwNnVlyPwtvIamwN8iDxEr+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdw_10vwGSHlXh/sr91o7fy/thWxwzM9Dp+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y_10/g6KGo7b0ehR241pV0cmqFm0ebF0m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7_10IQIDAQAB_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-----_27MIIEpAIBAAKCAQEAum6dAjx7jM1GTYOcIo1xb+KvO/FsKUMd4xLiDeKNd5DZ1sVK_27oJSH1oMGRtaVnN4Uzo1h5rUDGrB73hY9PRAKuGEGZotiVR7Zmbq7l+NuR+pR3KhY_27JagzLQ+K91GkBsJM0f4geK1qwXfHYmA11O198eNAMS3sRwNnVlyPwtvIamwN8iDx_27Er+GvT7OIGZxHOCYRXmDAueDDLZqSF5j/qdwvwGSHlXh/sr91o7fy/thWxwzM9Dp_27+h95OiML3cH/edt68NNLD5zxnHEZxx1K/w/Y/g6KGo7b0ehR241pV0cmqFm0ebF0_27m+950F7iCI+qha97kHpBtBSAzyyHOhy2d4v7IQIDAQABAoIBAQCIFvbGCyClR7Nq_27Igh3sIh+BBumxjUOadAHUmFxgU+DWFmsTZiMX+BI1pxeWYYdXIATx2EP6FK7yNii_275dkOGge5UBo8AMNnH334mjcWSQ7XsFTRnpG5625wFkh7AT2bMXqiT7+kV/L2B1mk_27lla1eCfXyuuw+rTfobxtbmQC+izygW6pri4KbmIBxhlTMPcgns3dTADL0eoH0po6_27u2mKHBaLP9GZpxR+pbZE0y4e6qDJt4M3nwUpm1zDkJGVuyAQebTbMxtsP4VHQ/0t_27wKKi73bnD62CanRf+bqt+FJWEIPI6yOBVbxcvLVLStRRkOVwugZlP0seDOlLrWVo_27YnwIReABAoGBAOfUlV9xgTwYdokaZKVOh8uJewAc4qqE1e3dh4epLm9jLiUul2bQ_27dFxL/dtAur76Th9kpRbbNQizGKKKjDOD4r0qF+aNbsRpNhx9OqaTaK40CH3g6zlc_27i8HmW5kjoRTJTBoFtp+8U6OdeiksUZ1Xbm5yR8395Wm7Y4p4LmCOGIcRAoGBAM3e_27YB5tNM6U0FgRFRc6R8UMrgo4SLXNlqvzMyKC/eHPziJP2PKAvBAasqZwEISlK4D6_27T6Fqbb6eFh0XNYJEQq/3JkuC5HNfBIMZ81X5gxGq/pMQPiPbr6QfY3hzgUljKyky_27xkYiQdcu9E6KiMJXWpz2GNmctlQT1b0cpQW3GNMRAoGARGN00RwFuLmqthVAHXfG_27HWfoDgd3YkAfb7ULFxz0Ys2KPlO5PA5AVT3hnD1DGbVzOFWTUePGiFN07/YZF9VP_27HOh+9ndAdtZmrQ7QL3WKyuD0pFWmblx7qe6PlORqz1v2hDKtRf/jWH/LGrxFMzoo_27jJJP1leQxpkN6zo6zCb+21ECgYAsoYk1D3fjUV/Zt9parsfgcF9K1+jrgSapIJB1_27avCfg+2sgqMF7+LVmvQgIStzlltYGuwokmo4aQ1iQSXYl/PdMjebJ0Vfvbm8smOO_27wAkqS2fleh/+piHt8uAdvOzKfDVfOSLDEao0fHl6jY4Yk9eRL8kzZEYi9CniVdNw_276cD4AQKBgQDPluvF2FmQiEPR0to4rcpfa3IznO2uC8V7fjSUBAZQ38zQhFbsL+DR_277SbJbloHv1K5HzcAwkNuKQJeJ7WKGjGgtm4ScuLJbkTWQF2BJTcZA6cuqQ0RVRq4_27LGJ+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.
_10eyJjdHkiOiJ0d2lsaW8tcGtydjt2PTEiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkNSMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifQ.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.
_68package com.twilio.example;_68_68_68import com.twilio.http.TwilioRestClient;_68import com.twilio.http.ValidationClient;_68import com.twilio.rest.accounts.v1.credential.PublicKey;_68import com.twilio.rest.api.v2010.account.Message;_68import com.twilio.rest.api.v2010.account.NewSigningKey;_68import com.twilio.twiml.TwiMLException;_68import com.twilio.type.PhoneNumber;_68_68import java.security.KeyPair;_68import java.security.KeyPairGenerator;_68import java.util.Base64;_68_68import io.jsonwebtoken.SignatureAlgorithm;_68_68public 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 for it to take effect.