Serverless Authentication with JSON Web Tokens
Sunday, 3 September 2017 · 47 min read · serverlessLet’s learn how we can use JSON Web Tokens to add authentication and authorization to our serverless functions! We will be using the Serverless framework.
The sample application is available on GitHub.
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.
Authentication & Authorization
First, a bit of background. Authentication and Authorization are two different processes.
Authentication determines a client’s identity - is the user who they claim to be? Clients present a set of credentials, which may be valid or invalid. Typically, a user verifies their identity with their username and password.
Authorization determines what a client is allowed to do. Clients may have presented valid authentication credentials, but they might not have sufficient privileges to perform an action. Each user has a different set of permissions, which limits what they can and cannot do.
We will look at how we can use JSON Web Tokens to add both Authentication and Authorization to our functions.
JSON Web Tokens (JWT)
JSON Web Tokens (JWT - pronounced ‘jot’) are a compact and self-contained way for securely transmitting information and represent claims between parties as a JSON object.
Below is an encoded JSON Web Token:
A JSON Web Token is a string consisting of three components, each component delimited by a period (.
) character.
Base64Url
decoding the JSON Web Token above gives us the following:
JSON Web Tokens consists of the Header, Payload, and Signature. A token is constructed as follows:
-
You generate a claim of arbitrary JSON data (the Payload), which in our case contains all the required information about a user for the purposes of authentication. A Header typically defines the signing algorithm
alg
and type of tokentyp
. -
You include some metadata in the Payload, such as when the claim expires, who the audience is, and so on. These claims are defined in the JWT IETF Draft.
-
The data (both Header and Payload) is then cryptographically signed with a Hash-based Message Authentication Code (HMAC) secret. The resulting Signature is used to verify the user’s identity and to ensure that the message was not tampered in any way.
Finally the Header, Payload, and Signature are Base64
encoded and concatenated together with periods to delimit the fields, which results in the token we see in the first example.
For authentication purposes, a JWT serves as the credential/identity object that clients must show to gatekeepers to verify that you’re allowed to access protected resources. JWTs can be signed by a trusted party, and verified by gatekeepers.
Authentication Flow
One of the primary use cases of JWTs is to authenticate requests. Once a user is issued a JWT, future requests can include the JWT in order to access protected resources and services. The authentication flow happens between the following parties:
- Resource Owner (the User): the party that owns the resource to be shared. Let’s call our user Sam.
- Resource Service: the service that holds the protected resource. Let’s call our service PhotoService.
- Authorizer: the service that verifies the identity of users.
- Client: the application (web/mobile/others) that makes requests to the Resource Server on behalf of the Resource Owner.
The flow goes as follows:
- Sam the Resource Owner wishes to view the contents of her photo album wallet through the Client.
- The Client talks to the PhotoService - a Resource Service, requesting for Sam’s Photo resource.
- Photos are a protected resource. Clients will need to pass an authorization check to continue.
- The Client talks to the Authorizer to request an access token. The Authorizer responds by asking for the user’s credentials.
- The Client redirects Sam the Resource Owner to the Authorizer.
- Sam the Resource Owner supplies her credentials to the Authorizer, which gives Sam the option to either deny or accept the Client’s request for access.
- The Authorizer verifies Sam’s credentials, redirects her back to the Client, and grants an Authorization Code to the Client.
- The Client presents the authorization code to the Authorizer, receiving an access token (a JSON Web Token) in return.
- The Client presents the access token (a JSON Web Token) to the PhotoService and retrieves Sam’s Photo resource.
- The PhotoService validates the token, decoding the JWT, and parsing its contents.
- If the access token is valid for the requested operation and resource, PhotoService returns Sam’s information to the Client.
- The Client shows Sam his photos.
Note that the Resource Owner does not share their credentials to the Client directly. Instead, users notify the Authorizer that the Client may access whatever it is that they requested, and the Client authenticates separately with an authorization code. For more details, check out the OpenID Connect spec.
In this article, we’re focusing on step 9 to 12.
Whenever the Client wants to access a protected route or resource,
it must send a JSON Web token in the request’s Authorization
header like so:
The Resource Server can read and verify this JWT to check if the user is allowed to perform certain actions.
Benefits of JWTs
Using a JSON Web Token as your identity object gives you some advantages compared to a traditional OAuth2 token:
1. Introspectable: A JSON Web Token carries an HTTP header-like metadata that can be easily inspected for client-side validation purposes. In contrast, plaintext Bearer
OAuth tokens can only be decoded by making API calls to the authorization server.
2. Fine Grained Access Control: You can specify detailed access control information within the token payload. In the same way that you can create AWS security policies with very specific permissions, you can limit the token to only give read/write access to a single resource.
To illustrate, you can populate your tokens with private claims containing a dynamic set of scopes
with JWTs like so:
Your Resource Server’s authentication middleware can then parse the JWT’s payload and check its scopes
.
3. Stateless: All the information needed to complete a particular request is sent along in the request. Since the JWT payload contains all the required information for us to authenticate the user, we can avoid making network calls to the authorization server. This is especially useful for mobile environments with unstable network conditions.
Hands-On
In this section, we will learn how to issue and verify JWTs. We’ll also look at a working serverless authorization example.
Issuing JWTs
For Node.js, use the node-jsonwebtoken
NPM module to issue and verify JWTs.
In the above snippet, we specify a payload
object { user }
to be signed with a secret
string.
This secret will also be used to verify the token later on.
Within your JWT Payload, you can include any fields. However, there are some reserved fields such as:
iat
(Issued at): Unix timestamp for time when the token was issuedexp
(Expires at): Unix timestamp for time after which this token should not be accepted.nbf
(Not before): Unix timestamp for time before which this token should not be accepted.iss
: The issuer of the token.sub
: The subject of the token.aud
: The audience of the token.jti
: Unique identifier for the JWT. Can be used to prevent the JWT from being replayed.
The node-jsonwebtoken
module automatically populates the payload’s iat
field for you by default, but it can be overriden. The exp
attribute is populated by the expiresIn
option.
For more information, learn more about Reserved JWT Claims.
Note that instead of issuing tokens yourself, you can also use a third-party auth provider such as Auth0 that issues tokens for you.
Verifying JWTs
Using the shared secret
you used to sign the JWT, you can verify the JWT’s authenticity:
By verifying the signature of the JWT with a shared secret, you can ensure that the token is issued by our authorization service without having to make a network call to our authorization service!
AWS Custom Authorizers
An AWS custom authorizer is a Lambda function that you provide to control access to your APIs. You can use an authorizer function to implement various authorization strategies, such as JSON Web Token (JWT) verification and OAuth provider callout, to return IAM policies that authorize the request.
When a client calls your function via HTTP, AWS API Gateway verifies whether a custom authorizer is configured for the API.
If so, API Gateway calls the Lambda function, supplying the authorization token extracted from a specified request header (e.g. Authorization
).
If the returned policy is invalid or the permissions are denied, the API call will not succeed.
In the Serverless framework, you can configure our HTTP endpoints to have a custom authorizer enabled like so:
In the case of a valid policy, API Gateway caches the returned policy, associated with the incoming token and used for the current and subsequent requests, over a pre-configured time-to-live (TTL) period of up to 3600 seconds (1 hour.) As a result, we don’t have to call the custom authorizer function before every individual API call.
You can also set the TTL period to zero seconds to disable the policy caching. The default TTL value is 300 seconds. Currently, the maximum TTL value of 3600 seconds cannot be increased.
Sample Application Walkthrough
Let’s try out our serverless-auth
application. In your terminal, do:
Our serverless-auth
application has three HTTP endpoints:
GET /cats
is a public endpoint anyone can access. You don’t need to be authenticated to access this endpoint.GET /pangolins
is a private endpoint, protected by an AWS Custom Authorizer. You need to have a valid identity to access this endpoint.POST /sessions
is a login endpoint. Pass a valid username and password in a JSON request body to get a JWT (see/lib/users.js
for valid combinations.) For example, the following is a valid combination:
In the sample application, we also have an authorize
authorizer function that is executed on protected HTTP endpoints.
In order to pass the authentication check, clients need to supply a valid JWT in their Authorization
request header when making calls to a protected endpoint.
In order to pass the authorization check, clients need a JWT belonging to a user with valid permissions. For this example, the user Cthon98
is authorized to access GET /pangolins
; AzureDiamond
is not.
Step 1: Access the public endpoint
Try calling the GET /cats
public endpoint. Even without supplying any Authorization
headers, you can get a response back:
This is because we don’t have a custom authorizer function set for this endpoint, making it publicly accessible even without a JWT.
Step 2: Attempt to access the protected endpoint
Next, try calling the GET /pangolins
protected endpoint.
We receive a 401 Unauthorized response, because we didn’t supply valid credentials in our HTTP call.
This endpoint does have a custom authorizer function enabled, as defined in the project’s serverless.yml
:
Step 3: Get a JWT by logging in
In order to access the /pangolins
protected endpoint, we need to include a JWT in the Authorization
request header.
Call the POST /sessions
login endpoint with a username and password like so:
We receive a JWT in response.
Step 4: Use the JWT to access protected endpoints
Call the GET /pangolins
protected endpoint with an Authorization
request header set to the JWT:
By supplying a valid JWT with sufficient credentials, we pass the authorizer check and are able access the endpoint.
In our example application, only the user Cthon98
has access to GET /pangolins
due to the scopes defined in the example users database.
You can try logging in as user AzureDiamond
and receive a JWT, but it will not have sufficient privileges to access the protected endpoint.
This example demonstrates how you can implement granular user permissions with JWTs.
Code Walkthrough
To make it more concrete, let’s walk through the serverless-auth
serverless authorization example.
Follow along by referring to the
serverless-auth
example included in the book’s sample code.
The example has four functions:
login
accepts a username and password pair, returning a JWT.authorize
is an authorization middleware function that can be enabled to protect individual functions.getCats
andgetPangolins
returns a dummy JSON response.
Login Function
The login
function takes in a username and password pair,
checks it against a dummy database, and returns a JSON Web Token (JWT) that can be used to access protected endpoints.
In the above code:
- First, we check the
username
andpassword
to see if it matches against a user in our database. - If the authentication process fails, we return a 401 response.
- We encode the user’s attributes (
username
andscopes
) as a JWT with a secret string. The token expires inJWT_EXPIRATION_TIME
5 minutes. - Finally, we return the JWT to the client. Clients can include this token in their
Authorization
header to access the/getPangolins
protected endpoint.
Authorizer Function
Authorizer functions are executed before another function. We specify which functions have a custom authorizer enabled in serverless.yml
:
Within the authorize
function, we verify and decode any JWTs in the Authorization
request header.
This is to determine if the client can access the endpoint:
In the above code:
- First, we verify and decode the JWT.
- If the JWT is invalid or has expired, we return an HTTP 401 Unauthorized response.
- Then, we check that the user’s
scopes
attribute includes the current function (e.g.getPangolins
) with theauthorizeUser
function. - Finally, we return an IAM policy object that
Allow
orDeny
access to the current function.
For your reference, here is the utils.buildIAMPolicy
function:
For a detailed reference on AWS Custom Authorizers, check out the official AWS docs.
Accessing the user object
You can include a context
object that will be available in the event.requestContext.authorizer
of protected functions.
This lets you pass the user context to the called function! For example:
In the above protected function getPangolins
, we can get the user
object from event.requestContext.authorizer
.
Note that the AWS Custom Authorizer
context
object cannot have an object attribute - we needed toJSON.stringify
theuser
object.
Summary
Congratulations! You learned about:
- The difference between Authentication and Authorization
- What JSON Web Tokens are
- How to use AWS Custom Authorizers to secure your functions
With these basic abstractions in mind, you can build an auth layer to secure your serverless APIs!
Get the Book
Interested to learn more?
Going Serverless is a practical guide to building Scalable applications with the Serverless framework and AWS Lambda. This book will teach you how to design, develop, test, deploy, monitor, and secure Serverless applications from planning to production.
In this book we’ll build three hands-on projects using Javascript, Amazon Web Services (AWS), and the Serverless framework. The projects you’ll build include:
- An event-driven image processing pipeline
- A scalable web scraper
- A serverless microservice
Each hands-on project is a real-life implementation of a serverless design pattern. You’ll also learn how to use a suite of AWS technologies beyond just Lambda such as API Gateway, DynamoDB, SNS, S3, and more.
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.