Serverless Course - Lesson 2: How to build AWS authentication

Duomly - Sep 7 '20 - - Dev Community

This article was originally published at:
https://www.blog.duomly.com/lesson-2-serverless-authentication/


Intro to serverless authentication

In today’s lesson, you will learn Serverless authentication.

Did you already finish the „What is Serverless” episode?

If not, you can find it here:

What is serverless

Next, you should do the Serverless course's first lesson, which was „Serverless How to Start”.

You can find it here:

Lesson 1: Serverless how to get started tutorial for beginners

If you finished them before, you can focus on today's lesson and learn new, essential skills.

We will have a bit more coding because we will work on building Serverless authentication, with all of the needed features.

You’ll learn how to build MongoDB models, register with serverless, how to do login with MongoDB, and how to verify JWT token in serverless.

Let’s start!

And if you prefer video, here you can find the youtube version:

How to setup MongoDB connection in Serverless

As the first that we should do is the database connection setup.

To do that, we need to create a file named „database.js”, and create a module named „db” containing logic responsible for the database connection.

const mongoose = require('mongoose');

let connection;

module.exports = db = () => {
  if(connection) {
    return Promise.resolve();
  }

  return mongoose.connect(process.env.DB)
    .then(database=>{
      connection = database.connections[0].readyState;
    });
}

Add verify-jwt to serverless.yml

We will create functions, specify handlers, and API paths for today's logic in the next steps.

Go to the serverless.yml, and start from creating a function named „verify-jwt” and pass a path to the handler's file.

functions:
  verify-jwt:
    handler: authentication/VerifyJWT.verify

Add login to serverless.yml

Next, we should create a function named „login”.
Pass the path to the node.js module and setup that as the HTTP endpoint with post method.
Setup API path as „auth/login”.

login:
  handler: authentication/AuthenticationHandler.login
  events:
    - http:
        path: auth/login
        method: post
        cors: true

Add register to serverless.yml

Similar to the login one, here we should create a function named „register”.

register:
  handler: authentication/AuthenticationHandler.register
  events:
    - http:
        path: auth/register
        method: post
        cors: true

Add myProfile to serverless.yml

And the last step in the serverless.yml should be the function named „myProfile”.
There will be two differences.

The first is that our API method is getting.

And the second one is, we need to use „verify-jwt” as the authorizer.

myProfile:
  handler: user/UserHandler.myProfile
  events:
    - http:
        path: user/myprofile
        method: get
        cors: true
        authorizer: verify-jwt

How to create MongoDB model

Our Serverless is ready, congratulations!

Now we can go into the User.js in the user directory and create the MongoDB model there.

Our User model should contain a few fields.

The first should be the name a string, email as a string, password as a string and premium as boolean, and premiumEnds as a date.

Name all the schema as User and export as a mongoose module.

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({  
  name: String,
  email: String,
  password: String,
  premium: Boolean,
  premiumEnds: Date,
});
mongoose.model('User', UserSchema);

module.exports = mongoose.model('User');

Create authenticationHandler

In this step, we just need to create a directory named „authentication” and create a file named „AuthenticationHandler.js” inside it.

Create authenticationHelpers

Similar to the previous step, here we should just create a file named „AuthenticationHelpers.js”, 

Create signJWT in authenticationHelpers

Now, we can come into coding a bit more.

Let’s go into the AuthenticationHelpers.js, and create an exported module with the name „signJWT”.

Inside the module, just return the jwt sign, and pass id inside the token.

module.exports.signJWT = (id) => {
  return jwt.sign({ id: id }, process.env.JWT_SECRET, {expiresIn: 604800});
}

Create registration validation in authenticationHelpers

In the next step, we should creat the next module.

This one should be named „validRegistration”, and contain logic that will ensure us our call’s body contains all required data.

In this case, we need to verify if there is a password and is longer than 6 chars.
And we should check if we have an email. If something is missing, we should return Promise.reject with a message.

module.exports.validRegistration = (body) => {
  if (!body.password || body.password.length <= 6) {
    return Promise.reject(new Error('Password need to be at least 7 characters.'));
  }
  if (!body.email) {
    return Promise.reject(new Error('Email is required'));
  }
  return Promise.resolve();
}

How to compare password in Node.js

Now, we should create the next module, this one should be „verifyPassword”, and is crucial in the authentication process.

Inside the function, we should compare two passwords, the sent one by API and the database's real password.

To do that, we should use the method „bcrypt.compare”, and return Promise.reject if password is incorrect.

module.exports.verifyPassword = (sentPassword, realPassword, userId) => {
  return bcrypt.compare(sentPassword, realPassword)
    .then(valid => !valid ? Promise.reject(new Error('Incorrect password.')) : this.signJWT(userId)
  );
}

Create register function and helper in authenticationHandler

We should move into the AuthenticationHandler.js file and create the exported module „register”.

That module should take two args, the first one is „r” as request, and the second is „cb”, as a callback.

On the first step, we should set up a callback, actually „cb.callbackWaitsForEmptyEventLoop” as false to send the response when the callback fires.

Next, we should return the DB connection and a few then.

The first one should pass the parsed body into the register function, the next should create a response, and the last should handle the error.

