Serverless Login with OpenJS Architect, Part 1

pchinjr

Paul Chin Jr.

Posted on October 29, 2020

Serverless Login with OpenJS Architect, Part 1

I wanted to learn how to build a login from scratch using only serverless functions to gain some understanding of what might happen underneath the various third-party libraries that also provide authentication and authorization.

I have chosen to use OpenJS Architect for organizing our serverless functions and Begin for the CI/CD. All you will need is a free GitHub account and Node.js to follow along. Begin takes care of deploying to live infrastructure without needing your own AWS account.

Serverless architecture

Our entire application will be comprised of individual functions that are triggered by HTTP GET and POST calls through API Gateway. The AWS API Gateway service is created for you with an Architect project when you declare @http routes in the app.arc manifest file. More on that file later.

  • The GET routes are the server-rendered views.
  • The POST routes will be our backend logic that operates on the database.

Each Begin app also has access to DynamoDB through @begin/data, a DynamoDB client.

Getting started

The first step is to click the button to deploy a Hello World app to live infrastructure with Begin.

Deploy to Begin

Underneath, Begin will create a new GitHub repo to your account that you can clone to work on locally. Each push to your default branch will trigger a new build and deploy to the staging environment. Your CI/CD is already complete!!

When your app deploys, clone the repo, and install the dependencies.

git clone https://github.com/username/begin-app-project-name.git
cd begin-app-project-name
npm install
Enter fullscreen mode Exit fullscreen mode

The index function

Every function we write is independent with its own dependencies and request/response lifecycle. This means that our entire application is decoupled and enjoys the benefits of individual scaling as well as security isolation.

The index function is the entry point of our app that is loaded when the user makes a GET request to /.

The app is composed of just routes that correspond to an AWS Lambda Function. The first step is to create our get-index function.

// src/http/get-index/index.js
let arc = require('@architect/functions')
let layout = require('@architect/views/layout')

exports.handler = arc.http.async(index)

async function index(req) {
  return {
    html: layout({
      account: req.session.account,
      body: '<p>Please log in or register for a new account</p>'
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we will have to create our layout file in /src/views/layout.js. This layout file will be copied to each GET function's node_modules folder, so we can access it as a dependency to the Lambda function.

// src/views/layout.js
module.exports = function layout(params) {

  let logout = `<a href=/logout>Logout</a> | <a href=/admin>Admin</a>`

  let notAuthed = `<a href=/login>Login</a> | <a href=/register>Register</a> | <a href=/reset>Reset Password</a>`

  return `
  <!doctype html>
  </html>
  <h1> My Login </h1>
  ${ params.account ? logout: notAuthed}
  ${ params.body}
  </html>
  `
}
Enter fullscreen mode Exit fullscreen mode

Then we need to install @architect/functions to our function folder so that we can use the runtime helpers for forming our response.

cd src/http/get-index
npm init -y
npm i @architect/functions
Enter fullscreen mode Exit fullscreen mode

IAC and the app.arc file

Next we can create a get-register and post-register function. Start by adding these routes to our app.arc file. The app.arc file is a declarative manifest that Architect uses to deploy our entire app infrastructure. At this point your file should look like this:

@app
login-flow

@http
get /
get /register
post /register

@tables
data
  scopeID *String
  dataID **String
  ttl TTL
Enter fullscreen mode Exit fullscreen mode

get-register function

This function is responsible for returning an HTML string with the layout and an HTML form for sending data to the backend. Then we'll create the corresponding post-register function to handle the login and password data. We'll also need to install @architect/functions to help form the response.

Alt Text

// src/http/get-register/index.js
let arc = require('@architect/functions')
let layout = require('@architect/views/layout')

exports.handler = arc.http.async(register)

let form = `
  <form action=/register method=post>
  Sign Up Now!
  <input name=email type=email placeholder="add your email" required>
  <input name=password type=password required>
  <button>Register</button>
`

async function register(req) {
  return {
    html: layout({
      account: req.session.account,
      body: form
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

The post-register function is responsible for salting the incoming password and saving it to the database. We can keep things simple by making POST functions simply return a location that brings users to the next part of our app. In this case, we will return them to a restricted route after they register. post-register also needs to install @architect/functions, @begin/data, and bcryptjs.

// src/http/post-register/index.js

let arc = require('@architect/functions')
let data = require('@begin/data')
let bcrypt = require('bcryptjs')

exports.handler = arc.http.async(valid, register)

// check to see if account exists
async function valid(req) {
  let result = await data.get({
    table: 'accounts',
    key: req.body.email
  })
  if(result) {
    return {
      location: `/?error=exists`
    }
  }
}

async function register(req) {
  // salt the password and generate a hash
  let salt = bcrypt.genSaltSync(10)
  let hash = bcrypt.hashSync(req.body.password, salt)

  //save hash and email account to db
  let result = await data.set({
    table: 'accounts',
    key: req.body.email,
    password: hash
  })

  return {
    session: {
      account: {
        email: req.body.email
      }
    },
    location: '/admin'
  }
}
Enter fullscreen mode Exit fullscreen mode

Push changes to deploy!

All that's left now is to commit and push your changes to your default branch. Once that happens a staging build will be available from your Begin console.

Check out the next part where we finish the restricted get-admin route and create a logout function.

💖 💪 🙅 🚩
pchinjr
Paul Chin Jr.

Posted on October 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related