Auth.js
Vercel Connect is available on all plans and is subject to the Vercel Connect terms
Auth.js can use Vercel Connect as an OAuth provider for
signing users into your Next.js application. The @vercel/connect/authjs
entrypoint creates the provider configuration, handles Vercel authentication,
and maps the Connect user profile into an Auth.js user.
This tutorial uses TypeScript and the Next.js App Router.
Before you start, you need:
- A Next.js App Router project linked to Vercel.
- A Vercel Connect connector that supports user authorization.
- A project link between the connector and your Vercel project for each environment that will serve the Auth.js callback.
- The connector UID, such as
oauth/linear, or its opaquescl_...identifier. - An upstream OAuth application, if you use a custom OAuth connector. Register
https://connect.vercel.com/callbackas the redirect URI at the provider. Managed connectors handle this provider callback for you.
Follow the Vercel Connect quickstart to create a connector and link it to your project.
Install Auth.js, its core types, and Vercel Connect:
Terminalpnpm i next-auth@beta @auth/core @vercel/connectTerminalyarn add next-auth@beta @auth/core @vercel/connectTerminalnpm i next-auth@beta @auth/core @vercel/connectTerminalbun add next-auth@beta @auth/core @vercel/connectThe Auth.js adapter requires
@auth/coreversion0.37.0or later.Link your local directory and pull a development OpenID Connect (OIDC) token:
Terminalvercel link vercel env pullThe command writes
VERCEL_OIDC_TOKENto.env.local. Vercel provides this token automatically after deployment. The Connect adapter fetches an OIDC token for every token exchange, so you do not configure a static OAuth client secret.Add your connector UID and an Auth.js secret to
.env.local:.env.localCONNECTOR_LINEAR=oauth/linear AUTH_SECRET=replace-with-a-random-secretGenerate a long, random value for
AUTH_SECRET, and do not commit.env.local.Create an Auth.js configuration and add the result of
connect()toAuthConfig.providers:auth.config.tsimport type { AuthConfig } from '@auth/core'; import { connect } from '@vercel/connect/authjs'; export const authConfig: AuthConfig = { providers: [ connect({ id: 'linear', name: 'Linear', connector: process.env.CONNECTOR_LINEAR!, }), ], };The options control these values:
Option Purpose idAuth.js provider ID. It also becomes the final callback URL path segment. nameProvider name shown in your sign-in interface. connectorConnector UID or opaque scl_...identifier.scopesOptional scopes that replace the default scope list. The example uses
linearas the provider ID. Auth.js therefore handles these callback URLs:- Local:
http://localhost:3000/api/auth/callback/linear - Production:
https://your-domain.example/api/auth/callback/linear
The route must be available on every origin where users sign in. Keep the provider ID consistent when you call
signIn(). Do not register these Auth.js URLs with the upstream provider. The upstream provider redirects to Vercel Connect, and Vercel Connect redirects to your Auth.js callback.- Local:
Create the root Auth.js module:
auth.tsimport NextAuth from 'next-auth'; import { authConfig } from './auth.config'; export const { auth, handlers, signIn, signOut } = NextAuth(authConfig);This module exposes the route handlers, session helper, and server actions used by your application.
Create the Auth.js catch-all route and export both handlers:
app/api/auth/[...nextauth]/route.tsimport { handlers } from '@/auth'; export const { GET, POST } = handlers;Auth.js now owns the sign-in, callback, sign-out, and session endpoints under
/api/auth.Use the provider ID from
connect()when you start the sign-in flow:app/page.tsximport { auth, signIn, signOut } from '@/auth'; export default async function Home() { const session = await auth(); if (!session?.user) { return ( <form action={async () => { 'use server'; await signIn('linear'); }} > <button type="submit">Sign in with Linear</button> </form> ); } return ( <main> <p>Signed in as {session.user.email ?? session.user.name}</p> <form action={async () => { 'use server'; await signOut(); }} > <button type="submit">Sign out</button> </form> </main> ); }Run the application and select Sign in with Linear:
Terminalpnpm devAfter the provider and Vercel Connect consent flows finish, Auth.js redirects the browser to
/api/auth/callback/linearand creates the session.
The provider created by connect() uses the OAuth 2.0 authorization code flow
with Proof Key for Code Exchange (PKCE). Auth.js creates the code verifier and
challenge, keeps the verifier in its secure cookie, and sends the verifier when
it exchanges the authorization code. You do not need to create PKCE parameters
or change the provider's checks setting.
The adapter also:
- Configures the OAuth client without a static client secret.
- Fetches a Vercel OIDC token for each request to the Connect token endpoint.
- Sends the connector identifier and Vercel OIDC token through HTTP Basic authentication for that token exchange.
- Adds
offline_accessonce, even if your custom scope list already contains it, so Vercel Connect can mint refresh tokens.
Without a scopes option, the adapter requests openid, profile, and
email. It then adds offline_access.
Passing scopes replaces the three defaults. Include every OpenID Connect and
provider scope your application needs:
import type { AuthConfig } from '@auth/core';
import { connect } from '@vercel/connect/authjs';
export const authConfig: AuthConfig = {
providers: [
connect({
id: 'linear',
name: 'Linear',
connector: process.env.CONNECTOR_LINEAR!,
scopes: ['openid', 'profile', 'email', 'read'],
}),
],
};This configuration requests openid profile email read offline_access.
Confirm the provider supports each custom scope before adding it.
The Connect userinfo endpoint returns a ConnectProfile:
| Connect field | Auth.js user field | Behavior |
|---|---|---|
sub | id | Required stable user identifier. |
name | name | Optional display name. |
email | email | Uses null when Connect returns no email. |
picture | image | Optional profile image. |
email_verified | Not mapped | Available on ConnectProfile for custom use. |
To change the standard mapping, spread the generated provider and override its
profile function:
import type { AuthConfig } from '@auth/core';
import {
connect,
type ConnectProfile,
} from '@vercel/connect/authjs';
const linear = connect({
id: 'linear',
name: 'Linear',
connector: process.env.CONNECTOR_LINEAR!,
});
export const authConfig: AuthConfig = {
providers: [
{
...linear,
profile(profile: ConnectProfile) {
return {
id: profile.sub,
name: profile.name ?? profile.email ?? profile.sub,
email: profile.email ?? null,
image: profile.picture,
};
},
},
],
};Treat sub as opaque. Do not derive authorization decisions from its format.
Authorization runs in your project's context. During the token exchange, Vercel Connect authenticates the project with its OIDC token and applies the connector's project links. The connector and project must belong to the correct team, and the link must include the current environment.
For local development, refresh the team and project context by running
vercel link and vercel env pull again if the OIDC token expires or points to
the wrong project.
Auth.js, provider API token access, and Vercel Passport solve different tasks:
| Task | Use | Result |
|---|---|---|
| Sign users into your Next.js application through a Connect connector | connect() from @vercel/connect/authjs | An Auth.js user and application session |
| Call a provider API as an app or user | getToken() from @vercel/connect | A short-lived provider access token |
| Require identity before a visitor can reach a deployment | Vercel Passport | Deployment protection enforced before your application route runs |
Do not use the Auth.js session token as a provider API token. If your signed-in
application also needs to call the provider, request a narrowly scoped token
with getToken() and an appropriate subject. See Tokens
for token subjects and provider scopes.
Passport does not create an Auth.js session inside your application. It protects the deployment at the Vercel layer. Use Auth.js when your application needs its own sign-in flow, session callbacks, and user records.
Was this helpful?