Next, we should create a function „register”, and take „body” as param.

Inside the register, we should valid our input by the „validRegistration” function.

Next, we should check if a user doesn't exist. If it exists, we should return the error msg. If not, we should bcrypt our password and create a user.

As the last step, we should return signed JWT to the user.

module.exports.register = (r, cb) => {
  cb.callbackWaitsForEmptyEventLoop = false;
  return db()
    .then(() => register(JSON.parse(r.body)))
    .then(res => success(res))
    .catch(err => errResponse(err));
};

function register(body) {
  return validRegistration(body)
    .then(() => User.findOne({ email: body.email }))
    .then(exists => exists ? Promise.reject(new Error('User exists')) : bcrypt.hash(body.password, 8))
    .then(hashedPass => User.create({ name: body.name, email: body.email, password: hashedPass, premium: false}))
    .then(user => ({ auth: true, token: signJWT(user._id) })); 
}

Create login function and helper in authenticationHandler

The login module looks almost the same as the register, just we change „register” to „login”.

But, the login helpers function is a bit different.

First, we need to check if the user at least exists. If no, we should return the error msg.

If it exists, we should call the verifyPassword function and return signedJWT if all is fine.

module.exports.login = (r, cb) => {
  cb.callbackWaitsForEmptyEventLoop = false;
  return db()
    .then(() => login(JSON.parse(r.body)))
    .then(res => success(res))
    .catch(err => errResponse(err));
};

function login(body) {
  return User.findOne({ email: body.email })
    .then(user => !user ? Promise.reject(new Error('Incorrect password or username')) : comparePassword(body.password, user.password, user._id))
    .then(signedJWT => ({ auth: true, token: signedJWT }));
}

Create VerifyJWT function (aws authorizer)

Now we can go into the VerifyJWT.js file and focus on the JWT tokens.
As the first step, we should create an exported module named „verify”.

That module should use jwt.verify function, and return an error if the token is not authorized.

If it's authorized, we should generate an AWS security policy, that will be returned as the callback.

To have the possibility of returning policy, we need to generate it first.

To do that, we should create a function „generatePolicy” that will take principalId, effect, and resource as params.

Next, we should verify if there are an effect and resource.

If yes, we can create an object with a policy that allows us to access API.

If you're interested in policies, explanation of the security policies we can find in the AWS Authorized documentation.

const jwt = require('jsonwebtoken');

module.exports.verify = (r, context, cb) => {
  const token = r.authorizationToken;
  if(token) {
    jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
      if (err)
        return cb(null, 'JWT not authorized');
      return cb(null, generatePolicy(decoded.id, 'Allow', r.methodArn))
    });
  } else {
    return cb(null, 'JWT not authorized');
  }
};

const generatePolicy = (principalId, effect, resource) => {
  const res = {};
  res.principalId = principalId;
  if (effect && resource) {
    const policyDocument = {
      Version: "2012-10-17",
      Statement: [
        {
          Action: "execute-api:Invoke",
          Effect: effect,
          Resource: resource
        }
      ]
    };
    res.policyDocument = policyDocument;
  }
  return res;
}

Create myProfile function and helper in userHandler

As the first step here, we should go into the „user" directory and create a file „UserHandler.js".

Next, we should open that file and create the exported module „myProfile".

Should look the same as the login or register function handler in AuthenticationHandler.js, but should be „myProfile” instead of „register” or „login”.

Next, create a simple function „myProfile” that will findById in the Model and return it.

const User = require('./User');
const { success, errResponse } = require('../authentication/AuthenticationHelpers');

module.exports.myProfile = (r, cb) => {
  cb.callbackWaitsForEmptyEventLoop = false;
  return db()
    .then(() => myProfile(r.requestContext.authorizer.principalId))
    .then(res => success(res))
    .catch(err => errResponse(err));
};

function myProfile(id) {
  return User.findById(id)
    .then(user => !user ? Promise.reject('User not found.') : user)
    .catch(err => Promise.reject(new Error(err)));
}

Test app (Authorization: token)

Woohoo, your authentication is ready!

Now, as the first, you need to start your application offline.

To do that, open your terminal, and type:

sls offline start —skipCacheInvalidation

Next, you can open a postman and call your API.

As the first call, you need to register the user by sending data into the POST:

http://localhost:3000/dev/auth/register

Next, you can use the GET method to take your profile.

To do that, you need to take token returned from registration, add it into the „Authorization” header, and call this endpoint:

http://localhost:3000/dev/myprofile

You should get your users to profile as the response.

Conclusion of serverless authentication

Congratulations, you’ve built Serverless authentication with Node.js.

Today you’ve learned a lot of things.

You’ve built a fully-working REST API, connected the MongoDB database, created first users in DB, and even built AWS security policies.

Here is today's episode's code:

https://github.com/Duomly/aws-serverlesss-nodejs/tree/serverless-course-lesson-2

I cannot wait until you join the next lesson to teach you how to build the next lesson's orders!

We will learn how to create orders, edit them, update statuses, delete, and what every proper SaaS application should contain.

Duomly promo code

Thanks for reading,
Radek from Duomly

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .