Migrating to PKCE-compatible generateLink in Supabase
@supabase/auth-helpers-nextjs
v.0.7.0
introduced PKCE authentication flow type and made it the default. This in turn broke existing usage of supabase.auth.generateLink
. This article explains what changed and how to fix it.
What does generateLink do?
Given code like
const linkResponse = await supabase.auth.admin.generateLink({ type: "magiclink", email: "my@email.com", options: { redirectTo: "/hereicome", },});const loginLink = linkResponse.data.properties.action_link;
it generates a loginLink
that looks something like this:
https://supabaseprojectid.supabase.co/auth/v1/verify?token=foobarfoobarfoobarfoobar&type=magiclink&redirect_to=http://example.com/hereicome
Navigating to loginLink
then redirects you to the redirect_to
path with an access token appended to it:
http://example.com/hereicome#access_token=supabasejwtaccesstokenhere&expires_at=1693650070&expires_in=7200&refresh_token=supabaserefreshtokenhere&token_type=bearer&type=magiclink
Old (pre-0.7.0) generateLink flow
In pre-PKCE world, Supabase library’s auth client would detect the hash with access token and automatically update clientside authentication with new access/refresh tokens. This made using generateLink
very easy: just give user the generated link directly and let Supabase clientside library handle the rest.
New (>=0.7.0) PKCE generateLink flow
In PKCE authentication flow, the authentication process includes generating a code verifier on the application server and using it in the verification process. This means that we can no longer rely on clientside library to handle everything, and instead need a serverside component to handle the verification.
Let’s start fixing generateLink
for PKCE by changing our link generation response handling slightly:
const linkResponse = await supabase.auth.admin.generateLink({ type: "magiclink", email: "my@email.com", options: { redirectTo: "/hereicome", },});const tokenHash = linkResponse.data.properties.hashed_token;
Instead of action_link
like previously, we’ll use the hashed token directly this time.
Next up, we’ll have to create a route handler to process our serverside token validation and session handling (from Supabase docs, except with type: "magiclink"
hardcoded):
app/auth/confirm/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'import { cookies } from 'next/headers'import { NextResponse } from 'next/server'export async function GET(req) { const { searchParams } = new URL(req.url) const token_hash = searchParams.get('token_hash') const next = searchParams.get('next') ?? '/' if (token_hash) { const supabase = createRouteHandlerClient({ cookies }) const { error } = await supabase.auth.verifyOtp({ type: "magiclink", token_hash }) if (!error) { return NextResponse.redirect(new URL(`/${next.slice(1)}`, req.url)) } } // return the user to an error page with some instructions return NextResponse.redirect(new URL('/auth/auth-code-error', req.url))}
Finally, we’ll have to build the redirect link to give to the user:
const linkResponse = await supabase.auth.admin.generateLink({ type: "magiclink", email: "my@email.com"});const tokenHash = linkResponse.data.properties.hashed_token;const searchParams = new URLSearchParams({ token_hash: l.data!.properties!.hashed_token, next: "/hereicome"})const loginLink = `/auth/confirm?${searchParams}`;
Note that we also moved redirectTo
from generateLink
to our serverside verification URL, since we don’t use the generateLink
’s provided link anyway.
References
Much of the information is sources from Github PRs and Supabase docs (especially from comments by @kangmingtay and @silentworks)