Protect your Function with JSON Web Token
Runtime handler version requirement
This example uses headers and cookies, which are only accessible when your Function is running @twilio/runtime-handler version 1.2.0 or later. Consult the Runtime Handler guide to learn more about the latest version and how to update.
When protecting your public Functions and any sensitive data that they can expose, from unwanted requests and bad actors, it is important to consider some form of authentication to validate that only intended users are making requests. In this example, we'll be covering one of the most common forms of authentication: Bearer Authentication using JSON Web Token (JWT).
If you want to learn an alternative approach, you can also see this example of using Basic Auth.
Let's create a Function that will only accept requests with valid JWTs, and reject all other traffic.
Before you run any of the examples on this page, create a Function and paste the example code into it. You can create a Function in the Twilio Console or by using the Serverless Toolkit.
If you prefer a UI-driven approach, complete these steps in the Twilio Console:
- Log in to the Twilio Console and navigate to Develop > Functions & Assets. If you're using the legacy Console, open the Functions tab.
- Functions are contained within Services. Click Create Service to create a new Service.
- Click Add + and select Add Function from the dropdown.
- The Console creates a new protected Function that you can rename. The filename becomes the URL path of the Function.
- Copy one of the example code snippets from this page and paste the code into your newly created Function. You can switch examples by using the dropdown menu in the code rail.
- Click Save.
- Click Deploy All to build and deploy the Function. After deployment, you can access your Function at
https://<service-name>-<random-characters>-<optional-domain-suffix>.twil.io/<function-path>
For example:test-function-3548.twil.io/hello-world.
You can now invoke your Function with HTTP requests, configure it as the webhook for a Twilio phone number, call it from a Twilio Studio Run Function Widget, and more.
First, create a new auth Service and add two Public Functions using the directions above. These will be named:
/jwt/bearer
Replace the default contents of each Function with the JWT generation code (Generate a JSON Web Token for Function Authentication) for /jwt, and the JWT validation snippet (Authenticate Function requests using Bearer Authorization and JWT) for /bearer respectively. Save both Functions once they contain the new code.
1const jwt = require('jsonwebtoken');23// Hardcoded credentials for this example4const creds = {5username: 'twilio',6password: 'ahoy',7};8// Hardcoded secret for this example. In a real app, you would9// generate this and store it securely as an environment variable10// and access it via context.SECRET or similar11const secret = 'secret_key';1213// Function to generate a JWT token14exports.handler = (context, event, callback) => {15// Retrieve the username and password from the request16const { username, password } = event;17// Prepare a new Twilio response18const response = new Twilio.Response();1920// If the provided credentials are invalid, return 401 Unauthorized.21// In a real app you would check the credentials against your database.22if (username !== creds.username || password !== creds.password) {23response24.setBody('Username or password is incorrect')25.setStatusCode(401);2627return callback(null, response);28}2930// Create a new signed JWT for the user that will expire in 1 day.31// To understand more about JWT and what sub, iss, and these32// other options are, see https://jwt.io/33const token = jwt.sign(34{35sub: username,36iss: 'twil.io',37org: 'twilio',38perms: ['read'],39},40secret,41{ expiresIn: '1d' }42);4344// Set the token as the access_token header and return the response45response.setBody('OK').appendHeader('access_token', token);4647return callback(null, response);48};
We can check that authentication is working first by sending an unauthenticated request to our deployed Function. You can get the URL of your Function by clicking the Copy URL button next to the Function.
Then, using your client of choice, make a GET or POST request to your Function. It should return a 401 Unauthorized since the request contains no valid Authorization header.
curl -i -L -X POST 'https://auth-4173-dev.twil.io/bearer'
Result:
1$ curl -i -L -X POST 'https://auth-4173-dev.twil.io/bearer' -i23HTTP/2 4014date: Tue, 03 Aug 2021 23:01:55 GMT5content-type: application/octet-stream6content-length: 127www-authenticate: Bearer realm="Access to read salaries"8x-shenanigans: none910Unauthorized
Great! Requests are successfully being blocked from non-authenticated requests.
To make an authenticated request and get back a 200 OK, we'll need to first generate a valid JWT by calling /jwt. We can then include that token in the Authorization header of our request to /bearer.
To get a valid JWT, we'll need to submit a valid username and password to the /jwt Function. Right now, these are hardcoded in the Function as twilio and ahoy respectively. The JWT generator Function is expecting the username and password to be passed in the body of the request, so you'll need to create a POST request with a JSON body composed of those values. Using cURL, that would look like this:
1curl -i -L -X POST 'https://auth-4173-dev.twil.io/jwt' \2-H 'Content-Type: application/json' \3--data-raw '{4"username": "twilio",5"password": "ahoy"6}'
and the response would be:
1$ curl -i -L -X POST 'https://auth-4173-dev.twil.io/jwt' \2-H 'Content-Type: application/json' \3--data-raw '{4"username": "twilio",5"password": "ahoy"6}'78HTTP/2 2009date: Tue, 03 Aug 2021 23:16:35 GMT10content-type: application/octet-stream11content-length: 212access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2lsaW8iLCJpc3MiOiJ0d2lsLmlvIiwib3JnIjoidHdpbGlvIiwicGVybXMiOlsicmVhZCJdLCJpYXQiOjE2MjgwMzI1OTUsImV4cCI6MTYyODExODk5NX0.uZzHuN5PpK6qM5wCu01_S8lkFPDpIcxQJq6A7sDr6gc13x-shenanigans: none14x-content-type-options: nosniff15x-xss-protection: 1; mode=block1617OK
The header access_token contains the valid JWT that was just generated for us. Go ahead and try your request to /bearer again, but this time including the Authorization header including this JWT:
1curl -i -L -X POST 'https://auth-4173-dev.twil.io/bearer' \2-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2lsaW8iLCJpc3MiOiJ0d2lsLmlvIiwib3JnIjoidHdpbGlvIiwicGVybXMiOlsicmVhZCJdLCJpYXQiOjE2MjgwMzA3ODIsImV4cCI6MTYyODExNzE4Mn0.gBusSFmlRt_o3H3E2UB4GGxjbZJLOOS0bKFXTxAgnlw'
the response should be:
1$ curl -i -L -X POST 'https://auth-4173-dev.twil.io/bearer' \2-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2lsaW8iLCJpc3MiOiJ0d2lsLmlvIiwib3JnIjoidHdpbGlvIiwicGVybXMiOlsicmVhZCJdLCJpYXQiOjE2MjgwMzA3ODIsImV4cCI6MTYyODExNzE4Mn0.gBusSFmlRt_o3H3E2UB4GGxjbZJLOOS0bKFXTxAgnlw'34HTTP/2 2005date: Tue, 03 Aug 2021 23:20:10 GMT6content-type: application/json7content-length: 848x-shenanigans: none9x-content-type-options: nosniff10x-xss-protection: 1; mode=block1112[{"username":"jdoe","salary":"$2000.00"},{"username":"mturner","salary":"$2500.00"}]
At this point, Bearer Authentication is working for your Function!
To make this example your own, you could experiment with the following:
- Refactor the common
'secret_key'into an Environment Variable so that it is stored securely and only needs to be changed in one place. - Use Environment Variables to store the approved credentials, or even create a database of approved usernames and passwords to support multiple users.
- Instead of using a hardcoded array of data, retrieve values from a database.