Getting started with the new Auth0 extension on Netlify

by Domitrius Clark

In collaboration with Auth0, we’ve released a new extension to connect your Auth0 tenants to your Netlify account dashboard and add authentication to your Netlify site!

#TL;DR

We’ll walk through:

  • Creating a new Astro project in Netlify
  • Configuring the Auth0 extension through Netlify
  • Connecting a tenant to our Netlify account
  • Using the generated Environment variables to authenticate an Astro site & protect our Netlify functions

Want to jump straight to the code?

Deploy a complete working example by clicking the button below:

Deploy to Netlify

#Prerequisites

Before getting started, you’ll need:

#Create a new site on Netlify

Already have a site?

If you already have a site and just want to enable the extension, you can skip to the next section.

For educational purposes, we’re going to start with a brand new website on our Netlify account. We’ll be using Astro to build our app. Run the following command and select the default values:

Terminal window
npm create astro@latest

Once our project is created, we’ll need to make a couple changes to the Layout.astro and index.astro files:

src/layout/Layout.astro
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Auth0 Authentication Example</title>
</head>
<body>
<slot />
</body>
</html>
src/pages/index.astro
---
import Layout from '../layouts/Layout.astro';
---
<Layout>
<header>
<h1>Welcome to Auth0 Authentication Example</h1>
</header>
<main>
<div id="auth0-login-container">
<button id="login">Log In</button>
<button id="logout" style="display: none">Log Out</button>
<button id="test-function">Test Function</button>
</div>
<div id="profile" style="display: none">
<h2>Profile</h2>
<pre id="profile-data"></pre>
</div>
</main>
</Layout>

Next, we’ll install the Auth0 JS SDK to streamline login, logout, and help us generate a token to protect our serverless functions.

Terminal window
npm install @auth0/auth0-spa-js

This page, while not authenticated, will show a header and a log in button. Eventually, when authenticated, we’ll be able to see a logout button and some of the logged in user’s details. (We append these later in index.astro within a script tag to the profile-data container.)

With our scaffold ready, let’s push up to GitHub and connect the site to Netlify.

#Install the extension

Now that we’ve completed setting our site up and connecting to Netlify we can move to installing and enabling the extension:

  1. In the team Dashboard, navigate to the Extensions page from the sidebar.
  2. Search for Auth0 and select it from the results, then install from the details page.

Select "Install" from the Auth0 Extension details page

  1. From the team’s Sites list, select the site you plan to use with this extension, navigate to the Access & security settings page and scroll to find the Auth0 extension settings.
  2. Next, start the process to link your Auth0 tenant to our Netlify account. Select Link an Auth0 tenant and follow the prompts to authorize Netlify to access your tenant.

Once you’ve connected your tenant, your next step is to configure the extension for your site.

#Configure the extension

  1. Under Site tenants, select Add a tenant and select a tenant to use for your site’s deploy contexts.
  2. Select the Auth0 application and, optionally, an API. If you don’t already have Auth0 application and APIs, we recommend that you create them from the Netlify UI. Creating Auth0 applications and APIs from the Netlify UI optimizes their settings for use with Netlify.

If you already have Auth0 applications and APIs, you can select and assign them to each tenant.

  1. Select one or more deploy contexts for your site tenant.

Branch deploy limitations

This extension does not support assigning a tenant to individual branch deploys. The tenant selected for the Branch Deploys context applies to all branch deploys.

  1. This extension automatically creates a set of environment variables based on your configuration. If your framework or build system requires the use of a specific prefix in environment variable names, input your desired prefix or select one from the Preset dropdown. If your framework or build system doesn’t require a prefix, leave this blank.

Environment Variables

For our project, we’re going to need to add prefixes for Astro to recognize a few of our environment variables in the client. Add PUBLIC as the prefix. This will prefix all of your client based environment variables with PUBLIC.

You can review these variables at any time by navigating to Site configuration > Environment variables.

To configure multiple tenants, repeat these steps for each tenant.

#Using Auth0 in your Astro site

With the extension now enabled, it’s time to dig into some code and set our application up to utilize Auth0 to set up our authorization flow alongside protected Netlify functions.

