Skip to main content
Version: v2

Integrate with NextAuth

This guide provides information on how to set up Beyond Identity as a passwordless authentication provider for a Next application that uses NextAuth (which is becoming Auth.js).

note

This guide uses the Embedded SDK Beyond Identity authenticator type, allowing full app customization. This solution involves creating more routes. For a lighter-weight solution, see guidance using the Hosted Web authenticator type in the guide: Getting Started with Next.js. See our Authentication article for more information on authenticator types.

In this guide, you'll:

  1. Configure Beyond Identity as an Identity Provider
  2. Create an identity and generate a passkey
  3. Authenticate with a passkey

Prerequisites

NextAuth

The following are provided for your reference:

Example

You'll overwrite the wellKnown, clientId, and clientSecret values later in this process so that you can use a dummy value for now.

  1. Create an auth route in your Next.js project

    Next.js VersionRoute
    12Create a route at pages/api/auth/[...nextauth].js.
    13Create a route at app/api/auth/[...nextauth]/route.ts.
  2. Add the following content.

    import NextAuth from 'next-auth';

    export default NextAuth({
    providers: [
    {
    id: 'beyondidentity',
    name: 'Beyond Identity',
    type: 'oauth',
    wellKnown: process.env.BEYOND_IDENTITY_DISCOVERY,
    authorization: { params: { scope: 'openid' } },
    clientId: process.env.BEYOND_IDENTITY_CLIENT_ID,
    clientSecret: process.env.BEYOND_IDENTITY_CLIENT_SECRET,
    idToken: true,
    checks: ['state', 'pkce'],
    profile(profile) {
    return {
    id: profile.sub,
    name: profile.sub,
    email: profile.sub,
    };
    },
    },
    ],
    });

Set up Beyond Identity as an Identity Provider

To set up Beyond Identity as an Identity Provider, you must create a Realm to hold identities and configuration. Inside that realm, you'll also create an Application that contains the authentication flow configuration. You can configure these in the admin console, which was created for you when you signed up for a developer account.

Create a Realm

Creating a realm from the Beyond Identity Admin Console is easy.

  1. In the Admin Console, under Tenant Management, select Go to realm > Create new realm.

    Admin Console Create new realm

  2. Enter a name for your realm and click Create realm.

  3. In the confirmation dialog, switch to the new realm.

    Create new realm confirmation success

Create an Application

  1. From the Admin Console, under Authentication, select Apps > Add new app.

    Admin Console Apps Add new app

  2. Give your application a name.

    Admin Console Add a new application window

  1. On the External Protocol tab, use the following values to complete this tab.

    PropertyValue
    Display NameDescriptive name you choose
    ProtocolOIDC

    Why OIDC?

    OAuth2 is primarily an authorization framework for resource access, while OIDC builds on OAuth2 to provide an identity layer for authentication, allowing client applications to obtain information about the authenticated user. Both protocols are often used together in modern applications to provide a comprehensive solution for secure authentication and authorization.

    Client TypeConfidential

    Why Confidential?

    A "confidential" client type is ideal when your application can securely store a client secret and requires enhanced security features for token exchange and accessing user-specific resources. If your application runs in an untrusted environment or you cannot securely manage a client secret, a "public" client type might be more appropriate.

    PKCES256

    Why S256?

    Choosing the "S256" PKCE type is strongly recommended for public clients like SPAs or mobile apps. It offers a significant security improvement over the "disabled" and "plain" options, effectively protecting against code interception attacks, which are a major concern in less secure client environments. By using "S256" PKCE, you can ensure a higher level of security for your OAuth 2.0 and OIDC flows and better protect your users' data and resources.

    Redirect URIsUse your application's App Scheme or Universal URL.

    Your redirect URI follows the pattern:

    http://localhost:3000/api/auth/callback/beyondidentity

    The beyondidentity in this URI is the id of the OAuth provider as configured in the providers array in NextAuth.js. /api/auth/callback/ is based on the Next.js route file structure.

    Token Endpoint Auth MethodClient Secret Basic

    Why Client Secret Basic?

    If your OIDC client is a confidential client and you can securely store the client secret, "client_secret_basic" is the recommended option due to its security benefits and broad support across various authorization servers. However, for public clients or environments where storing the client secret securely is challenging, you might opt for alternative authentication methods like "client_secret_post" or "none," while understanding the trade-offs in security.

    Grant TypeAuthorization Code

    Why Authorization Code?

    The "authorization_code" grant type is suitable for confidential clients, especially when your application needs to access user-specific resources, requires Single Sign-On (SSO) support, and prioritizes security in the authentication process. It provides a secure and standardized way to obtain access to user data and resources without exposing user credentials to the client application.

    All other optionsUse the default values for the remaining options
  2. On the Authenticator Config tab, use the following values to complete this tab.

    PropertyValue
    Configuration TypeEmbedded SDK
    Invocation TypeAutomatic
    Invoke URLYour application's App Scheme or Universal URL
    Trusted OriginYour application's App Scheme or Universal URL
  3. Click Submit to save the new app.

