To make the most of this tutorial, sign up for Serverless Framework’s dashboard account for free:
https://dashboard.serverless.com
Searching for a way to do user authentication in your serverless project?
Look no further. In this post I’ll be covering robust approaches to storing authentication-related data in serverless applications!
We’ll talk about storing user information with sessions and JWT, token validity with Lambda Custom Authorizers, user management from scratch vs hosted services, and so much more. (Spoiler alert: there is no one perfect solution.)
I’ll cover a few examples of implementing both authentication and user management, and give my thoughts on the future of authentication mechanisms for the Serverless architecture.
I’ll be mentioning the following examples in this post; feel free to check them out beforehand if you’d like:
When implementing authentication in your Serverless project, there are two steps: (1) give your users the ability to identify themselves, (2) retrieve their identity in your Serverless functions.
The most common ways to accomplish this are storing user sessions, and writing user information inside JSON Web Tokens.
Sessions are a standard for storing authentication-related information.
Upon authentication, the user gets a token. The token is then sent to the server on every request, and used to look up user information in the database—the status of the session, expiration time, and authentication scopes.
Typically, you would store session data in either Redis or Memcached. But for Serverless projects, it makes sense to use hosted datastores instead—Amazon ElastiCache or DynamoDB, Google Cloud Datastore, etc.
The down side is, hitting DynamoDB or another datastore to retrieve session information can be a challenge. With a high enough load on your application, retrieving sessions might add a significant amount to the datastore costs and increase page load times for users. Not so optimal.
Enter JSON Web Tokens (JWT), a growing favorite for serverless projects.
The authentication mechanism here is similar to sessions, in that the user gets a token upon logging in, and then sends that token back to the endpoint on every request. But JWT has a key advantage; it makes it easy to store additional user information directly in the token, not just the access credentials.
On every request, the user will send the whole token to the endpoint. If you store their username or access scopes in the JWT token, it will be very easy to access that information in every HTTP request you receive.
This has a number of benefits for serverless projects compared to sessions:
Unfortunately, JWT isn’t a holy grail:
So when and where should you check the user’s credentials inside your app?
One solution would be to check the JWT or session content on every call to any of your functions. This gives you the most control over the authentication flow, but it is complicated to implement; you have to do everything yourself. It’s also not optimal from the database access standpoint.
Another solution that improves on some of these issues is using Custom Authorizers supported by API Gateway.
AWS Lambda offers a convenient way to perform authentication outside of your core functions. With API Gateway’s Custom Authorizers, you can specify a separate Lambda function that is only going to take care of authenticating your users.
In serverless.yml
, you can specify custom authorizers as follows:
functions:
create:
handler: posts.create
events:
- http:
path: posts/create
method: post
authorizer: authorizerFunc # execute this before posts.create!
authorizerFunc:
handler: handler.authorizerFunc
Custom Authorizers are currently only supported in joint use of Amazon API Gateway + Lambda. The Authorizer function has to return a policy of a specific shape. It’s a little inconvenient at first, but gets you access to a lot of flexibility.
Amazon provides a blueprint for implementing authorizer functions, which you can find right here.
You can also find a working implementation of an Authorizer function here in the Serverless Examples repo.
The best part: API Gateway will cache the resulting policy that gets returned by the Authorizer function for up to one hour. If the Custom Authorizer gets user information from, say, DynamoDB, this caching is going to reduce DynamoDB traffic significantly and improve the load times of your Serverless app’s endpoints. Nice.
Check out our documentation on using the Custom Authorizers with the Serverless Framework.
To manage users, you’ll need to create and delete them, as well as log them in and out. So the the big question is: should you manage users entirely yourself, or use a hosted service?
This basically requires a CRUD interface for your Users database, plus a login
method to generate a new JWT token or to create a session. Those can be implemented as separate functions.
I found this example to be very useful. The Register User function is simply:
// https://github.com/adnanrahic/a-crash-course-on-serverless-auth/blob/master/auth/AuthHandler.js
function register(eventBody) {
return checkIfInputIsValid(eventBody) // validate input
.then(() =>
User.findOne({ email: eventBody.email }) // check if user exists
)
.then(user =>
user
? Promise.reject(new Error('User with that email exists.'))
: bcrypt.hash(eventBody.password, 8) // hash the pass
)
.then(hash =>
User.create({ name: eventBody.name, email: eventBody.email, password: hash }) // create the new user
)
.then(user => ({ auth: true, token: signToken(user._id) })); // sign the token and send it back
}
Which then gets wrapped in a handler:
// https://github.com/adnanrahic/a-crash-course-on-serverless-auth/blob/master/auth/AuthHandler.js
module.exports.register = (event, context) => {
context.callbackWaitsForEmptyEventLoop = false;
return connectToDatabase()
.then(() =>
register(JSON.parse(event.body))
)
.then(session => ({
statusCode: 200,
body: JSON.stringify(session)
}))
.catch(err => ({
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message
}));
};
And specified in the serverless.yml
:
# https://github.com/adnanrahic/a-crash-course-on-serverless-auth/blob/master/serverless.yml
...
register:
handler: auth/AuthHandler.register
events:
- http:
path: register
method: post
cors: true
...
You can find the full example in this GitHub repo.
Services like Auth0 and Amazon Cognito handle creating users, logging them in, and storing sessions. If your goal is to allow users to log in with their social accounts or their corporate SAML identities, this is especially useful.
With Auth0, your app's frontend gets a JS element via the Auth0 SDK that displays a nice-looking login window, as in the example here:
// https://github.com/serverless/examples/blob/master/aws-node-auth0-custom-authorizers-api/frontend/app.js
const lock = new Auth0Lock(AUTH0_CLIENT_ID, AUTH0_DOMAIN, {
auth: {
params: {
scope: 'openid email',
},
responseType: 'token id_token',
},
});
And then your Authorizer function will check the user's token using the Auth0 public key:
// https://github.com/serverless/examples/blob/master/aws-node-auth0-custom-authorizers-api/handler.js
...
try {
jwt.verify(tokenValue, AUTH0_CLIENT_PUBLIC_KEY, options, (verifyError, decoded) => {
if (verifyError) {
// 401 Unauthorized
return callback('Unauthorized');
}
return callback(null, generatePolicy(decoded.sub, 'Allow', event.methodArn));
});
...
All without a need for you to maintain the Users database. Pretty slick.
We’re unfortunately still in the early stages of authentication for serverless.
While API Gateway provides a convenient way to implement authorization for Lambda functions (with, logically, more Lambda functions), other serverless compute providers don’t offer ways to conveniently authenticate users.
And even authorizer functions in Lambda have their issues, with fairly complex policies and caching limitations.
We are eager to see what solutions for authentication the serverless compute providers offer going forward, and are always happy to hear from you about how you’re handling authentication. Feel free to drop a comment, post in our forum, or hit us up on Twitter.
Jeremy Coffield is Platform Architect at Serverless, Inc.
Product