Next.js integration

Lean how to implement Zalter Identity using Next.js

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.

Clone the sample application

You can clone the sample application from GitHub.

Before you start

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

Setup application

Create a new Next.js application.

Terminal
npx create-next-app demo

Install the SDKs

Terminal
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);

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.

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.

JavaScript
// 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.

JavaScript
// 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.

JavaScript
// 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!'
  });
}

Was this page helpful?