January 9, 2024

Credentials User Authentication with HarperDB, Hono and Vercel

Welcome to Community Posts
Click below to read the full article.
Arrow
Summary of What to Expect
Table of Contents

In this post, I talk about how to set up credentials authentication with HarperDB, Hono and Vercel. We learn how to setup our HarperDB instance, configure credentials authentication helpers for HarperDB, create Hono Endpoints to sign up, sign in and create custom sessions for the users in a given app.

Tech Stack

  • Hono (Web Framework)
  • HarperDB (Database & Credentials Authentication Module)
  • Vercel (Hosting)

Prerequisites

Setting up the project

To set up, just clone the app repo and follow this tutorial to learn everything that's in it. To fork the project, run:

git clone https://github.com/rishi-raj-jain/harperdb-hono-vercel-credentials-auth
cd harperdb-hono-vercel-credentials-auth
npm install

Once you have cloned the repo, you are going to create a .env file. You are going to add the values we obtain from the steps below.

Setting up HarperDB

Let’s start by creating our database instance. Sign in to Harper Studio and click on Create New HarperDB Cloud Instance.

Fill in the database instance information, like here, we’ve added credentialsauth as the instance name with a username and password.

Go with the default instance setup for RAM and Storage Size while optimizing the Instance Region to be as close to your serverless functions region in Vercel.

After some time, you’d see the instance (here, credentialsauth) ready to have databases and its tables. The dashboard would look something like as below:

Let’s start by creating a database (here, list) inside which we’ll spin our storage table, make sure to click the check icon to successfully spin up the database.

Let’s start by creating a table (here, collection) with a hashing key (here, id) which will be the named primary key of the table. Make sure to click the check icon to successfully spin up the table.

Once done:

  1. Open lib/harper.ts and update the database and table values per the names given above.
  2. Click on config at the top right corner in the dashboard, and:
  3. Copy the Instance URL and save it as HARPER_DB_URL in your .env file
  4. Copy the Instance API Auth Header and save it as HARPER_AUTH_TOKEN in your .env file

Awesome, you’re good to go.

Configuring Credentials Authentication helpers for HarperDB for Vercel Edge and Middleware Compatibility

To interact with the HarperDB database, we’ll use HarperDB Security REST APIs called over fetch. This approach will help us opt out of any specific runtime requirements, and keep things simple and ready to deploy to Vercel Edge and Middleware.

Let’s create a global harperFetch function which will abstract the fetch related code from our Credentials Authentication helpers 👇🏻

// File: lib/harper.ts

export const harperFetch = (c: Context, body: { [k: string]: any }) => {
  const { HARPER_DB_URL, HARPER_AUTH_TOKEN } = env<{ HARPER_DB_URL: string | undefined; HARPER_AUTH_TOKEN: string | undefined }>(c, 'edge-light')

  if (!HARPER_DB_URL) throw new Error('No HARPER_DB_URL environment variable found.')
  if (!HARPER_AUTH_TOKEN) throw new Error('No HARPER_AUTH_TOKEN environment variable found.')

  const postBody: { [property: string]: any } = body

	// In case of add_role operation
	// Nothing but username and password is required
  if (postBody['operation'] !== 'add_role') {
    postBody['database'] = 'list'
    postBody['table'] = 'collection'
  }

  return fetch(HARPER_DB_URL, {
    method: 'POST',
    body: JSON.stringify(postBody),
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Basic ' + HARPER_AUTH_TOKEN,
    },
  })
}

Let’s configure the utility to read environment variables in Hono with Vercel Runtime 👇🏻
// File: lib/harper.ts

import { env } from 'hono/adapter'
import type { Context } from 'hono'

// Get a secret in your deployment
export const getEnv = (c: Context, name: string) => {
  const ENV = env(c, 'edge-light')
  return ENV[name]
}

Creating User Roles in HarperDB Instance

When creating a new, user-defined role in a HarperDB instance, one needs to provide a role name and the permissions to assign to that role. As we just require to authenticate users for our app, and not actually require authentication to read/write values from/to database, we’ll restrict all kind of permissions to a specific table in a HarperDB instance.

The role, table name, and associated permissions are configured through the function parameters of addRole, and the response includes the assigned role's unique identifier.