Over the next section, we’ll:

  • Build utility functions to:
    • handle client authentication actions like login, log out, and token generation to pass to our Netlify functions
    • handle server side authentication actions like reading & verifying the token from the request inside of our functions
  • Build a serverless function that will verify your token and fail or succeed based on that verification
  • Connect to Auth0’s SDK and send the user to an Auth0 hosted Sign in/up page

#Setting up the project structure

First, we’ll create a couple of files to hold the functions we’ll be using across the client and server. Inside of your base directory, create a utils folder with two files:

Terminal window
mkdir utils
touch utils/server-auth.ts utils/client-auth.ts

Client vs Server Code

To make sure we don’t bundle our server code into our client bundle, we make this distinction between client and server utilities.

#Building the client utilities

Let’s build out our client-auth.ts file first:

utils/client-auth.ts
import { Auth0Client, createAuth0Client } from '@auth0/auth0-spa-js';
let auth0Client: Auth0Client | null = null;
// Let each function invoke the client in scope & make sure we have the proper authorization scopes
export async function getAuth0Client(): Promise<Auth0Client> {
if (auth0Client) return auth0Client;
auth0Client = await createAuth0Client({
domain: import.meta.env.PUBLIC_AUTH0_DOMAIN,
clientId: import.meta.env.PUBLIC_AUTH0_CLIENT_ID,
authorizationParams: {
audience: import.meta.env.PUBLIC_AUTH0_AUDIENCE,
redirect_uri: window.location.origin,
scope: 'openid profile email'
}
});
return auth0Client;
}
// This function will bring us to the hosted Auth0 sign in/up page
export async function login(): Promise<void> {
const client = await getAuth0Client();
await client.loginWithRedirect({
authorizationParams: {
redirect_uri: window.location.origin
}
});
}
export async function logout(): Promise<void> {
const client = await getAuth0Client();
await client.logout({
logoutParams: {
returnTo: window.location.origin
}
});
}
// Make sure we're properly redirected and our data is represented properly afterward
export async function handleCallback(): Promise<void> {
const client = await getAuth0Client();
await client.handleRedirectCallback();
window.history.replaceState({}, document.title, '/');
}
// Utility function to make fetching protected endpoints easier
export async function callProtectedEndpoint<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const client = await getAuth0Client();
const token = await client.getTokenSilently();
const response = await fetch(endpoint, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('Full error response:', {
status: response.status,
statusText: response.statusText,
body: errorText,
headers: Object.fromEntries(response.headers)
});
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}

#Building the server-side utilities

Our server function will be used to verify the token from the request inside of our functions. First, we’ll need to install the jose package for JWT verification:

Terminal window
npm install jose
npm install @types/node -D

Why jose?

jose is a JavaScript module that implements JSON Web Token (JWT) and JSON Web Signature (JWS) standards. It’s particularly well-suited for token verification in Node.js environments.

Now we can build our token verification function:

utils/server-auth.ts
import { type JWTVerifyResult, createRemoteJWKSet, jwtVerify } from "jose";
export type VerifyAuth0TokenResult<
CustomClaims extends Record<string, unknown> = Record<string, unknown>,
> = {
token: string;
result: JWTVerifyResult & { payload: CustomClaims };
};
export const verifyAuth0Token = async <
CustomClaims extends Record<string, unknown>,
>(
request: Request,
): Promise<VerifyAuth0TokenResult<CustomClaims>> => {
const authorization = request.headers.get("Authorization") ?? "";
const [type, token, ...parts] = authorization
.replace(/\s+/g, " ")
.trim()
.split(" ");
if (type !== "Bearer" || parts.length !== 0) {
throw new Error("Missing or invalid Authorization header");
}
const AUTH0_ISSUER = process.env.AUTH0_ISSUER;
const AUTH0_AUDIENCE = process.env.PUBLIC_AUTH0_AUDIENCE;
const JWKS = createRemoteJWKSet(
new URL(".well-known/jwks.json", AUTH0_ISSUER),
);
const result = (await jwtVerify(token, JWKS, {
issuer: AUTH0_ISSUER,
audience: AUTH0_AUDIENCE,
})) as unknown as JWTVerifyResult & { payload: CustomClaims };
return { token, result };
};