Configure environment variables

Now that you've created an app in Beyond Identity, you're ready to update some values. Store these values in your Next application's environment variables to use with the Beyond Identity provider.

  1. For wellKnown, copy and paste the value from Applications > {New Application} > External Protocol > Discovery Endpoint.

  2. For clientId, copy and paste the value from Applications > {New Application} > External Protocol > Client ID.

  3. For clientSecret, copy and paste the value from Applications > {New Application} > External Protocol > Client Secret.

You'll need to store a few more Beyond Identity values for API calls.

API callsDescription
BEYOND_IDENTITY_REGIONThis is your tenant's region, either "us" or "eu", which you can locate in the URL of the Beyond Identity Admin Console.
BEYOND_IDENTITY_TENANT_IDFrom your realm's Home page, click Edit realm, then copy the Tenant ID from the Edit realm page. If you need help finding your tenant ID, see our Find Tenant ID how-to article.
BEYOND_IDENTITY_REALM_IDFrom your realm's Home page, click Edit realm, then copy the Realm Id from the Edit realm page.
BEYOND_IDENTITY_APPLICATION_CONFIG_IDFrom Applications > {New Application} > Authenticator Config > Authenticator Config ID

Your .env file's contents should look something like the example below:

.env.local
# Next Auth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=generate one here: https://generate-secret.vercel.app/32
...

# Beyond Identity
BEYOND_IDENTITY_CLIENT_ID=Njlb6brHbCofRyCWGkiUQweTz
BEYOND_IDENTITY_CLIENT_SECRET=zs2oENddYNes ... 7uamRhvRJ9d3ZJcI97X
BEYOND_IDENTITY_DISCOVERY=https://auth-us.beyondidentity.com/v1/tenants/000111222333/realms/444555666777888/applications/1233449b-11c4-4884-b622-0bfbaefd44d3/.well-known/openid-configuration

BEYOND_IDENTITY_REGION=us
BEYOND_IDENTITY_TENANT_ID=000150caae867219
BEYOND_IDENTITY_REALM_ID=898e1e08d7da2373
BEYOND_IDENTITY_APPLICATION_CONFIG_ID=2e540d7a-4caf-7448-94ba-70183a82b4ad

Create an Identity and generate a passkey

Once you've created an application in the admin console, you're ready to provision users in your realm's directory, generate passkeys, and handle those passkeys in your application.

Create an Identity

User creation can be done either in the admin console or through an API.

Using the Admin Console

  1. From the Admin Console, under Directory, select Identities > Add identity.

  2. Enter the name, username, and email of the new identity you're adding.

    Add an identity

  3. Click Add Identity.

For more information about identities, see Add an identity.

Creating an API

In your application, create an API to call the Beyond Identity Cloud. All APIs require Authorization with an accessToken. See Access tokens for information.