HarperDB takes care of ensuring that a specific role is created only once. Hence, even if the endpoint is called multiple times with the same field values, HarperDB would return error messages to indicate database conflicts.

// File: lib/harper.ts

import type { Context } from 'hono'

export const addRole = async (c: Context, role: string, tableName: string) => {
  const t = await harperFetch(c, {
    operation: 'add_role',
    // role of the users
    role,
    // permission for the users added
    // by default allow them nothing
    permission: {
      super_user: false,
      list: {
        tables: {
          [tableName]: {
            read: false,
            insert: false,
            update: false,
            delete: false,
            attribute_permissions: [],
          },
        },
      },
    },
  })
  // the response would contain the "id" attribute
  return await t.json()
}

Sign Up Users via HarperDB

To let user sign up into our app, we’d use the add_user HarperDB operation to create a new user in the HarperDB instance, specifying the username, password, role, and optional active status. The response includes a message attribute indicating the success of the user creation process.

HarperDB takes care of implementing user credentials collisions and enforce value validations. This allows you to effortlessly pass on the username and password for signing up users without validating the field values on your own.

export const createUser = async (c: Context, username: string, password: string, role: string, active: boolean = true) => {
  const t = await harperFetch(c, {
    operation: 'add_user',
    username,
    password,
    active,
    role,
  })
  // the response would contain the "message" attribute which'd contain succesfully
  return await t.json()
}

Sign In Users via HarperDB

To let user sign in into our app, we’d use the create_authentication_tokens HarperDB operation to create tokens via the HarperDB instance, specifying the username and password of the user. A successful response includes an operation_token attribute indicating the correct credentials of the user.

With HarperDB, the create_authentication_tokens operation can be used at any time to refresh the operation_token in the event that it expired or was lost.

export const getUser = async (c: Context, username: string, password: string) => {
  const t = await harperFetch(c, {
    operation: 'create_authentication_tokens',
    username,
    password,
  })
  // the response would contain the "operation_token" attribute
  return await t.json()
}

Set up Hono with Vercel Edge

Let's kick things off by setting up a Hono app with the Vercel template. It’s pretty similar to creating an Express app 👇🏻

// File: api/index.ts

import { Hono } from 'hono'

// Import the request and
// response handler for Vercel by Hono
import { handle } from 'hono/vercel'

// Run it on the Vercel edge
export const config = {
  runtime: 'edge',
}

const app = new Hono()

// App Routes
app.get('/', async (c) => {
	// Process Request
	// Return with c.json() for JSON responses
})

export default handle(app)

Now, let’s configure Hono endpoints for each operation i.e. to sign up, sign in and create custom sessions for the users 👇🏻

An Endpoint To Create User Roles Dynamically

We’ll establish an endpoint responsible for dynamically creating user roles in HarperDB. The route /create/user/roles expects a JSON payload containing roleName and tableName, validates the input fields, and utilizes the addRole function we created earlier to add the specified role with associated permissions to the designated table.

// File: api/index.ts

import { addRole } from '../lib/harper'

// If you're getting started with deploying your auth
// Do this for the very first time to be able to
// Create custom user login(s) from here on
app.post('/create/user/roles', async (c) => {
  const { roleName, tableName } = await c.req.json()
  // Validation of the body fields
  if (!roleName || !tableName) return c.json({ error: 'Invalid role creation.' })
  // Create a new kind user roles
  const newRoleResponse = await addRole(c, roleName, tableName)
  return c.json(newRoleResponse)
})

Below a sample response from this particular endpoint 👇

An Endpoint To Sign Up Users

We’ll establish an endpoint responsible for user sign-ups in HarperDB through the createUser function we created earlier. The route /create/user expects a JSON payload containing username, password, and roleName. It performs input validation, and creates a new user with the specified credentials. Upon successful user creation, it extracts the operation token and, if applicable, any error messages. Additionally, we set a signed cookie (custom_auth) with server-side operations and secure configurations for authentication.

// File: api/index.ts

import { setSignedCookie } from 'hono/cookie'
import { createUser, getEnv } from '../lib/harper'

