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.
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
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>'
})
}
}
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>
`
}
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
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
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.
// 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
.
// 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.
Top comments (0)