Signature-based Auth + Next.js? How to integrate Zalter under 10 minutes
Tags: authentication, nextjs, passwordless
Published: 2022-08-01 13:57:22

---
title: Signature-based Auth + Next.js? How to integrate Zalter under 10 minutes
published: true
description: Authenticate users through Zalter Identity API in a few minutes. No security compromises.
tags: authentication, nextjs, passwordless
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wc3txg8l8lc38j9jz2vf.png
---


Zalter Identity is an identity and authentication provider service based on signatures rather than tokens. It provides a very secure authentication system which protects your users even against Active Man In the Middle Attacks as well as protection for all the user requests ensuring that all of these requests do come from the intended user.

*Zalter is currently in closed-beta, but we usually accept any type of organisations and projects! If you have any questions regarding integrations or bug reports please join the discussion on our [Discord server](https://discord.gg/5GNKkunf)*



<img alt="Zalter Diagram" src="https://developer.zalter.com/_next/image?url=%2Fstatic%2Fidentity-overview-flow.png&w=3840&q=75" target="_blank" />

#### GitHub Repo
Clone the sample app [here](https://github.com/zalter-com/zalter-identity-nextjs-sample)


#### Introduction

This is a demonstration on how to add the user authentication and do basic processing of signatures in your backend. Please keep in mind that this is made only for demonstration purpose as it does not go into detail with the best security practices in order to increase the security past the level provided by a simple OAuth or token based authentication. It should be understood that it still is at least as secure as OAuth2 or other authentication systems but can be better than that.

#### Before you start

To get the most of this guide, you'll need:

* Follow [Project Setup](http://localhost:7004/identity/project-setup)


## Setup application

Create a new Next.js application.

```bash
npx create-next-app demo
```

## Install the SDKs

```bash
npm install --save @zalter/identity @zalter/identity-js
```

## Client side

### Initialize the auth library

```javascript
// lib/auth.js

import { Auth } from '@zalter/identity-js';

export const auth = new Auth({
  projectId: '<your-project-id>'
});
```

### Create the sign-in page

This page has two forms, one for the email address and one for the code that will be received to complete the authentication.

```jsx
// pages/sign-in.js

import { useState } from 'react';
import Router from 'next/router';
import { auth } from '../lib/auth';

export default function SignIn() {
  const [email, setEmail] = useState('');
  const [emailSent, setEmailSent] = useState(false);
  const [code, setCode] = useState('');
  const [error, setError] = useState('');

  const onEmailSubmit = async (event) => {
    event.preventDefault();

    try {
      await auth.signInWithCode('start', {
        email
      });
      setEmailSent(true);
    } catch (err) {
      console.error(err);
      setError(err.message || 'Something went wrong');
    }
  };

  const onCodeSubmit = async (event) => {
    event.preventDefault();

    try {
      await auth.signInWithCode('finalize', {
        code
      });

      // Now you can redirect the user to a private page
      Router.push('/dashboard').catch(console.error);
    } catch (err) {
      console.error(err);
      setError(err.message || 'Something went wrong');
      // Zalter Identity service allows only one try per code.
      // The user has to request another code.
      // This allows Zalter to prevent man-in-the-middle attacks.
      setCode('');
      setEmailSent(false);
    }
  };

  return (
    <div>
      {!emailSent ? (
        <form onSubmit={onEmailSubmit}>
          <h3>Enter your email</h3>
          <div>
            <input
              name="email"
              onChange={(event) => setEmail(event.target.value)}
              placeholder="Email address"
              type="email"
              value={email}
            />
          </div>
          {error && (
            <div>
              {error}
            </div>
          )}
          <button type="submit">
            Continue
          </button>
        </form>
      ) : (
        <form onSubmit={onCodeSubmit}>
          <h3>Enter your code</h3>
          <div>
            <input
              name="code"
              onChange={(event) => setCode(event.target.value)}
              placeholder="Code"
              type="text"
              value={code}
            />
          </div>
          <button type="submit">
            Continue
          </button>
        </form>
      )}
    </div>
  );
}
```

### Routing

You can use the `auth.isAuthenticated()` function to check whether the user has been authenticated in case of a page refresh and in order to route the user to one page or another.

```javascript
if (await auth.isAuthenticated()) {
  // show the user back-office pages
} else {
  // show the login page.
}
```

### Sign out

```jsx
// pages/dashboard.js

import Router from 'next/router';
import { auth } from '../lib/auth';

export default function Dashboard() {
  const onSignOut = async () => {
    try {
      await auth.signOut();

      // Redirect user to a public page
      Router.push('/').catch(console.error);
    } catch (err) {
      console.error(err);
    }
  };

  return (
     <div>
      <h1>Dashboard</h1>
      <button onClick={onSignOut}>
        Sign out
      </button>
     </div>
  );
}
```

### Retrieve current authenticated user

To sign a message, you can get the current authenticated user which exposes the necessary utility functions.

```javascript
const user = await auth.getCurrentUser();
```

### Sign / authorize requests

Now that the user has been successfully authenticated, you can sign all the requests that you need the user to be authenticated in order for them to perform.

```javascript
/*
 * Signing helper function that accepts a requestInit, similar to fetch.
 * @param {RequestInit} request
 * @return {Promise<RequestInit>}
 */
async function signRequest(request) {
  const { method, headers, body } = request;

  // Load current user
  const user = await auth.getCurrentUser();

  // Get signing key ID
  const keyId = user.subSigKeyId;

  // Convert the body from String to Uint8Array
  const dataToSign = new TextEncoder().encode(body);

  // Sign the data using the user credentials
  const sig = await user.signMessage(dataToSign);

  // Convert the sig from Uint8Array to Base64
  // You might need an external package to handle Base64 encode/decode
  // https://www.npmjs.com/package/buffer
  const encodedSig = Buffer.from(sig).toString('base64');

  return {
    method,
    headers: {
      ...headers,
      'x-signature': `${keyId};${encodedSig}`
    },
    body
  };
}

// Lets say that we want to make a POST request to an API

const request = await signRequest({
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'John',
    email: 'john@example.com'
  })
});

const response = await fetch('/api/orders', request);
```

> This example only signs the body, but it can be adapted to sign other data, such as current timestamp, expire date, request headers, etc.

## Server side

### Initialize the identity client

In order for the SDK to be usable on the server side it needs to be initialized using the **service account credentials**.

```javascript
// lib/identity.js

import { IdentityClient } from '@zalter/identity';

const config = {
  projectId: '<your-project-id>',
  credentials: '<your-credentials>'
};

export const identityClient = new IdentityClient(config);
```

### Retrieve the user public key

Once the client has been initiated you can easily retrieve the public key for the user making any requests to your server, and verify their signature once you have their key.

Please note that the public key may be invalidated for a lot of reasons, and you should try not to cache it for long as you'd put your server in situations where it would process a key that is no longer valid (say for example the user logged out from all sessions).


```javascript
const keyId = ''; // retrieve key ID from 'x-signature' header

const keyRecord = await identityClient.getPublicKey(keyId);
```

### Create body parser middleware

Currently Next.js does not expose the raw data of the request. It automatically handles the request and parses the body based on the request content-type header. So we need to handle it ourselves.

```js
// lib/middlewares/body-parser-middleware.js

import bodyParser from 'body-parser';

export const bodyParserMiddleware = bodyParser.json({
  verify: (req, res, buf) => {
    req.rawBody = buf
  }
});
```

### Create authorization middleware

This middleware retrieves the value of the `x-signature` header sent from the client side.
Extracts the `keyId` to get the public key from Zalter Identity service, and the `sig` to verify
the request.

```javascript
// lib/middlewares/auth-middleware.js

import { Verifier } from '@zalter/identity';
import { identityClient } from './identity';

export async function authMiddleware(req, res, next) {
  const signatureHeader = req.headers['x-signature'];

  if (!signatureHeader) {
    console.log('Missing signature header');
    res.status(401).json({ message: 'Not authorized' });
    return;
  }

  // Get the key ID and sig
  const [keyId, sig] = signatureHeader.split(';');

  if (!keyId || !sig) {
    console.log('Invalid signature header format');
    res.status(401).json({ message: 'Not authorized' });
    return;
  }

  // Decode sig to get the signature bytes
  const rawSig = new Uint8Array(Buffer.from(sig, 'base64'));

  // Fetch the user public key from Zalter Identity service
  let keyRecord;

  try {
    keyRecord = await identityClient.getPublicKey(keyId);
  } catch (err) {
    console.error(err);

    if (err.statusCode === 404) {
      console.log('Public key not found');
      res.status(401).json({ message: 'Not authorized' });
      return;
    }

    res.status(500).json({ message: 'Internal Server Error' });
    return;
  }

  // Get the raw body of the message
  // Remember to add your own bodyParser since Next.js server does not expose the raw body
  const { rawBody } = req;

  // Construct data to verify (must be the same value as the data signed on the browser side)
  const dataToVerify = rawBody ? new Uint8Array(rawBody) : new Uint8Array(0);

  // Verify the signature
  const isValid = Verifier.verify(
    keyRecord.key,
    keyRecord.alg,
    rawSig,
    dataToVerify
  );

  if (!isValid) {
    console.log('Invalid signature');
    res.status(401).json({ message: 'Not authorized' });
    return;
  }

  // Persist user ID for other use
  // We can use any storage strategy. Here we simulate the "res.locals" from Express.
  res.locals = res.locals || {};
  res.locals.userId = keyRecord.subId;

  // Continue to the next middleware / handler
  next();
}
```

### Create middleware helper

This helper allows us to run middlewares in any API handler.

```js
// lib/run-middleware.js

export function runMiddleware(req, res, fn) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result);
      }

      return resolve(result);
    });
  });
}
```

### Create an API route

Now that we have the auth middleware we can protect any route. Until Next.js offers a solution, we need to disable the default body-parser and use our own implementation.

```js
// pages/api/orders.js

import { authMiddleware } from '../../lib/middlewares/auth-middleware';
import { bodyParserMiddleware } from '../../lib/middlewares/body-parser-middleware';
import { runMiddleware } from '../../lib/run-middleware';

// Disable default body parser middleware
export const config = {
  api: {
    bodyParser: false
  }
};

export default async function(req, res) {
  await runMiddleware(req, res, bodyParserMiddleware);
  await runMiddleware(req, res, authMiddleware);

  // To retrieve the user ID set in auth middleware you can use
  // res.locals.userId

  // Now you can retrieve the user data from your database.

  res.status(200).json({
    message: 'It works!'
  });
}
```
## Final notes
Hopefully this article makes the integration a little less scary and may even get you excited about our new approach to user authentication. We’re constantly updating our API and products as a result of feedback from developers - we want to hear from you. Reach out on [Discord](https://discord.gg/5GNKkunf) or by [email](https://zalter.com/contact).