app.post('/create/user', async (c) => {
  const { username, password, roleName } = await c.req.parseBody()
  // Validation of the body fields
  if (!username || !roleName || !password) return c.json({ error: 'Invalid credentials.' })
  const tmp = await createUser(c, username as string, password as string, roleName as string)
  // Extract the two vital things: operation tokens & error (if any)
  const { operation_token, error } = tmp
  if (error) return c.json({ error })

  // Do some server-side operation
  // say detect that it's logged in via Web (and not mobile)
  // const web = true
  // await insert([{ username, web }])
  // Create and set a signed cookie based on the server-side secret
  await setSignedCookie(c, 'custom_auth', JSON.stringify({ username, operation_token }), getEnv(c, 'CUSTOM_SECRET'), {
    path: '/',
    httpOnly: true,
    sameSite: 'Strict',
  })
  return c.json({})
})

Below a sample response from this particular endpoint 👇

An Endpoint To Sign In Users

We’ll establish an endpoint responsible for signing in users via HarperDB. The route /get/user expects a JSON payload containing username and password. It validates the input fields and utilizes the getUser function we created earlier to authenticate the user. Upon successful authentication, it extracts the operation token and, if applicable, any error messages. The code concludes by setting a signed cookie (custom_auth).

// File: api/index.ts

import { setSignedCookie } from 'hono/cookie'
import { getEnv, getUser } from '../lib/harper'

app.post('/get/user', async (c) => {
  const { username, password } = await c.req.parseBody()
  // Validation of the body fields
  if (!username || !password) return c.json({ error: 'Invalid credentials' })
  // Fetch the user from the credentials by creating authentication token
  const tmp = await getUser(c, username as string, password as string)
  // Extract the two vital things: operation tokens & error (if any)
  const { operation_token, error } = tmp
  if (error) return c.json({ error })
  // Create and set a signed cookie based on the server-side secret
  await setSignedCookie(c, 'custom_auth', JSON.stringify({ username, operation_token }), getEnv(c, 'CUSTOM_SECRET'), {
    path: '/',
    httpOnly: true,
    sameSite: 'Strict',
  })
  return c.json({})
})

Below a sample response from this particular endpoint 👇

An Endpoint To Get User Details

We’ll establish an endpoint to retrieve user details on the server-side. Utilizing the getSignedCookie method from hono/cookie and the getEnv function we created earlier, the route /get/session obtains a signed cookie (custom_auth) based on the server-side secret. If the cookie is present, it extracts the operation token of the logged-in user and returns a sanitized user object, excluding sensitive fields like operation_token and refresh_token. If no login is found, the endpoint responds with an error message.

// File: api/index.ts

import { getSignedCookie } from 'hono/cookie'
import { getEnv } from '../lib/harper'

// Just like any endpoint
// You can use this method to obtain the user object
// containing the auth header for database calls
app.get('/get/session', async (c) => {
  // Obtain the signed cookie based on the server-side secret
  const authCookie = await getSignedCookie(c, getEnv(c, 'CUSTOM_SECRET'), 'custom_auth')
  if (authCookie) {
    const tmp = JSON.parse(authCookie)
    // Extract the vital thing: operation token of the logged in user
    const { operation_token } = tmp
    if (operation_token) {
      // delete the fields that you do not want
      // to be exposed on the client-side
      delete tmp['operation_token']
      delete tmp['refresh_token']
      // return everything that was stored with the session
      return c.json(tmp)
    }
  }
  return c.json({ error: 'No login found.' })
})

Below a sample response from this particular endpoint 👇

Whew! All done, let's deploy our Hono app to Vercel 👇

Deploy to Vercel

The repository is ready to deploy to Vercel. Follow the steps below to deploy seamlessly 👇🏻

  • Create a GitHub Repository with the app code
  • Create a New Project in Vercel Dashboard
  • Link the created GitHub Repository as your new project
  • Scroll down and update the Environment Variables from the .env locally
  • Deploy! 🚀

References

Demo: https://harperdb-hono-vercel-credentials-auth.vercel.app/

GitHub Repo: https://github.com/rishi-raj-jain/harperdb-hono-vercel-credentials-auth

JWT Authentication: https://docs.harperdb.io/docs/developers/security/jwt-auth

HarperDB Users & Roles: https://docs.harperdb.io/docs/developers/security/users-and-roles

HarperDB Basic Authentication: https://docs.harperdb.io/docs/developers/security/basic-auth

Hono Web Framework: https://hono.dev