const identityResponse = await fetch(
`https://api-${process.env.BEYOND_IDENTITY_REGION}.beyondidentity.com/v1/tenants/${process.env.BEYOND_IDENTITY_TENANT_ID}/realms/${process.env.BEYOND_IDENTITY_REALM_ID}/identities`,
{
body: JSON.stringify({
identity: {
display_name: email,
traits: {
type: 'traits_v0',
username: email,
primary_email_address: email,
},
},
}),
headers: {
Authorization: 'Bearer ' + accessToken,
},
method: 'POST',
}
);

The response should contain an identity ID you'll need to generate a passkey.

let identityResponseJson = await identityResponse.json();
let identityId = identityResponseJson.id;

Generate a passkey

Once you have an identity, you can generate a passkey.

Using the Admin Console

  1. In the Admin Console, under Directory, select Identities.

  2. Select the identity you want to bind a passkey to and click Add a passkey.

  3. Select the app and then select Send an email to user.

    Create a new passkey dialog box

  4. Click Proceed & send email.

    A device enrollment email is sent to your user's primary email address with a link to create their passkey. Clicking or tapping the link redirects your user to the Beyond Identity Cloud. The Beyond Identity Cloud then looks up the Authenticator Config associated with that passkey creation link. Finally, it redirects your user to your application using the Authenticator Config's Invoke URL specified.

For more information, How passkeys are created.

Creating an API

Continuing your API, use the identityId you generated above, and create a credential-binding job. A device enrollment email gets sent to your user's primary email address.

The postBindingRedirectUri is the URL you want to redirect the user toafter successfully binding a passkey.

const credentialBindingLinkResponse = await fetch(`https://api-${process.env.BEYOND_IDENTITY_REGION}.beyondidentity.com/v1/tenants/${process.env.BEYOND_IDENTITY_TENANT_ID}/realms/${process.env.BEYOND_IDENTITY_REALM_ID}/identities/${identityId}/credential-binding-jobs`,
{
body: JSON.stringify({
job: {
delivery_method: 'EMAIL',
authenticator_config_id: ${process.env.BEYOND_IDENTITY_APPLICATION_CONFIG_ID},
post_binding_redirect_uri: postBindingRedirectUri,
},
}),
headers: {
Authorization: 'Bearer ' + accessToken,
},
method: 'POST',
}
);

let credentialBindingLinkResponseJson =
await credentialBindingLinkResponse.json();
res.send(credentialBindingLinkResponseJson);

For more information on this API, see Add a passkey.

Configure your application

Bind passkey to device

When the user clicks or taps the link in the enrollment email, they are redirected to your application. You must create a route in your application to intercept the link.

  1. Intercept the link from the enrollment email. The link that redirects to your application will have the /bind path appended to your Invoke URL and several other query parameters.

    $invoke_url/bind?api_base_url=<api_base_url>&tenant_id=<tenant_id>&realm_id=<realm_id>&identity_id=<identity_id>&job_id=<job_id>&token=<token>
  2. Pass the link from the enrollment email into the SDK to complete the binding process.

    You can validate the incoming URL with isBindPasskeyUrl. Upon success, a private key is created in the device's hardware trust module, and the corresponding public key will be sent to the Beyond Identity Cloud. At this point, the user has a passkey enrolled on this device.

  3. Create a bind route.

    Next.js VersionRoute
    12create a page.tsx page under /app/bind/
    13create a bind.tsx page under /pages/

    If your Invoke URL is configured properly in your Authenticator Config, this page gets redirected to during a bind passkey flow. Copy the following code snippet into that page.

import { useEffect, useState } from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import { signIn } from 'next-auth/react';
import { Passkey } from '@beyondidentity/bi-sdk-js';

