Firebase and Fauna B2C Auth App in AWS Land
Tags: webdev, serverless, firebase, fauna
Published: 2021-12-14 05:34:11

<h2 id="introduction">Introduction</h2>

In the [previous article](https://dev.to/samiell/authentication-with-firebase-and-fauna-introduction-1nnh), we explored the authentication offerings of [Firebase](https://firebase.google.com/docs/auth) to turbocharge your next Fauna-powered application and introduce the building blocks to architect applications for the client-serverless paradigm. Now we will build a serverless authentication [business-to-consumer](https://smallbusiness.chron.com/explain-business-consumer-model-2258.html) (B2C) backend with services from the AWS ecosystem. But first, let's look at a few prerequisites needed to begin our journey:
<h3 id="requirements">Requirements</h3>
[](/#requirements)

* [Register for a Firebase account](https://console.firebase.google.com/project/_/overview?purchaseBillingPlan=free) if you don't have one and enjoy the [free tier](https://firebase.google.com/pricing). Payment information is not required to begin until you upgrade your plan.
* [Create a Fauna account](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah) and benefit from the[ free plan](https://fauna.com/pricing?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah) offerings. Payment information is not needed to get started.
* [Sign up for an AWS account](https://portal.aws.amazon.com/billing/signup) if you don't already have an account with these [instructions](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/). A payment method is required to create an account and access the AWS [free tier](https://aws.amazon.com/free/).

<h2 id="authentication-requirements-and-strategy">Authentication Requirements and Strategy</h2>


The client (frontend) application will communicate with our backend through an [API](https://en.wikipedia.org/wiki/API). AWS provides [API Gateway](https://aws.amazon.com/api-gateway/), which enables us to create, maintain, and secure our authentication API at any scale. API Gateway offers two [RESTful API](https://restfulapi.net/) options for our B2C authentication application - [HTTP](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [REST](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). Which option do we utilize for our business-to-consumer backend application? These two preliminary questions hold the clue to this puzzle:



* Where are the target users located?
* How do users access the authentication API?

This comprehensive [guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html) helps you decide between the two for your application needs. For this sample application, we are working with the following assumptions:



* Our users reside in a single continent.
* Our users will access the authentication API from our client application.

Hence, we utilize the HTTP API option, which provides low latency and cheap regional API to proxy our business logic for authentication. [AWS Lambda](https://aws.amazon.com/lambda/) will encapsulate this business logic. To make working with the various AWS services a breeze, we will use the [serverless framework](https://www.serverless.com/). Let's build this faunabase authentication application.
<center>![Serverless Authentication App in AWS ecosystem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jkl274s4g6ysqoguslw8.jpg)<span style="text-decoration:underline;"><u>Serverless Authentication App in AWS ecosystem</u></span></center>
<h2 id="faunabase-authentication-app">Faunabase Authentication App</h2>


We install the serverless framework and get a free dashboard account from this [guide](https://www.serverless.com/framework/docs/getting-started/#set-up-your-free-dashboard-account). If you don't have [Node](https://nodejs.org/en/download/package-manager/) installed, be sure to do that first. The recommended  [NodeJS version](https://nodejs.org/en/download/) for installation is the long-term support (LTS) version. Next, we create our faunabase-auth application using the AWS NodeJS template with the following command:

```javascript
serverless create --template aws-nodejs --path faunabase-auth
```

<center>or the shorthand version:</center>

```javascript
sls create -t aws-nodejs -p faunabase-auth
```

Open the _faunabase-auth_ folder with your text editor and you should see the results below:

<center>![faunabase-auth folder in code editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ptfs3tu97v48kbegs7ag.jpg)</center>
The command we run created two files: `serverless.yml` and `handler.js`. The `serverless.yml` file serves as the control panel to manage all our AWS infrastructure resources. Let's go through the default configuration and make a few customizations. 
<center>
![default serverless.yml file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8cg1o7i2d9pbw7hqna4r.jpg)</center>
We will change the provider runtime from `nodejs12.x` to the current LTS version, which is `nodejs14.x`. We add a region and stage to the provider and set the default stage to `dev` and the default region to `us-east-1` for this demo. Set this region to the location of your end-users. You should get the following results:

![serverless.yml use node14.X](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/95i4qiojuihf5lban8n4.jpg)
Let's clear all the comments to tidy up the `serverless.yml` file. Next, we set up our authentication API and integrate it with the business logic encapsulated in [Lambda](https://aws.amazon.com/lambda/) functions.

<h3 id="setting-up-firebase-project">Setting up Firebase project</h3>

Before we proceed to create our API, let's create a Firebase project and Fauna database. Open the [Firebase console](https://console.firebase.google.com) and click the `Create a project` button. 
<center>
![Firebase console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a0ruzh6orbl4owpomzz.jpg)
</center>
Enter `faunabase` for the project name and click continue.
<center>
![create firebase project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bwqnftv0hm6n9ainn5nb.jpg)
</center>
Click `Continue` and disable Google Analytics for this project. Finally, click on the `Create project` button to complete the process.
<center>
![Create firebase project 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dvpm8m0sjzukf3bh6png.jpg)
</center>
Click `Continue` to proceed to the project dashboard.
<center>
![create firebase project 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tk7q3fnh7lt0e8b6cmua.jpg)</center>
Click on the `Settings gear icon` button in the sidebar menu and select `Project settings`.
<center>
![firebase settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xwv6zmz29kexe24abow.jpg)</center>
Copy the Project ID for the project and save it. We need it later for the authentication API.
<center>
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7uk7wrooh1vxgmpy4b2g.jpg)</center>

<h3 id="creating-a-database-in-fauna">Creating a Database in Fauna</h3>

[Login to your Fauna Dashboard](https://dashboard.fauna.com/?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah), and click on the `Create Database` button to create a new database for this demo. 
<center>
![create fauna database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9zo7qhotlasovfeqm72.jpg)</center>
Type `faunabase-b2c` as the database name, and select the `United States` option for the region group. Click on the `Create` button to complete the process. 
<center>
![create fauna database 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0on2gk9nlvh56fdrmvca.jpg)</center>
We chose the United States region for data locality and better latency since our authentication API (AWS) is in the United States. Once the database is available, select the `Security` tab and choose `New Key` to create your first key. Accept the defaults of the current database and `Server` for the role. Enter AWS Serverless Framework as the `Key Name`, and choose `Save` to create a new key. 
<center>
![create fauna database 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j0dsel86foujk7c8chpn.jpg)</center>
Copy the generated secret. We are going to store it in the next step.
<center>
![create fauna database 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymc96tahz6v7ve6a7alr.jpg)</center>
<h3 id="securing-secret-with-aws-parameter-store">Securing Secret with AWS Parameter Store</h3>
The secret we generated in the previous step provides unlimited access to the newly created database. We have to store it safely and provide access to our authentication API logic. Hence, we use [Parameter Store](https://console.aws.amazon.com/systems-manager/parameters) to safeguard the secret using `SecureString` value, which encrypts the key accessed from the authentication API. Click on the `Create parameter` button.
<center>
![aws ssm](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4d7q5bimu7kptralqns.jpg)</center>
Parameter Store creates the `fauna-secret` parameter and displays it on the `My parameters` tab. We will access this secret from our authentication logic encapsulated in our [lambda functions](https://aws.amazon.com/lambda/).
<center>
![aws ssm 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w6uf9wo2vlwb1ymgjfb8.jpg)</center>
<h3 id="configure-aws-credentials-for-serverless-framework">Configure AWS Credentials for Serverless Framework</h3>
The Serverless Framework needs access to our AWS cloud account to create and manage resources on our behalf. Since we are using the serverless command-line interface (CLI) to set up our AWS credentials, we use an AWS profile and create an [IAM](https://console.aws.amazon.com/iamv2/home#/users) user for the serverless framework. Ensure that you are logged in to your AWS account to proceed. Click on `Users` and then `Add users`. Type `faunabase-serverless` for the User name and tick the `Access key` checkbox to enable programmatic access. Click `Next: Permissions` to go to the permissions page.
<center>
![aws credential](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5otcptgdjsyjnkncw8v.jpg)</center>
Click on `Attach existing policies directly`. Select `the AdministratorAccess` option in the list, then click `Next: Tags`.
<center>
![aws credential 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h53elesug6g5v0d86h9y.jpg)</center>
Add optional tags in the next step and select `Review` to complete the process. Check to make sure everything looks good and click `Create user`. View and copy the `Access Key ID` & `Secret Access Key` as we will use them in the next step to configure our serverless CLI.
<center>
![aws credential 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6y6z72jmxlw4eb18yb9.jpg)</center>
It is important to note that the above step grants unrestricted access to resources in the AWS account and makes starting our application easy. The recommended security practice is to provide fine-grained permissions through [IAM policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create.html) to match the scope of the application. Configure the serverless CLI with the following command using the copied values from the previous step:
```javascript
serverless config credentials --provider aws --key copied-access-key-id --secret copied-secret-access-key
```
Next, we proceed to set up API Gateway and integrate it with Lambda functions.

<h3 id="configuring-api-gateway">Configuring API Gateway</h3>
Our authentication API has two endpoints: signup and login. The client applications will use the Firebase client SDK to request an [ID token](https://firebase.google.com/docs/auth/admin/verify-id-tokens) - a [JSON Web Token](https://jwt.io/) (JWT). It will send this token to our authentication API, which will verify the integrity of the JWT before calling the business logic at the API endpoint. But first, let us do a little housekeeping in our `faunabase-auth` folder. Create a folder at the root location and name it `src`. Inside the `src` folder, we will create the following folders - `functions`, `libs`. 
<center>
![faunabase-auth folder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dl7toolv97hmkbdo9w7q.jpg)</center>
Delete the `handler.js` file. Next, let us set up our API in the `serverless.yml` file and connect it with the functions. In the [YAML](https://yaml.org/) file under `provider`, add the `apiName` property and give it a value of `auth-b2c-api`. This value is our API Gateway name.
<center>
![faunabase-auth folder 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oq4ipexeo112begxjca2.jpg)</center>
Delete the `hello` function with its `handler` property and add the `httpApi` property under `provider`. Next, set the `cors` and `metrics` properties to `true` under `httpApi`. We enabled [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) and [Cloudwatch](https://aws.amazon.com/cloudwatch/) metrics by setting `cors` and `metrics` to `true`. With CORS, we can specify which clients can access our API and what actions they can take. We also set up monitoring and alerts for our API with Cloudwatch. We enable logging for API Gateway - type `logs` under `provider`. Next, set `httpApi` under `logs` to `true`. Let's set up authorization logic in the authentication API. Firebase has an [OIDC](https://openid.net/connect/) discovery document that will help us verify users from our firebase project in our authentication API. First, we will need the firebase project ID from earlier when setting up our firebase account. Go to the [URL](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL) below and replace project-id with your firebase project ID.
```javascript
https://securetoken.google.com/project-id/.well-known/openid-configuration
```
In this demo, the project ID from the earlier is `faunabase-687bd`, so we visit the link below. 
<center>
![faunabase-auth folder 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/056hsfnwbhm8w5iyeyaz.jpg)</center>
Our firebase project can accept and verify ID tokens from the discovery document. In `serverless.yml`, create `authorizers` property under `httpApi`. Under `authorizers`, we name our authorizer as `firebaseB2CAuthorizer`. We have to set an identity source, issuer URL, and audience. Under `firebaseB2CAuthorizer`, set `identitySource` to `$request.header.Authorization`, `issuerUrl` to  `https://securetoken.google.com/project-id`, and `audience` to `project-id`. Remember to replace project-id with your Firebase project ID. Change the `frameworkVersion` value from `'2'` to `'>=2.66.1 &lt;4.0.0'` - serverless framework at the time of this writing is transitioning from version 2 to version 3 which is currently in beta. [Version 3](https://www.serverless.com/blog/serverless-framework-v3-beta) comes with breaking changes to the `serverless.yml` configuration. The current version of the stable serverless node library is `2.66.1`. The current code can upgrade to the version 3 package when it becomes stable.
<center>
![faunabase-auth folder 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/inzsv02600wuvub74tir.jpg)</center>
The frontend client must pass a valid Firebase ID token in the authorization header of the request. The identity source tells our authentication API where to find the ID token, a JWT to perform validation checks before providing access to the business logic encapsulated in a function. We have configured our authentication API. Now let's connect it to our two authentication endpoints - `signup` and `login`. We create the first endpoint for signing up. Navigate to the `src` folder --> then the `functions` folder. Create a `signup.js` and `login.js` file inside the functions folder.
<center>
![faunabase-auth folder 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tb88hqkh3961su2xy39c.jpg)</center>
Enter the code below in both the signup.js and login.js.
```javascript
 module.exports.handler = async (event, context) => {


 }
```
We have set up the boilerplate code for our two endpoints - `signup` and `login`. Let's link the business logic to the endpoints in `serverless.yml`, beginning with the `signup` endpoint. Enter `signup` under `functions`. Next, we write the following code under `signup:`
```javascript
functions:
  signup:
    handler: src/functions/signup.handler
    description: create user in fauna
    memorySize: 1048
    timeout: 15
    events:
      - httpApi:
          method: POST
          path: /signup          
          authorizer:
            name: firebaseB2CAuthorizer

```
* the `handler` property sets the source file location for the business logic
* the `description` property provides information about the business logic
* the `memorySize` property sets the memory size for this business logic in MB
* the `timeout` property determines how long the business logic is allowed to run before termination in seconds
* the `events` property shows which AWS events can trigger the business logic to execute
* the `httpApi` property under `events` tells us that the API Gateway [HTTP API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) event can run this business logic
* the `method` property under `httpApi` sets which HTTP method can access the API endpoint
* the `path` property under `httpApi` sets the pathname for the API endpoint
* Under `httpApi`, the `authorizer` property with `name`, its child property allows us to use the `firebaseB2CAuthorizer` we configured earlier to provide a security layer for the endpoint. Requests without a valid ID token from our Firebase project cannot execute the underlying business logic.

We have set up our signup endpoint for authentication API. Let's repeat the same process for our login endpoint. The final code configuration should look like this:
```javascript
functions:
  signup:
    handler: src/functions/signup.handler
    description: create user in fauna
    memorySize: 1048
    timeout: 15
    events:
      - httpApi:
          method: POST
          path: /signup          
          authorizer:
            name: firebaseB2CAuthorizer
  login:
    handler: src/functions/login.handler
    description: login user in fauna
    memorySize: 512
    timeout: 10
    events:
      - httpApi:
          method: POST
          path: /login          
          authorizer:
            name: firebaseB2CAuthorizer
```
We have to make the database secret in Fauna available to the `signup` and `login` functions from the AWS Parameter Store. The serverless framework provides access to plugins. We will install the Serverless SSM Fetch plugin to help us retrieve the stored fauna secret in AWS. Run the command below to install the plugin as an [npm](https://www.npmjs.com/) package:
```javascript
  serverless plugin install --name serverless-ssm-fetch
```
Add the installed plugin to the serverless.yml file with the following code at the root:
```javascript
plugins:
  - serverless-ssm-fetch
```
Next, add a custom variable called `FAUNA_SECRET` as a property of `custom.serverlessSsmFetch` with this code:
```javascript
custom:
  serverlessSsmFetch:
    FAUNA_SECRET: fauna-secret~true
```
The `serverlessSsmFetch` is an accessor provided by the plugin we installed, which decrypts the encrypted fauna-secret parameter with the  `~true` flag and automatically injects the AWS Parameter Store Fauna server key as an environment variable, accessible in all our functions. Add the `logRetentionInDays` property under `provider` and set the number of days to retain the Cloudwatch logs we set up for the authentication API. We proceed to code the business logic for signup and login. The current `serverless.yml` file should have the following results:
```javascript
service: faunabase-auth
frameworkVersion: '>=2.66.1 <4.0.0'

provider:
  name: aws
  runtime: nodejs14.x
  apiName: auth-b2c-api
  httpApi:
    cors: true
    metrics: true
    authorizers:
      firebaseB2CAuthorizer:
        identitySource: $request.header.Authorization
        issueUrl: https://securetoken.google.com/faunabase-687bd
        audience: faunabase-687bd
  logs:
    httpApi: true
  logRetentionDays: 14
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}
  lambdaHashingVersion: 20201221

plugins:
  - serverless-ssm-fetch

custom:
  serverlessSsmFetch:
    FAUNA_SECRET: fauna-secret~true

functions:
  signup:
    handler: src/functions/signup.handler
    description: create user in fauna
    memorySize: 1048
    timeout: 15
    events:
      - httpApi:
          method: POST
          path: /signup          
          authorizer:
            name: firebaseB2CAuthorizer
  login:
    handler: src/functions/login.handler
    description: login user in fauna
    memorySize: 512
    timeout: 10
    events:
      - httpApi:
          method: POST
          path: /login          
          authorizer:
            name: firebaseB2CAuthorizer
```
<h3 id="business-logic-in-lambda-functions">Business Logic in Lambda Functions</h3>
We need to create a collection in our database called `User` to house each user’s document. We access the database from our client application through [FQL](https://docs.fauna.com/fauna/current/api/fql/?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah) or [GraphQL](https://docs.fauna.com/fauna/current/api/graphql/?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah) in Fauna. FQL can access resources created with the GraphQL API but not the converse. We create a `main.gql` file which we will upload to bootstrap resources for our database in this demo. Inside the `src` folder, navigate to the `libs` folder and create a new folder with `fauna`. Create a folder named `schema` within the `fauna` folder. Create the `main.gql` file inside the `schema` folder and type the following code in the [schema](https://graphql.org/learn/schema/):
```javascript
type User {
  role: Role!
  email: String! @unique  
}

enum Role {
  ADMIN
  MEMBER
}

type Mutation {
  logoutUser: Boolean! @resolver(name: "logout_user")
}
```
In the code above, we started with creating a collection in our database using `type User` with the `role` and `email` attributes. We also set a uniqueness constraint on the email field to validate against duplicates using the[ `@uinque`](https://docs.fauna.com/fauna/current/api/graphql/directives/d_unique?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah) directive. The `role` attribute will allow us to implement fine-grained access control for the database using [User Defined Roles](https://docs.fauna.com/fauna/current/security/roles?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah). The `logoutUser` mutation allows our client application to revoke the access token from the frontend. We will implement the logout logic in the `logout_user` [User Defined Function](https://docs.fauna.com/fauna/current/api/graphql/functions?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah) (UDF).

On the [Fauna dashboard](https://dashboard.fauna.com/?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah), Select the `faunabase-b2c` database and click the `GraphQL` tab. Click on the `IMPORT SCHEMA` button to upload the schema we created.
<center>
![faunabase-auth folder 6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bnmlqz7vr64nsfc8vfvs.jpg)</center>
If successful, you should obtain the following results:
<center>
![faunabase-auth folder 7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sgdv6vooc72b9inty9jr.jpg)</center>
Click on `Functions` located to the left of your Fauna dashboard. We will update the logic for the `logout_user` which was created when we uploaded our graphql schema.
<center>
![faunabase-auth folder 8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3nlna0liaz5jlc6gwbgz.jpg)</center>
Clear the contents of `Function Body` and replace it with the following code:
```javascript
Query(
  Lambda(
    [],
    Logout(true)
  )
)
```
This logout logic revokes all authenticated token access for the current user. The client application can access this functionality from the GraphQL API. Click on the `SAVE` button to update the logout logic.
<center>
![faunabase-auth folder 9](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4i6eeul9ekt70xt7lrnf.jpg)</center>
<h4 id="setting-up-libraries">Setting up Libraries</h4>
Next, let’s install the `fauna` driver with this command inside our codebase:
```javascript
  npm install --save faunadb
```
Create the `firebase` folder inside the `libs` folder in addition to the `fauna` folder. Also, create a `util.js` file inside the `libs` folder. The `fauna` folder will house all code about our Fauna database. The `firebase` folder will house functionality on Firebase authentication. The `util.js` file will contain all reusable utility functions which we will use in the login and signup endpoints. Paste the following code inside `util.js`:
```javascript
const response = (code, message) => {
  return JSON.stringify({
    statusCode: code,
    body: message
  })
}

module.exports = { response }
```
The `response` function accepts an [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP#:~:text=Hypertext%20Transfer%20Protocol%20(HTTP)%20is,be%20used%20for%20other%20purposes.) code and message and returns the response to the client application requesting the authentication API endpoint. The client application will send a request to API Gateway which will validate the JWT in the authorization header and call the AWS Lambda function. Inside the `firebase` folder, create an `index.js` which will contain logic to decode the JWT. Run the following code to install the `jwt-decode` node library:
```javascript
 npm install jwt-decode
```
Enter the following code in the `index.js`:
```javascript
const decode = require('jwt-decode')

const decodeJWT = (jwt) => {
  return decode(jwt)
}

module.exports = { decodeJWT }
```
The `decodeJWT` function accepts a JWT as input and decodes the payload. Inside the `fauna` folder, create a `function.js` file and paste the following code:
```javascript
const {
  Create, Tokens, Ref, Match, Now,
  Collection, Select, Get, Index, TimeAdd
} = require('faunadb').query

const loginUDF = (email) => {
  return Create(
    Tokens(), {
      instance: Ref(
        Collection('User'),
        Select(
          ["ref", "id"],
          Get(
            Match(
              Index('unique_User_email'),
              email
            )
          )
        )
      ),
      ttl: TimeAdd(Now(), 1, 'hour')
    }
  )
}

const registerUDF = (email) => {
  return Create(
    Collection("User"),
    {
      data: {
        email,
        role: "MEMBER"
      }
    }
  )
}

module.exports = { loginUDF, registerUDF }
```
This file contains the `signup` and `login` business logic. The `loginUDF` exchanges the decoded Firebase JWT token for an access token to the Fauna database which is valid for one hour. The `registerUDF` creates a new user in the Fauna database with the decoded email input from the Firebase JWT and assigns a basic role of `MEMBER`. Finally, we export the `loginUDF` and `registerUDF` with `module.exports`. Next, we set up our Fauna client to query the database - create an `index.js` file and input the following code:
```javascript
const faunadb = require('faunadb') // import faunadb driver
const { loginUDF, registerUDF } = require('./function') // import UDFs
// create new fauna client
const client = new faunadb.Client({
  secret: process.env.FAUNA_SECRET
})
// get Fauna access token from verified and decoded JWT
const login = (email) => {
  return client.query(loginUDF(email))
}
// create new Fauna user from verified and decoded JWT
const register = (email) => {
  return client.query(registerUDF(email))
}

module.exports = { login, register }
```
The code above imports the Fauna driver as well as the user-defined functions (UDFs) we created in `function.js`. A new Fauna client is instantiated to perform the login and register operations in our database. Finally, we export the `login` and `register` functions for use in our API endpoints. Next, we finalize the `signup` and `login` lambdas in the `functions` folder.
<h4 id="signup-lambda">Signup Lambda</h4>
Open the `signup.js` file inside the `functions` folder. We import the `register` function, we just created from the index.js file with the following code:
```javascript
const { register } = require('../libs/fauna')
```
We also import Firebase JWT decode function with this code:
```javascript
const { decodeJWT } = require('../libs/firebase')
```
Next, we import the `response` function from the `util.js` file:
```javascript
const { response } = require('../libs/util')
```
The `handler` function accepts `event` and [`context`](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html) as inputs. The `event` input contains [`information`](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) such as the JWT token in the authorization header from the API invoker. The context input provides information on the invocation, function, and execution environment. Since AWS Lambda utilizes a pay-as-you-go model, we want to timeout our function immediately after receiving a response from our promise instead of waiting for the event loop to complete. We configure the runtime to send the response immediately with [`context.callbackWaitsForEmptyEventLoop`](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html) to `false` inside the `handler` function to obtain the following code:
```javascript
const { register } = require('../libs/fauna')
const { decodeJWT } = require('../libs/firebase')
const { response } = require('../libs/util')

module.exports.handler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false
}
```
 Add the following code after `context.callbackWaitsForEmptyEventLoop` inside the `handler` function:
```javascript
 try {
    const user = event.headers && event.headers.authorization
    ? decodeJWT(event.headers.authorization)
    : { email: null }


    if(!user.email) return response(401, "authorization header not found")
  } catch (error) {
     console.log("signup error", error.message)
  }
```
In the above code, we decode the JWT located in `event.headers.authorization` and store it in `user` if the authorization header exists, else we exit the function with a [401 HTTP response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401). Next, we implement the logic to create a new user in our Fauna database with the following code:
```javascript
 const dbResponse = await register(user.email).catch(
  error => {
   console.log("create fauna user error", error.message || error.description)
    return response(500, "Oops! Something went wrong from our end.")
  }
 )
 if (
   dbResponse && dbResponse.data
   && dbResponse.data.email === user.email
 ){
   return response(200, dbResponse.data)
 }
```
If the `register` function executes successfully, `dbResponse` data is sent to the invoker and the function exited immediately. However, if an error occurs during this process, the invoker is notified and the error logged to [AWS Cloudwatch Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html). The complete code for signup is shown below:
```javascript
const { register } = require('../libs/fauna')
const { decodeJWT } = require('../libs/firebase')
const { response } = require('../libs/util')
module.exports.handler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false
  try {
    const user = event.headers && event.headers.authorization
      ? decodeJWT(event.headers.authorization)
      : { email: null }
    if(!user.email) return response(401, "authorization header not found")

    const dbResponse = await register(user.email).catch(
     error => {
      console.log("create fauna user error", error.message || error.description)
      return response(500, "Oops! Something went wrong from our end.")
     }
    )
    if (
      dbResponse && dbResponse.data
      && dbResponse.data.email === user.email
    ) {
      return response(200, dbResponse.data)
    }
  } catch (error) {
    console.log("signup trycatch error", error.message)
  }
}
```
Now, we implement the _login_ lambda function.
<h4 id="login-lambda">Login Lambda</h4>
Open the `login.js` file inside the `functions` folder. Next, we import the `login` function from the `fauna` folder, `decodeJWT` from the `firebase` folder, and `response` function from `utils.js` all located in the `libs` folder. Set the `context.callbackWaitsForEmptyEventLoop` to `false` inside _handler_ like we did earlier in `signup.js`.The code should look like this:
```javascript
const { login } = require('../libs/fauna')
const { decodeJWT } = require('../libs/firebase')
const { response } = require('../libs/util')

module.exports.handler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop =  false
}
```
Enter the following code after `context.callbackWaitsForEmptyEventLoop` inside the `handler` function:
```javascript
try {
 const user = event.headers && event.headers.authorization
  ? decodeJWT(event.headers.authorization)
  : { email: null }
 if(!user.email) return response(401, "authorization header not found")
} catch (error) {
 console.log("login trycatch error", error.message)
}
```
Likewise in the `signup` lambda, we decode the JWT located in the `event.headers.authorization` and store it in `user` if the authorization header exists, else we exit the function with a [401 HTTP response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401). Next, we implement the login logic to create a user access token from the verified and  decoded email with the code below inside the `trycatch` block:
```javascript
 const dbResponse = await login(user.email).catch(
  error => {
   console.log("create fauna token error", error.message || error.description)
   return response(500, "Oops! Something went wrong from our end.")
  }
 )
 if (
   dbResponse && dbResponse.data
   && dbResponse.data.email === user.email
 ) {
      return response(200, dbResponse.data)
 }
```
On successful execution of the `login` function, `dbResponse` data is sent to the invoker and the function exited immediately. However, if an error occurs during this process, the invoker is notified and the error logged to [AWS Cloudwatch Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html). The complete code for `login` is shown below:
```javascript
 const { login } = require('../libs/fauna')
 const { decodeJWT } = require('../libs/firebase')
 const { response } = require('../libs/util')

 module.exports.handler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop =  false
  try {
   const user = event.headers && event.headers.authorization
    ? decodeJWT(event.headers.authorization)
    : { email: null }
   if(!user.email) return response(401, "authorization header not found")
   const dbResponse = await login(user.email).catch(
    error => {
     console.log("create fauna token error", error.message || error.description)
     return response(500, "Oops! Something went wrong from our end.")
    }
   )

   if (
      dbResponse && dbResponse.data
      && dbResponse.data.email === user.email
    ) {
      return response(200, dbResponse.data)
    }

  } catch (error) {
    console.log("login trycatch error", error.message)
  }
}
```
We have completed the `signup` and `login` lambda functions. Next, we look at the process to deploy the code to the AWS Cloud Service.
<h3 id="deploy-api-to-aws">Deploy API to AWS</h3>
We are in the last mile of building our application and deploying it to the AWS cloud. Serverless Framework packages all node libraries and their dependencies which can result in very bloated lambda functions. Lambda functions in general are plagued by [cold-starts](https://aws.amazon.com/blogs/compute/operating-lambda-performance-optimization-part-1/) which is the time taken to prepare the execution environment. This time scales with the code size. Therefore, we can reduce the cold-start time with [webpack bundling](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/webpack.html), importing only the code needed for each function. The overarching benefit is that end-users get a better user experience with reduced latencies. The [serverless-webpack](https://www.serverless.com/plugins/serverless-webpack) plugin provides an option to achieve this optimization. 
<h4 id="webpack-bundle-setup">Webpack Bundle Setup</h4>
Install the `serverless-webpack` plugin, and `webpack`, as `[devDependencies](https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file)` with this code:
```javascript
 npm install --save-dev serverless-webpack webpack
```
Go to the `serverless.yml` file at the root of the `faunabase-auth` folder and add the `serverless-webpack` plugin under `plugins` after `serverless-ssm-fetch`.
Next, we add the webpack configuration file to `serverless.yml`. Under the `custom:` property add the code below:
```javascript
webpack:
    webpackConfig: 'webpack.config.js'
```
The `serverless.yml` file should be similar to the code below:
<center>
![faunabase-auth folder 10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btpo4hzesn99o7cmffjv.jpg)</center>
We exclude any GraphQL files (.gql) from the bundle with the `excludeFiles` property under `webpack:` in our `src` folder.
```javascript
webpack:
    webpackConfig: 'webpack.config.js'
    excludeFiles: src/**/*.gql
```
After the `plugins` property at the root of the `serverless.yml` file add the code below to tell webpack to package our functions individually:
```javascript
package:
  individually: true
```
The final `serverless.yml` file should have the following code:
```javascript
service: faunabase-auth
frameworkVersion: '>=2.66.1 <4.0.0'

provider:
  name: aws
  runtime: nodejs14.x
  apiName: auth-b2c-api
  httpApi:
    cors: true
    metrics: true
    authorizers:
      firebaseB2CAuthorizer:
        identitySource: $request.header.Authorization
        issueUrl: https://securetoken.google.com/faunabase-687bd
        audience: faunabase-687bd
  logs:
    httpApi: true
  logRetentionDays: 14
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}
  lambdaHashingVersion: 20201221

plugins:
  - serverless-ssm-fetch
  - serverless-webpack

package:
  individually: true

custom:
  serverlessSsmFetch:
    FAUNA_SECRET: fauna-secret~true
  webpack:
    webpackConfig: 'webpack.config.js'
    excludeFiles: src/**/*.gql

functions:
  signup:
    handler: src/functions/signup.handler
    description: create user in fauna
    memorySize: 1048
    timeout: 15
    events:
      - httpApi:
          method: POST
          path: /signup          
          authorizer:
            name: firebaseB2CAuthorizer
  login:
    handler: src/functions/login.handler
    description: login user in fauna
    memorySize: 512
    timeout: 10
    events:
      - httpApi:
          method: POST
          path: /login          
          authorizer:
            name: firebaseB2CAuthorizer




```
Create a `webpack.config.js` file in the root directory and import the `serverless-webpack` library:
```javascript
const slsw = require('serverless-webpack')
```
The file we created will house all our code bundling configurations for deployment. Export an empty object with `module.exports`:
```javascript
const slsw = require('serverless-webpack')

module.exports = {


}
```
Inside the empty object, we set the `target` property to `node`. At build time, the plugin can automatically determine the correct entry point for each lambda function. We set the `entry` to use `slsw.lib.entries` inside the exported object. Finally, we set `mode` to run in development mode if we are running locally else production mode with:
```javascript
 slsw.lib.webpack.isLocal ? 'development' : 'production'
```
 The final webpack configuration file should be similar to this result:
```javascript
const slsw = require('serverless-webpack')
const nodeExternals = require('webpack-node-externals')

module.exports = {
  target: 'node',
  entry: slsw.lib.entries,
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production'
}
```
<h4 id="serverless-deploy-to-aws-cloud">Serverless Deploy to AWS Cloud</h4>
We run the `serverless deploy` command to deploy all artifacts to the AWS cloud with the default stage and region:
```javascript
 serverless deploy
```
<h2 id="summary">Summary</h2>
We have been able to build a serverless authentication API for a business-to-consumer application. The frontend or client application can access the signup endpoint to create a new user and the login endpoint to exchange a Firebase JWT for an access token to the Fauna database to facilitate direct communication. It is important to secure the access token with a [user-defined role (UDR)](https://docs.fauna.com/fauna/current/security/roles.html?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_FirebaseFaunaAuth_SDanquah). We will do that in a future article in this series. In the next article, we explore building a serverless authentication API for a multi-tenant business-to-business application.