Serverless Login with OpenJS Architect, Part 1
Tags: serverless, node, javascript, cloud
Published: 2020-10-29 14:37:33

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](https://arc.codes) for organizing our serverless functions and [Begin](https://begin.com) 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](https://static.begin.com/deploy-to-begin.svg)](https://begin.com/apps/create?template=https://github.com/begin-examples/node-hello-world)

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.
```bash
git clone https://github.com/username/begin-app-project-name.git
cd begin-app-project-name
npm install
```

## 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. 

```javascript
// 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>'
    })
  }
}
```
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. 
```javascript
// 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>
  `
}
```
Then we need to install @architect/functions to our function folder so that we can use the runtime helpers for forming our response.
```bash
cd src/http/get-index
npm init -y
npm i @architect/functions
```
## 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: 
```bash
@app
login-flow

@http
get /
get /register
post /register

@tables
data
  scopeID *String
  dataID **String
  ttl TTL
```
## `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](https://dev-to-uploads.s3.amazonaws.com/i/5qz9dh57o7gghi00vwas.png)

```javascript
// 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
    })
  }
}
```
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`.
```javascript
// 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'
  }
}
```
## 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.