const BIBindPasskey = () => {
const [bindPasskeyResult, setBindPasskeyResult] = useState('');

useEffect(() => {
// -- 1
const bindPasskey = async () => {
const BeyondIdentityEmbeddedSdk = await import(
'@beyondidentity/bi-sdk-js'
);
let embedded = await BeyondIdentitySdk.EmbeddedSdk.initialize();
if (embedded.isBindPasskeyUrl(window.location.href)) {
// Only bind passkey if the URL is a "bind" URL
let bindPasskeyUrl = window.location.href;
// -- 2
embedded
.bindPasskey(bindPasskeyUrl)
.then((result) => {
// -- 3
setBindPasskeyResult(result);
signIn('beyondidentity', {
tenant_id: tenantId,
});
})
.catch((error) => {
setBindPasskeyResult(error.toString());
});
}
};

bindPasskey().catch(console.error);
}, []);

return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
}}
>
<div className="container">
<div className="row">
<div className="d-flex justify-content-center">
<div className="spinner-border" role="status">
<span className="sr-only"></span>
</div>
</div>
</div>
<div className="row">
{bindPasskeyResult.length > 0 && (
<div className="row row-cols-1 row-cols-md-1 mt-3">
<div className="col">
<code>{JSON.stringify(bindPasskeyResult, null, 2)}</code>
</div>
</div>
)}
</div>
</div>
</div>
);
};

export default BIBindPasskey;

What's happening here?

  1. After tapping the email sent to the user's device, the user is directed to your application's /bind route appending to the invoke_url configured in the application config.

  2. The useEffect is only called once on page load. In this function, we initialize the Beyond Identity SDK and use embedded.isBindPasskeyUrl to check if the current page redirected to is, in fact, a valid bind URL.

  3. If the URL is valid, the URL using window.location.href is passed directly into embedded.bindPasskey to complete the binding process.

  4. Finally, the response of embedded.bindPasskey contains a passkey object, representing the passkey bound to the device.

Once you have one passkey bound to a device, you can use it to authenticate.

Configure the NextAuth Provider

Next.js VersionRoute
12create a [...nextauth].ts page under /pages/api/auth/
13create a page.tsx page under /app/auth/[...nextauth]/

Add the following Beyond Identity provider. The provider goes through an OAuth/OIDC that fetches an ID token to log you into the example app. Use the values you saved in your environment variables when creating an application above.

...
import NextAuth from "next-auth"
...
providers: [
{
id: "beyondidentity",
name: "Beyond Identity",
type: "oauth",
wellKnown: process.env.APP_DISCOVERY_ENDPOINT,
authorization: { params: { scope: "openid" } },
clientId: process.env.APP_CLIENT_ID,
clientSecret: process.env.APP_CLIENT_SECRET,
idToken: true,
checks: ["state"],
profile(profile) {
return {
id: profile.sub,
name: profile.sub,
email: profile.sub,
}
}
}
]
...

Authenticate

The authenticate URL redirecting to your application will append a /bi-authenticate path to your Invoke URL.

$invoke_url/bi-authenticate?request=<request>

Create a /bi-authenticate route to intercept this URL in your application.

Next.js VersionRoute
12create a bi-authenticate.tsx page under /pages/
13create a page.tsx page under /app/bi-authenticate/

If your Invoke URL is configured properly in your Authenticator Config, this page gets redirected to during a bind passkey flow. Copy the following code snippet into that page.

import { useEffect, useState } from "react";
import "bootstrap/dist/css/bootstrap.css";
import { Passkey } from "@beyondidentity/bi-sdk-js";

