Machine To Machine Auth

M2M auth with Next.js

Design proposal on how to perform M2M auth for SaaS applications built with Next.js that exposes route handlers for external clients.

Clerk is used as the auth provider for this example. Although Clerk doesn't expose M2M auth at the time of writing this, the goal is to showcase how this would fit their product.

A live example can be found at m2m-auth.vercel.app - Source code

Table of contents

UI component

Managing keys in the UI should be as easy as rendering a component. Eliminating the need for developers to directly interact with the auth provider to build the UI from scratch.

import { ApiKeyManager } from '@clerk/react';

export default function Page() {
  return <ApiKeyManager />;
}

Clerk could expose a similar (or even the same - dogfooding) UI component as in their Dashboard:

Clerk's Secret Keys component

Best practices

  • Displaying the key creation date helps developers link it to incidents and distinguish between multiple keys.
  • Only display API keys on request, preferably using a copy button to avoid showing them.
  • Depending on the key format (and this is related with the API implementation as well) then keys might be easier to select - it is easier to select the API key in snake case:
    • 4a8b93d2-7f82-46f8-a8b1-88f2a5d67254
    • b7e23eeb44b34185bcf657e5c88df016_24d4b6
  • Show the token's last few digits in the UI to help users manage keys effectively.

UI utilities

Besides the UI component, some utilities could also be exposed to manage API keys from the client-side.

React Hooks, such as: useApiKeyManager()

"use client";

import { useApiKeyManager } from "@clerk/clerk-react";

export default function Page() {
  const { apiKeys, createApiKeys } = useApiKeyManager();

  return (...);
}

Protecting route handlers

import { NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs';

export async function GET() {
  const { consumerId } = auth();

  if (!consumerId){
    return new Response("Unauthorized", { status: 401 });
  }

  const data = { message: 'Hello World' };

  return NextResponse.json({ data });
}

With middleware

Introduce a new option into the existing authMiddleware to authenticate certain routes via API keys, rather than using the user's identity.

import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware({
  protectedWithKeys: ["/my-sass-api-route"],
});

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

The current workaround for developers that integrate Unkey with Clerk, requires setting protected routes with keys to publicRoutes in order to bypass the user's identity verification on the request headers, which is not intuitive.

Identify external client within the request

The Auth object should contain an identifier for the non-user principal making the request. An example would be an application belonging to a different domain that has been granted access to the SaaS application's API.

This should be accessible via the auth helper (requiring the authMiddleware to be enabled).

import { auth } from "@clerk/nextjs";

export default function Page() {
  const { consumerId } = auth();

  return (...);
}

Using consumerId here, with the thought of identifying the machine "consuming" your API. Other options are:

  • thirdPartyClientId
  • externalClientId