Learn more about token verification

Want to learn more about how token verification works? Check out these docs from Auth0.

#Building the Protected Function

Before we wire up our client utilities, let’s build out our serverless function. First, install the functions package from Netlify:

Terminal window
npm install @netlify/functions

Create a new folder structure for our function:

Terminal window
mkdir -p netlify/functions
touch netlify/functions/auth-gated-function.ts

Now let’s create our protected function:

netlify/functions/auth-gated-function.ts
import type { Context } from "@netlify/functions";
import { verifyAuth0Token } from "../../utils/server-auth";
export default async function handler(req: Request, context: Context) {
try {
const { token, result } = await verifyAuth0Token(req);
if (!token) {
return new Response('Unauthorized - No token', { status: 401 });
}
const response = {
message: 'Authenticated',
result
};
return new Response(JSON.stringify(response), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Auth error:', error);
return new Response(error instanceof Error ? error.message : 'Unauthorized', {
status: 401
});
}
}

#Implementing the Client-Side Authentication

Now let’s wire up our authentication UI. Add this script tag below your HTML in index.astro:

src/pages/index.astro
<script>
import { login, logout, handleCallback, getAuth0Client, callProtectedEndpoint } from '../../utils/client-auth';
interface Auth0Profile {
name: string;
picture: string;
}
interface DOMElements {
loginButton: HTMLElement;
logoutButton: HTMLElement;
profileElement: HTMLElement;
testButton: HTMLElement;
}
async function updateUI(): Promise<void> {
const auth0Client = await getAuth0Client();
const isAuthenticated = await auth0Client.isAuthenticated();
const userProfile = await auth0Client.getUser<Auth0Profile>();
const elements: DOMElements = {
loginButton: document.getElementById("login") as HTMLElement,
logoutButton: document.getElementById("logout") as HTMLElement,
profileElement: document.getElementById("profile") as HTMLElement,
testButton: document.getElementById("test-function") as HTMLElement
};
if (isAuthenticated && userProfile) {
elements.loginButton.style.display = "none";
elements.logoutButton.style.display = "block";
elements.profileElement.style.display = "block";
elements.profileElement.innerHTML = `
<p>${userProfile.name}</p>
<img src="${userProfile.picture}" alt="Profile" />
`;
} else {
elements.loginButton.style.display = "block";
elements.logoutButton.style.display = "none";
elements.profileElement.style.display = "none";
}
}
function attachEventListeners(): void {
document.getElementById('login')?.addEventListener('click', login);
document.getElementById('logout')?.addEventListener('click', logout);
document.getElementById('test-function')?.addEventListener('click', async () => {
try {
const result = await callProtectedEndpoint('/.netlify/functions/auth-gated-function');
console.log('Function response:', result);
alert(JSON.stringify(result, null, 2));
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error details:', error);
alert(`Error calling function: ${error.message}`);
} else {
console.error('Unknown error:', error);
alert('An unknown error occurred');
}
}
});
}
if (location.search.includes("code=") && location.search.includes("state=")) {
await handleCallback();
attachEventListeners();
await updateUI();
} else {
attachEventListeners();
await updateUI();
}
</script>

Local Development

During the process of enabling the extension, Netlify will set the site URL for the Auth0 Application URIs. If you’re developing locally, don’t forget to set your URL for your local server in your Auth0 application settings.

#Testing the Authentication Flow

  1. Push your changes to your repository (a succesful deploy can be seen in the image below)
  2. Visit your deployed Netlify site
  3. Click the “Log In” button
  4. Complete the Auth0 signup/signin process
  5. You should see your profile picture and name appear
  6. Try the “Test Function” button to verify the protected endpoint

Once you push your changes, a successful deploy will show a published badge and your site link should be updated to the latest build

Tenant Management

If you go back to the extension settings and choose your tenant, you should see the new user you just created!

#Next Steps

Now that you have authentication working in your site, here are some ways to build upon this foundation:

  • Remove the Flash of Content (FOC) after redirecting using loading states
  • Build custom authentication UI with the @auth0/react package
  • Add role-based access control (RBAC) to your protected functions

These additions will help create a smoother, more secure experience for your users.

#Additional Resources