const BIAuthenticate = () => {
const [biAuthenticateResult, setBiAuthenticateResult] = useState("");

useEffect(() => {
// -- 1
const authenticate = async () => {
const BeyondIdentityEmbeddedSdk = await import("@beyondidentity/bi-sdk-js");
let embedded = await BeyondIdentityEmbeddedSdk.Embedded.initialize();
if (embedded.isAuthenticateUrl(window.location.href)) {
// Only authenticate if the URL is a "bi-authenticate" URL
let biAuthenticateUrl = window.location.href;
// -- 2
biAuthenticate(biAuthenticateUrl).then(redirectURL => {
// -- 4
window.location.href = redirectURL;
}).catch(error => {
setBiAuthenticateResult(error.toString());
});
}
}
authenticate().catch(console.error);
}, []);

// -- 3
async function biAuthenticate(url: string): Promise<string> {
const BeyondIdentityEmbeddedSdk = await import("@beyondidentity/bi-sdk-js");
let embedded = await BeyondIdentityEmbeddedSdk.Embedded.initialize();

// Display passkeys so user can select one
let passkeys = await embedded.getPasskeys();
let promptText = passkeys.map((passkey, index) => {
return `${index}: ${passkey.identity.username}`;
}).join("\n");
let selectedIndex = parseInt(prompt(promptText, "index")!!);
if (selectedIndex >= 0 && selectedIndex < passkeys.length) {
let selectedId = passkeys[selectedIndex].id;
// Perform authentication using selected id
let result = await embedded.authenticate(url, selectedId);
return Promise.resolve(result.redirectURL);
} else {
// This will fail in core as it won't match to any id
return Promise.resolve("unknown_id");
}
}

return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
}}
>
<div className="container">
<div className="row">
<div className="d-flex justify-content-center">
<div className="spinner-border" role="status">
<span className="sr-only"></span>
</div>
</div>
</div>
<div className="row">
{
biAuthenticateResult.length > 0 &&
<div className="row row-cols-1 row-cols-md-1 mt-3">
<div className="col">
<code>
{JSON.stringify(biAuthenticateResult, null, 2)}
</code>
</div>
</div>
}
</div>
</div>
</div>
);
};

export default BIAuthenticate;

What's happening here?

  1. The useEffect is only called once on page load. In this function, we initialize the Beyond Identity SDK and use embedded.isBindPasskeyUrl to check if the current page redirected to is, in fact, a valid bind URL.

  2. If the URL is valid, the URL using window.location.href is passed directly into biAuthenticate.

  3. biAuthenticate calls embedded.authenticate with a valid bi-authenticate URL. This function performs a challenge/response against a passkey bound to your browser. Note that the callback in embedded.authenticate contains logic to prompt a user to select a passkey if there is more than one.

  4. Finally, the embedded.authenticate response contains a redirectURL. Follow this redirectURL to complete the OAuth/OIDC flow.

Configure UI

NextAuth has a useSession hook to access session data and authentication status on the client side. To use this hook, you must wrap your components in a SessionProvider, which uses React Context.

  1. Wrap your main components in a SessionProvider

    import { SessionProvider } from 'next-auth/react';
    import type { AppProps } from 'next/app';

    export default function App({ Component, pageProps }: AppProps) {
    return (
    <SessionProvider session={pageProps.session} refetchInterval={0}>
    <Component {...pageProps} />
    </SessionProvider>
    );
    }
  2. Use the useSession() hook and signIn and signOut from next-auth/react inside a component.

    note

    'beyondidentity' in signIn is the ID of the OAuth provider as configured in the providers array above.

    import { signIn, signOut, useSession } from 'next-auth/react';
    const { data: session } = useSession();
    const [email, setEmail] = useState('');

    export default function Login() {
    const { data: session, status } = useSession();
    const [email, setEmail] = useState('');

    const handleRegistration = () => {
    // Call your registration API that you created above
    // to generate an identity and bind a passkey
    callAPICreatedAbove(email);
    };

    return (
    <div>
    {session?.user && (
    <div>
    <p>{`Welcome ${session.user.name}!`}</p>
    <button onClick={() => signOut()}>Sign Out</button>
    </div>
    )}
    {!session && (
    <div>
    <div>
    <button onClick={() => signIn('beyondidentity')}>Sign In</button>
    </div>
    <div>
    <input
    type="email"
    onChange={(event) => setEmail(event.target.value)}
    />
    <button onClick={handleRegistration}>Register</button>
    </div>
    </div>
    )}
    </div>
    );
    }