
Authsignal
Overview
Out of the box Multi-Factor Authentication (MFA). Built specifically for developers and Fraud Ops teams. Implementation in hours, protection for a lifetime.
With Authsignal's flexible single API and SDK, step-up passwordless authentication challenge flows can be implemented at any stage or action in the customer journey. Drop in our code, select the authentication method(s), upload your branding, and you’re away. With a no-code rules engine, fraud ops teams can focus on creating rules so you can keep building.
Documentation
This guide shows how to integrate Authsignal with Next.js and Supabase in order to add an MFA step after sign-in.
The user flow is as follows:
- The user enters their email and password to sign in
 - If the user has set up MFA, they're prompted to complete an MFA challenge (via Authenticator App) in order to complete sign-in
 - If the user has not set up MFA, they're signed in immediately and will see a button to set up MFA
 
The approach uses a temporary encrypted cookie to ensure that the Supabase auth cookies (access_token and refresh_token) are only set if the MFA challenge was successful. Session data is encrypted using @hapi/iron.
The full code version of this example can be found here.
A live demo can be found here.
How it works
- A sign-in form posts email and password to the Next.js API route 
/api/sign-in - The 
signInAPI route calls the Supabase client'ssignInWithEmailmethod and gets back a session object - The 
signInAPI route then calls the Authsignal client'strackmethod to determine if an MFA challenge is required - If a challenge is required, the 
signInAPI route saves the session object in a temporary encrypted cookie and redirects to Authsignal - Once the challenge is completed, Authsignal redirects back to 
/api/callbackwhich retrieves the session and sets the Supabase auth cookies - The 
callbackAPI route then redirects to the index page which is protected with Supabase'swithPageAuthwrapper aroundgetServerSideProps 
Step 1: Configuring an Authsignal tenant
Go to the Authsignal Portal and create a new project and tenant.
You will also need to enable at least one authenticator for your tenant - for example Authenticator Apps.
Finally, to configure the sign-in action to always challenge, go here and set the default action outcome to CHALLENGE and click save.

Step 2: Creating a Supabase project
From your Supabase dashboard, click New project.
Enter a Name for your Supabase project and enter or generate a secure Database Password, then click Create new project.
Once your project is created go to Authentication -> Settings -> Auth Providers and ensure Enable Email provider is checked and that Confirm Email is unchecked.

