All Articles

Authorizer as a middleware in API Gateway via Node.js (Serverless Framework)

Overview

Authorizer provides security to Restful API. In this article, we’ll create Authorizer function which uploads to AWS Lambda Function and integrate with API gateway.

Prerequisite

  • AWS account
  • Severless Framwork

Authorizer Function

In this article, our purpose is to integrate authorizer with API gateway. For this, first we need to make our authorizer function and upload it to AWS Lambda Function. To authrize if your incoming token is verified or not.

Step 1. API to generate token

Skip this step, if you have already generated token.

If you are newbie of Serverless, then Let’s first walk through of API Gateway via Serverless Farmework which will give you basic idea of serverless project setup. Now, create one service which generate token using JWT (JSON Web Token).

Genreate token - We are taking an example of user who is already authenticated via Google. You can approach this setp to know How to authenticate from web sign-in. After validating Google SignIn from front side, we recieve access token generated by Google, which we’ll verify at our end. If received access token is not valid then we’ll send “Unauthorized” errror, so they won’t be able to access our application. Please look at the below code to get complete idea -

const GoogleAuth = require('google-auth-library');
const auth = new GoogleAuth;
const clientId = 'clientid';
const clientSecret = 'clientSecret';
const client = new auth.OAuth2(clientId, clientSecret, '');
const Oauth = require('../businesslogic/oauth');
const async = require('async');

exports.oauth = (event, context, callback) => {
      async.waterfall([
          (callback) => {
            client.verifyIdToken(
                JSON.parse(event.body).token,
                clientId,
                (e, login) => {
                    console.log(e, login)
                    if(e){
                        callback(e, null);
                    }
                    else{
                        let payload = login.getPayload();
                        callback(null, payload);
                    }
            });
        },
        (payloadData, callback) => {
            let oauth = new Oauth();
            oauth.getUser(payloadData['email'])
                    .then(data => {
                        if(data.Items.length > 0) callback(null, data.Items[0]);
                        else callback('Invalid User', null)
                    })
                    .catch(err => {
                        callback(err, null);
                    })
        },
        (user, callback) => {
            let oauth = new Oauth();
            oauth.generateToken(user.userId, user.districtId, user.role, (err, result) => {
                callback(err,result)
            })       
        }
    ], (err, result) {
        console.log("err", err, result)
        if(err){
             const response = {
                    statusCode: 500,
                    headers: {
                    "Access-Control-Allow-Origin": "*",
                    "Content-Type": "application/json "
                    },
                    body: JSON.stringify({
                    message: 'Invalid Login',
                    stack: err
                    })
                };
                callback(null, response);
                return;
        }
        else{
            const response = {
                statusCode: 200,
                headers: {
                "Access-Control-Allow-Origin" : "*",
                "Content-Type": "application/json "
                },
                body: JSON.stringify({accessToken:result})
            };
            callback(null, response);
            return;
        }
    })   
}

Linking to Github Repo, where you’ll get complete service of generate token in Serverless Framework.

Step 2. Upload Authorizer to AWS Lambda

Authorizer lambda function validates incoming token. We will check that the user has access rights for requested API. Here we have considered two roles “Admin” and “User”. Each role has differnet API to access. We decode the incoming token and decoded token will have some user information like role, userId and email so. Sigining-key will be same as which we have used while generating token. Based on the role user will be able to access API.

const jwt = require('jsonwebtoken');
const signingKey = "37LvDSm5XvjYOh9Y";
const BEARER_TOKEN_PATTERN = /^Bearer[ ]+([^ ]+)[ ]*$/i;

exports.handler = (event, context) => {
  console.log('Client token: ' + event.authorizationToken);
  console.log('Method ARN: ' + event.methodArn);
  let token = extract_access_token(event.authorizationToken);
  try {
    let decoded = jwt.verify(token, signingKey);
    if (decoded.role == "Admin") {
      let resource = methodArn.substring(0, methodArn.indexOf("/") + 1) + "*"
      let resourceList = ["arn:aws:execute-api:us-west-2:accountId:lwbctpkhk1/*"]
      let resouceIndex = resourceList.indexOf(resource)
      if (resouceIndex >= 0) {
        context.done(null, generatePolicy(decoded.email, 'Allow', event.methodArn, decoded))
      } else context.done(null, generatePolicy('user', 'Deny', methodArn));
    } else if (decoded.role == "User") {
      let resource = methodArn.substring(0, methodArn.indexOf("/") + 1) + "*"
      let resourceList = ["arn:aws:execute-api:us-west-2:accountId:s4kl2she14/*"]
      let resouceIndex = resourceList.indexOf(resource)
      if (resouceIndex >= 0) context.done(null, generatePolicy(decoded.email, 'Allow', event.methodArn, decoded))
      else context.done(null, generatePolicy('user', 'Deny', methodArn));
    } else context.done(null, generatePolicy('user', 'Deny', methodArn));
    
    context.done(null, generatePolicy(decoded.userId, 'Allow', event.methodArn, decoded));
  } catch(ex) {
    console.error(ex.name + ": " + ex.message);
    context.done(null, generatePolicy('user', 'Deny', event.methodArn));
  }
};

const generatePolicy = (principalId, effect, resource, decoded) => {
    let authResponse = {};
    authResponse.principalId = principalId;
    if (effect && resource) {
        let policyDocument = {};
        policyDocument.Version = '2012-10-17'; // default version
        policyDocument.Statement = [];
        let statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; // default action
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }
    if(decoded) authResponse.context = decoded;
    return authResponse;
};

const extract_access_token = (authorization) => {
  if (!authorization)
  {
    return null;
  }
  let result = BEARER_TOKEN_PATTERN.exec(authorization);
  if (!result)
  {
    return null;
  }
  return result[1];
};

Github link of authorizer Lambda Function.

When we successfully upload Function Package to AWS Lambda. It generate unique arn for Lambda Function. That, we’ll use as a middleware for all API.

Step 3. Use Authorizer as a Middleware

This way we’ll use authorizer as a middleware in serverless.yml file of service.

service: user-service

provider:
  name: aws
  profile: default
  role: ${self:custom.config.iamrole}
  runtime: nodejs6.10
  region: ${self:custom.config.region}
  stage: ${self:custom.config.stage}
  memorySize: 128
  timeout: 30
  versionFunctions: false
  environment:
    STAGE: ${self:custom.config.stage}
    TABLE_USERS: ${self:custom.config.stage}-${self:custom.config.tables.users.name}

functions:
  user-list:
    handler: functions/list.list
    events:
      - http:
          path: /
          method: get
          cors: true
          authorizer: ${self:custom.config.authFunction}

resources:
  Resources:
    GatewayResponseDefault4XX:
      Type: 'AWS::ApiGateway::GatewayResponse'
      Properties:
        ResponseParameters:
          gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
          gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
          gatewayresponse.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
        ResponseType: DEFAULT_4XX
        RestApiId:
          Ref: 'ApiGatewayRestApi'
custom:
  config: ${file(../config.yml)}

In the above code, we have added resource section also because whenever authorizer declines incoming request , it will send correct error code in response to API. Visit Github Link to get an overview of user service.

Conclusion

We have tried to cover steps from generating token to integrate authorizer with a service as a middleware, how authorizer works completely.

Please feel free to download complete source code.