In the series of articles I will explain basics of Servlerless authorizers in Serverless Framework: where they can be used and how to write custom authorizers for Amazon API Gateway.
I am saying 'authorizers' but it is first of all about authentication mechanism. Authorization comes as second part.
Before we dive into details let's think for a moment what kind of authentication techniques are available.
- Basic
The most simple and very common is basic authentication where each request contains encoded username and password in request headers, e.g.:
GET /spec.html HTTP/1.1
Host: www.example.org
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
- Token in HTTP headers
An example of this kind of authentication is OAuth 2. and JWT. The API client needs to first call sign-in endpoint (unsecured) with username and password in the payload to obtain a token. This token is later passed in headers of subsequent secured API calls.
A good practice is to expire the token after some time and let the API client refresh it or sign in again to receive a new token.
GET /resource/1 HTTP/1.1
Host: example.com
Authorization: Bearer mF_9.B5f-4.1JqM
- Query Authentication with additional signature parameters.
In this kind of authentication a signature string is generated from plain API call and added to the URL parameters.
E.g. of such authentication is used by Amazon in AWS Signature Version 4
There are probably more variations of the above-mentioned techniques available, but you can get a general idea.
When to use which authentication mechanism?
The answer is as usual - it depends!
It depends if our application is a public REST API or maybe on-premises service which does not get exposed behind company virtual private network.
Sometimes it's also a balance between security and ease of use.
Let's take e.g. Amazon Signature 4 signed requests.
They are hard to create manually without using helpers API to sign requests (forget about Curl, which you could use easily with Basic and Token headers).
On the other hand, Amazon explains that these requests are secured against replay attacks (see more here).
If you are building an API for banking then it must be very secure, but for most of the non-mission-critical cases, Token headers should be fine.
So we have chosen authentication and authorization mechanism. Now, how do we implement it with AWS?
We can do our own user identity storage or use an existing one, which is Amazon IAM ( Identity and Access Management ).
The last one has this advantage, that we don't need to worry about secure storing of username and password in the database but rely on Amazon.
Custom REST Authorizer
Let's first look at a simple example of REST API authorized with a custom authorizer
Create a new SLS project
serverless create --template aws-nodejs --path serverless-authorizers
Add simple endpoint /hello/rest
The code is here (Note the commit ID).
The endpoint is completely insecure.
Deploy application
sls deploy -v function -f helloRest
When it deploys it will print endpoint URL, e.g.:
endpoints:
GET - https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest
Call endpoint from client
Using curl we can call it like that:
curl https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest
Secure endpoint with custom authorizer.
For the sake of simplicity, we will only compare the token with a hardcoded value in authorizer function.
In real case this value should be searched in the database. There should be another unsecured endpoint allowing to get the token value for username and password sent in the request.
Our authorizer will be defined in serverless.yml like this:
functions:
authorizerUser:
handler: authorizer.user
helloRest:
handler: helloRest.handler
events:
- http:
path: hello/rest
method: get
authorizer: ${self:custom.authorizer.users}
custom:
stage: ${opt:stage, self:provider.stage}
authorizer:
users:
name: authorizerUser
type: TOKEN
identitySource: method.request.header.Authorization
identityValidationExpression: Bearer (.*)
In http events section we defined authorizer as:
authorizer: ${self:custom.authorizer.users}
This will link to custom section where we defined authorizer with name authorizerUser
. This is actually the name of a function which we defined in functions
section as:
functions:
authorizerUser:
handler: authorizer.user
The handler
points to a file where authorizer handler function is defined by naming convention: authorizer.user
means file authoriser.js
with exported user
function.
The implementation will look as follows:
'use strict';
const generatePolicy = function(principalId, effect, resource) {
const authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
const policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
const statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
return authResponse;
};
module.exports.user = (event, context, callback) => {
// Get Token
if (typeof event.authorizationToken === 'undefined') {
if (process.env.DEBUG === 'true') {
console.log('AUTH: No token');
}
callback('Unauthorized');
}
const split = event.authorizationToken.split('Bearer');
if (split.length !== 2) {
if (process.env.DEBUG === 'true') {
console.log('AUTH: no token in Bearer');
}
callback('Unauthorized');
}
const token = split[1].trim();
/*
* extra custom authorization logic here: OAUTH, JWT ... etc
* search token in database and check if valid
* here for demo purpose we will just compare with hardcoded value
*/
switch (token.toLowerCase()) {
case "4674cc54-bd05-11e7-abc4-cec278b6b50a":
callback(null, generatePolicy('user123', 'Allow', event.methodArn));
break;
case "4674cc54-bd05-11e7-abc4-cec278b6b50b":
callback(null, generatePolicy('user123', 'Deny', event.methodArn));
break;
default:
callback('Unauthorized');
}
};
Authorizer function returns an Allow IAM policy on a specified method if the token value is 674cc54-bd05-11e7-abc4-cec278b6b50a
.
This permits a caller to invoke the specified method. The caller receives a 200 OK response.
The authorizer function returns a Deny policy against the specified method if the authorization token is 4674cc54-bd05-11e7-abc4-cec278b6b50b
.
If there is no token in the header or unrecognized token, it exits with HTTP code 401 'Unauthorized'.
Here is the complete source code (note the commit ID).
We can now test the endpoint with Curl:
curl https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest
{"message":"Unauthorized"}
curl -H "Authorization:Bearer 4674cc54-bd05-11e7-abc4-cec278b6b50b" https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest
{"Message":"User is not authorized to access this resource with an explicit deny"}
curl -H "Authorization:Bearer 4674cc54-bd05-11e7-abc4-cec278b6b50a" https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest
{"message":"Hello REST, authenticated user: user123 !"}
More about custom authorizers in AWS docs
In the next series of Serverless Authorizers articles I will explain IAM Authorizer and how we can authorize GraphQL endpoints.
This article was initially posted at https://cloudly.tech which is my blog about Serverless technologies and Serverless Framework in particular.
Top comments (10)
I used the provided code and it works when deployed as well. Now I'm removing the "Bearer" string from the token. I've removed the code that looks for "Bearer" string from the code and removed "indentityValidationExpression" from .yaml as well. But the authorizer still only works with the "Bearer" string in the Header.
Also I'm trying to change the response from "unauthorized" to anything else in the callback. But it still comes back "unauthorized".
Are you sure you deployed full stack or single function only? Can you share your code on git?
I tried deploying just the authorizer as well as the whole stack. Still the same result. Turns out the authorizer in APIGW still have the "indentityValidationExpression" check set to Bearer (.*), even though I had removed it.
To be completely sure your app is OK you can try to delete the stack and sls tmp folder called
.serverless
from your project root and redeploy from fresh. If this is the case maybe it's a bug in sls. You're using latest version, right?yep I have the latest version .. I deleted the stack via "sls remove" but I'm still confused why the APIGW authorizer didn't update.
I'm still stuck at the authorizer, it times out or returns 500 whenever I try to match the token in my database. I'm using Sequelize and AWS RDS (MySQL). I can't give you my private repo, but I'll duplicate the code in a public repo.
It would be great if you could help! Thanks
github.com/hzburki/serverless
This is code repo. It's connected to a new database. Two routes /users and /user, an authorizer is connected to /user.
Works fine on serverless-offline, but both endpoints timeout when deployed to AWS. Even if I set timeout to 30sec.
Help Please !
thx, will try to have a look at it by the end of this week..possibly sooner.
I checked your code, added a couple of logs and changes.
I tested on AWS and it works.
You can check my code here: github.com/piczmar/sls-test-author...
I'm not sure what was your problem. I can think of wrong DB connection details causing Sequilize to wait on connection. Can you make sure the correct env. variables are set on Lambda function?
Can you check my version and see if it helped?
I got the authorizer to work :D
The issue was with the principalId. I wanted to set the authenticated object as the principalId and add it in the request body, that way I would save an extra database query. Once I set the principalId to the token. The authorizer started working.
I have to query the authenticated user again in my controller, but I can live with that.
Thanks for your help.
Glad to hear that :)