Step 3: Building a Next.js app
Create a new Next.js project:
_10npx create-next-app --typescript supabase-authsignal-example_10cd supabase-authsignal-example
Create a .env.local file and enter the following values:
_10NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard_10NEXT_PUBLIC_SUPABASE_ANON_KEY=get-from-supabase-dashboard_10AUTHSIGNAL_SECRET=get-from-authsignal-dashboard_10TEMP_TOKEN_SECRET=this-is-a-secret-value-with-at-least-32-characters
Supabase values can be found under Settings > API for your project.
Authsignal values can be found under Settings > API Keys for your tenant.
TEMP_TOKEN_SECRET is used to encrypt the temporary cookie. Set it to a random 32 character length string.
Restart your Next.js development server to read in the new values from .env.local.
_10npm run dev
Step 4: Installing dependencies
Install the Supabase client and Auth helpers for Next.js:
_10npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
Install the Authsignal Node.js client:
_10npm install @authsignal/node
Finally install 2 packages to help encrypt and serialize session data in cookies:
_10npm install @hapi/iron cookie_10npm install --save-dev @types/cookie
Step 5: Initializing the Authsignal client
Add the following code to /lib/authsignal.ts:
_11import { Authsignal } from '@authsignal/node'_11_11const secret = process.env.AUTHSIGNAL_SECRET_11_11if (!secret) {_11  throw new Error('AUTHSIGNAL_SECRET is undefined')_11}_11_11const redirectUrl = 'http://localhost:3000/api/callback'_11_11export const authsignal = new Authsignal({ secret, redirectUrl })
The redirectUrl here is a Next.js API route which Authsignal will redirect back to after an MFA challenge. We'll implement this below.
Step 6: Managing session data in cookies
Next we will add some helper functions for managing cookies:
setTempCookieencrypts and serializes the Supabase session data and sets it in a temporary cookiegetSessionFromTempCookiedecrypts and parses this session data back from the cookiesetAuthCookiesets the Supabase auth cookies (access_tokenandrefresh_token) and clears the temporary cookie
Add the following code to /lib/cookies.ts:
_65import Iron from '@hapi/iron'_65import { Session } from '@supabase/supabase-js'_65import { parse, serialize } from 'cookie'_65import { NextApiRequest, NextApiResponse } from 'next'_65_65export async function setTempCookie(session: Session, res: NextApiResponse) {_65  const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults)_65_65  const cookie = serialize(TEMP_COOKIE, token, {_65    maxAge: session.expires_in,_65    httpOnly: true,_65    secure: process.env.NODE_ENV === 'production',_65    path: '/',_65    sameSite: 'lax',_65  })_65_65  res.setHeader('Set-Cookie', cookie)_65}_65_65export async function getSessionFromTempCookie(req: NextApiRequest): Promise<Session | undefined> {_65  const cookie = req.headers.cookie as string_65_65  const cookies = parse(cookie ?? '')_65_65  const tempCookie = cookies[TEMP_COOKIE]_65_65  if (!tempCookie) {_65    return undefined_65  }_65_65  const session = await Iron.unseal(tempCookie, TEMP_TOKEN_SECRET, Iron.defaults)_65_65  return session_65}_65_65export function setAuthCookie(session: Session, res: NextApiResponse) {_65  const { access_token, refresh_token, expires_in } = session_65_65  const authCookies = [_65    { name: ACCESS_TOKEN_COOKIE, value: access_token },_65    refresh_token ? { name: REFRESH_TOKEN_COOKIE, value: refresh_token } : undefined,_65  ]_65    .filter(isDefined)_65    .map(({ name, value }) =>_65      serialize(name, value, {_65        maxAge: expires_in,_65        httpOnly: true,_65        secure: process.env.NODE_ENV === 'production',_65        path: '/',_65        sameSite: 'lax',_65      })_65    )_65_65  // Also clear the temp cookie_65  const updatedCookies = [...authCookies, serialize(TEMP_COOKIE, '', { maxAge: -1, path: '/' })]_65_65  res.setHeader('Set-Cookie', updatedCookies)_65}_65_65const isDefined = <T>(value: T | undefined): value is T => !!value_65_65const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET!_65const TEMP_COOKIE = 'as-mfa-cookie'_65const ACCESS_TOKEN_COOKIE = 'sb-access-token'_65const REFRESH_TOKEN_COOKIE = 'sb-refresh-token'
Step 7: Building the UI
We will add some form components for signing in and signing up as well as a basic home page.
Add the following code to /pages/sign-up.tsx:
_44import Link from 'next/link'_44import { useRouter } from 'next/router'_44_44export default function SignUpPage() {_44  const router = useRouter()_44_44  return (_44    <main>_44      <form_44        onSubmit={async (e) => {_44          e.preventDefault()_44_44          const target = e.target as typeof e.target & {_44            email: { value: string }_44            password: { value: string }_44          }_44_44          const email = target.email.value_44          const password = target.password.value_44_44          await fetch('/api/sign-up', {_44            method: 'POST',_44            headers: { 'Content-Type': 'application/json' },_44            body: JSON.stringify({ email, password }),_44          }).then((res) => res.json())_44_44          router.push('/')_44        }}_44      >_44        <label htmlFor="email">Email</label>_44        <input id="email" type="email" name="email" required />_44        <label htmlFor="password">Password</label>_44        <input id="password" type="password" name="password" required />_44        <button type="submit">Sign up</button>_44      </form>_44      <div>_44        {'Already have an account? '}_44        <Link href="sign-in">_44          <a>Sign in</a>_44        </Link>_44      </div>_44    </main>_44  )_44}
Then add the following code to /pages/sign-in.tsx:
_48import Link from 'next/link'_48import { useRouter } from 'next/router'_48_48export default function SignInPage() {_48  const router = useRouter()_48_48  return (_48    <main>_48      <form_48        onSubmit={async (e) => {_48          e.preventDefault()_48_48          const target = e.target as typeof e.target & {_48            email: { value: string }_48            password: { value: string }_48          }_48_48          const email = target.email.value_48          const password = target.password.value_48_48          const { state, mfaUrl } = await fetch('/api/sign-in', {_48            method: 'POST',_48            headers: { 'Content-Type': 'application/json' },_48            body: JSON.stringify({ email, password }),_48          }).then((res) => res.json())_48_48          if (state === 'CHALLENGE_REQUIRED') {_48            window.location.href = mfaUrl_48          } else {_48            router.push('/')_48          }_48        }}_48      >_48        <label htmlFor="email">Email</label>_48        <input id="email" type="email" name="email" required />_48        <label htmlFor="password">Password</label>_48        <input id="password" type="password" name="password" required />_48        <button type="submit">Sign in</button>_48      </form>_48      <div>_48        {"Don't have an account? "}_48        <Link href="sign-up">_48          <a>Sign up</a>_48        </Link>_48      </div>_48    </main>_48  )_48}
Now we will use Supabase's withPageAuth wrapper around getServerSideProps to make the home page require authentication via SSR. Replace the existing code in /pages/index.tsx with the following:
_50import { getUser, User, withPageAuth } from '@supabase/auth-helpers-nextjs'_50import { GetServerSideProps } from 'next'_50import { useRouter } from 'next/router'_50import { authsignal } from '../lib/authsignal'_50_50interface Props {_50  user: User_50  isEnrolled: boolean_50}_50_50export const getServerSideProps: GetServerSideProps<Props> = withPageAuth({_50  redirectTo: '/sign-in',_50  async getServerSideProps(ctx) {_50    const { user } = await getUser(ctx)_50_50    const { isEnrolled } = await authsignal.getUser({ userId: user.id })_50_50    return {_50      props: { user, isEnrolled },_50    }_50  },_50})_50_50export default function HomePage({ user, isEnrolled }: Props) {_50  const router = useRouter()_50_50  return (_50    <main>_50      <section>_50        <div> Signed in as: {user?.email}</div>_50        <button_50          onClick={async (e) => {_50            e.preventDefault()_50_50            const { mfaUrl } = await fetch('/api/mfa', {_50              method: 'POST',_50              headers: { 'Content-Type': 'application/json' },_50              body: JSON.stringify({ isEnrolled }),_50            }).then((res) => res.json())_50_50            window.location.href = mfaUrl_50          }}_50        >_50          {isEnrolled ? 'Manage MFA settings' : 'Set up MFA'}_50        </button>_50        <button onClick={() => router.push('/api/sign-out')}>Sign out</button>_50      </section>_50    </main>_50  )_50}
Optional: To make things look a bit nicer, you can add the following to /styles/globals.css:
_47main {_47  min-height: 100vh;_47  display: flex;_47  flex: 1;_47  flex-direction: column;_47  justify-content: center;_47  align-items: center;_47}_47_47section,_47form {_47  display: flex;_47  flex-direction: column;_47  min-width: 300px;_47}_47_47button {_47  cursor: pointer;_47  font-weight: 500;_47  line-height: 1;_47  border-radius: 6px;_47  border: none;_47  background-color: #24b47e;_47  color: #fff;_47  padding: 0 15px;_47  height: 40px;_47  margin: 10px 0;_47  transition: background-color 0.15s, color 0.15s;_47}_47_47input {_47  outline: none;_47  font-family: inherit;_47  font-weight: 400;_47  background-color: #fff;_47  border-radius: 6px;_47  color: #1d1d1d;_47  border: 1px solid #e8e8e8;_47  padding: 0 15px;_47  margin: 5px 0;_47  height: 40px;_47}_47_47a {_47  color: #24b47e;_47  cursor: pointer;_47}
Step 8: Adding the API routes
Now we'll replace the existing api routes in /pages/api/ with 5 new routes:
/sign-in.ts: handles signing in with Supabase and initiating the MFA challenge with Authsignal/sign-up.ts: handles signing up with Supabase/sign-out.ts: clears the Supabase auth cookies and signs the user out/mfa.ts: handles the user's attempt to set up MFA or to manage their existing MFA settings/callback.ts: handles completing the MFA challenge with Authsignal
Add the following code to /pages/api/sign-in.ts:
_27import { supabaseClient } from '@supabase/auth-helpers-nextjs'_27import { NextApiRequest, NextApiResponse } from 'next'_27import { authsignal } from '../../lib/authsignal'_27import { setAuthCookie, setTempCookie } from '../../lib/cookies'_27_27export default async function signIn(req: NextApiRequest, res: NextApiResponse) {_27  const { email, password } = req.body_27_27  const { data, error } = await supabaseClient.auth.api.signInWithEmail(email, password)_27_27  if (error || !data?.user) {_27    return res.send({ error })_27  }_27_27  const { state, url: mfaUrl } = await authsignal.track({_27    action: 'signIn',_27    userId: data.user.id,_27  })_27_27  if (state === 'CHALLENGE_REQUIRED') {_27    await setTempCookie(data, res)_27  } else {_27    setAuthCookie(data, res)_27  }_27_27  res.send({ state, mfaUrl })_27}
Then to handle new sign-ups add the following to /pages/api/sign-up.ts:
_19import { supabaseClient } from '@supabase/auth-helpers-nextjs'_19import { Session } from '@supabase/supabase-js'_19import { NextApiRequest, NextApiResponse } from 'next'_19import { setAuthCookie } from '../../lib/cookies'_19_19export default async function signUp(req: NextApiRequest, res: NextApiResponse) {_19  const { email, password } = req.body_19_19  const { data, error } = await supabaseClient.auth.api.signUpWithEmail(email, password)_19_19  if (error || !isSession(data)) {_19    res.send({ error })_19  } else {_19    setAuthCookie(data, res)_19    res.send({ data })_19  }_19}_19_19const isSession = (data: any): data is Session => !!data?.access_token
To clear the auth cookies on sign-out add the following to /pages/api/sign-out.ts:
_10import { supabaseClient } from '@supabase/auth-helpers-nextjs'_10import { NextApiRequest, NextApiResponse } from 'next'_10_10export default async function signOut(req: NextApiRequest, res: NextApiResponse) {_10  supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: '/sign-in' })_10}
To handle the user's actions to set up MFA or manage their existing MFA settings, add the following to /pages/api/mfa.ts:
_21import { getUser, withApiAuth } from '@supabase/auth-helpers-nextjs'_21import { NextApiRequest, NextApiResponse } from 'next'_21import { authsignal } from '../../lib/authsignal'_21_21export default withApiAuth(async function mfa(req: NextApiRequest, res: NextApiResponse) {_21  if (req.method !== 'POST') {_21    return res.status(405).send({ message: 'Only POST requests allowed' })_21  }_21_21  const { user } = await getUser({ req, res })_21_21  const { isEnrolled } = req.body_21_21  const { url: mfaUrl } = await authsignal.track({_21    action: isEnrolled ? 'manageSettings' : 'enroll',_21    userId: user.id,_21    redirectToSettings: isEnrolled,_21  })_21_21  res.send({ mfaUrl })_21})
Because the user should be authenticated with Supabase to set up or manage MFA, we can use Supabase's withApiAuth wrapper to protect this route.
The redirectToSettings param specifies whether the user should be redirected to the MFA page settings panel after a challenge, rather than redirecting them immediately back to the application.
Finally we need a route to handle the redirect back from Authsignal after an MFA challenge. Add the following to /pages/api/callback.ts:
_19import { NextApiRequest, NextApiResponse } from 'next'_19import { authsignal } from '../../lib/authsignal'_19import { getSessionFromTempCookie, setAuthCookie } from '../../lib/cookies'_19_19export default async function callback(req: NextApiRequest, res: NextApiResponse) {_19  const token = req.query.token as string_19_19  const { success } = await authsignal.validateChallenge({ token })_19_19  if (success) {_19    const session = await getSessionFromTempCookie(req)_19_19    if (session) {_19      setAuthCookie(session, res)_19    }_19  }_19_19  res.redirect('/')_19}
That's it! You should now be able to sign up a new user and set up MFA.
Then if you sign out, you'll be prompted to complete an MFA challenge when signing back in again.
Resources
- To learn more about Authsignal take a look at the API Documentation.
 - You can customize the look and feel of the Authsignal Prebuilt MFA page here.
 
Details
Third-party integrations and docs are managed by Supabase partners.



