Authorization Server API

Authorization Server API

Last updated November 26, 2025

The Authorization Server API exposes a set of endpoints which are used by your application for obtaining, refreshing, revoking, and introspecting tokens, as well querying user info:

These endpoints and other features of the authorization server are advertised at the following well-known URL:

https://vercel.com/.well-known/openid-configuration

When the user clicks your Sign in with Vercel button, your application should redirect the user to the Authorization Endpoint (https://vercel.com/oauth/authorize) with the required parameters.

If the user is not logged in, Vercel will show a login screen and then the consent page to grant or deny the requested permissions. If they have already authorized the app, they will be redirected immediately. After approval, Vercel redirects the user back to your application's redirect_uri with a short lived code in the code query parameter.

The Authorization Endpoint supports the following parameters:

ParameterRequiredDescription
client_idYesThe ID of the App, located in the Manage page of the App.
scopeNoA space-separated list of scopes you're requesting: openid, email, profile, and offline_access. If you pass scopes that aren't configured in your app's Manage settings, they're filtered out. If you don't pass scope, all scopes configured in your app are included by default.
redirect_uriYesThe URL used to redirect users back to the application after granting authorization, located in the Manage page of the App under Authorization Callback URLs.
response_typeYesMust be code.
nonceNoA random string generated by the application that is used to protect against replay attacks. The same value will be attached as a claim in the ID Token.
stateNoA random string generated by the application that is used to protect against CSRF attacks.
code_challengeYesA random string generated by the application for additional protection, based on the PKCE specification.
code_challenge_methodYesMust be S256.

In your application create an API Route that saves the state, nonce and code_verifier in cookies and redirects the user to the Authorization Endpoint with the required parameters.

After Vercel redirects the user back to your application's redirect_uri with a code, your application should call the Token Endpoint to exchange the code for tokens.

app/api/auth/authorize/route.ts
import crypto from 'node:crypto';
import { type NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
 
function generateSecureRandomString(length: number) {
  const charset =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const randomBytes = crypto.getRandomValues(new Uint8Array(length));
  return Array.from(randomBytes, (byte) => charset[byte % charset.length]).join(
    '',
  );
}
 
export async function GET(req: NextRequest) {
  const state = generateSecureRandomString(43);
  const nonce = generateSecureRandomString(43);
  const code_verifier = crypto.randomBytes(43).toString('hex');
  const code_challenge = crypto
    .createHash('sha256')
    .update(code_verifier)
    .digest('base64url');
  const cookieStore = await cookies();
 
  cookieStore.set('oauth_state', state, {
    maxAge: 10 * 60, // 10 minutes
    secure: true,
    httpOnly: true,
    sameSite: 'lax',
  });
  cookieStore.set('oauth_nonce', nonce, {
    maxAge: 10 * 60, // 10 minutes
    secure: true,
    httpOnly: true,
    sameSite: 'lax',
  });
  cookieStore.set('oauth_code_verifier', code_verifier, {
    maxAge: 10 * 60, // 10 minutes
    secure: true,
    httpOnly: true,
    sameSite: 'lax',
  });
 
  const queryParams = new URLSearchParams({
    client_id: process.env.NEXT_PUBLIC_VERCEL_APP_CLIENT_ID as string,
    redirect_uri: `${req.nextUrl.origin}/api/auth/callback`,
    state,
    nonce,
    code_challenge,
    code_challenge_method: 'S256',
    response_type: 'code',
    scope: 'openid email profile offline_access',
  });
 
  const authorizationUrl = `https://vercel.com/oauth/authorize?${queryParams.toString()}`;
  return NextResponse.redirect(authorizationUrl);
}

The Token Endpoint is used to exchange the code returned from the Authorization Endpoint, or a Refresh Token for a new Access Token and Refresh Token pair.

ParameterRequiredDescription
grant_typeYesEither authorization_code or refresh_token.
- If the user signs in from the application then authorization_code should be used.
- If the user is already signed in but the Access Token has expired, then refresh_token should be used.
client_idYesThe ID of the App located in the Manage page.
client_secretOptionalThe client secret generated in the Manage page. The client_secret parameter is optional if client authentication is set to none. Setting none is suitable for public applications that cannot securely store secrets, such as SPAs and mobile apps.
codeNoIf grant_type is authorization_code then this parameter is required. The value is obtained during the Authorization Endpoint flow.
code_verifierNoIf grant_type is authorization_code then this parameter is required. It should be the code verifier bound to the code_challenge from the authorization request.
redirect_uriNoIf grant_type is authorization_code then this parameter is required. It should be the same value used in the Authorization Endpoint.
refresh_tokenNoIf grant_type is refresh_token then this parameter is required. This is the Refresh Token which will be used to obtain a new pair of Access and Refresh tokens.

The example below shows how to exchange the code for tokens in Next.js, validating the state and nonce before setting the authentication cookies.

app/api/auth/callback/route.ts
import type { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
 
interface TokenData {
  access_token: string;
  token_type: string;
  id_token: string;
  expires_in: number;
  scope: string;
  refresh_token: string;
}
 
export async function GET(request: NextRequest) {
  try {
    const url = new URL(request.url);
    const code = url.searchParams.get('code');
    const state = url.searchParams.get('state');
 
    if (!code) {
      throw new Error('Authorization code is required');
    }
 
    const storedState = request.cookies.get('oauth_state')?.value;
    const storedNonce = request.cookies.get('oauth_nonce')?.value;
    const codeVerifier = request.cookies.get('oauth_code_verifier')?.value;
 
    if (!validate(state, storedState)) {
      throw new Error('State mismatch');
    }
 
    const tokenData = await exchangeCodeForToken(
      code,
      codeVerifier,
      request.nextUrl.origin,
    );
    const decodedNonce = decodeNonce(tokenData.id_token);
 
    if (!validate(decodedNonce, storedNonce)) {
      throw new Error('Nonce mismatch');
    }
 
    await setAuthCookies(tokenData);
 
    const cookieStore = await cookies();
 
    // Clear the state, nonce, and oauth_code_verifier cookies
    cookieStore.set('oauth_state', '', { maxAge: 0 });
    cookieStore.set('oauth_nonce', '', { maxAge: 0 });
    cookieStore.set('oauth_code_verifier', '', { maxAge: 0 });
 
    // Redirect the user to the profile page, your application may have a different page
    return Response.redirect(new URL('/profile', request.url));
  } catch (error) {
    console.error('OAuth callback error:', error);
    // Redirect the user to the error page, your application may have a different page
    return Response.redirect(new URL('/auth/error', request.url));
  }
}
 
function validate(
  value: string | null,
  storedValue: string | undefined,
): boolean {
  if (!value || !storedValue) {
    return false;
  }
  return value === storedValue;
}
 
function decodeNonce(idToken: string): string {
  const payload = idToken.split('.')[1];
  const decodedPayload = Buffer.from(payload, 'base64').toString('utf-8');
  const nonceMatch = decodedPayload.match(/"nonce":"([^"]+)"/);
  return nonceMatch ? nonceMatch[1] : '';
}
 
async function exchangeCodeForToken(
  code: string,
  code_verifier: string | undefined,
  requestOrigin: string,
): Promise<TokenData> {
  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: process.env.NEXT_PUBLIC_VERCEL_APP_CLIENT_ID as string,
    client_secret: process.env.VERCEL_APP_CLIENT_SECRET as string,
    code: code,
    code_verifier: code_verifier || '',
    redirect_uri: `${requestOrigin}/api/auth/callback`,
  });
 
  const response = await fetch('https://vercel.com/api/login/oauth/token', {
    method: 'POST',
    body: params,
  });
 
  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(
      `Failed to exchange code for token: ${JSON.stringify(errorData)}`,
    );
  }
 
  return await response.json();
}
 
async function setAuthCookies(tokenData: TokenData) {
  const cookieStore = await cookies();
 
  cookieStore.set('access_token', tokenData.access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: tokenData.expires_in,
  });
 
  cookieStore.set('refresh_token', tokenData.refresh_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 30, // 30 days
  });
}

The expected response from the Token Endpoint is a JSON object with the following properties:

Token Endpoint response example
{
  "access_token": "vca_...",
  "token_type": "Bearer",
  "id_token": "...", // The ID Token is a JWT
  "expires_in": 3600,
  "scope": "openid email profile offline_access", // The scopes that were granted to the application
  "refresh_token": "vcr_..." // Present if offline_access scope is requested
}

Both the Access and Refresh Token can be revoked before expiration if needed. If the Access Token is revoked, the Refresh Token is also revoked. The example below shows how to revoke the Access Token in Next.js.

app/api/auth/signout/route.ts
import { cookies } from 'next/headers';
 
export async function POST() {
  const cookieStore = await cookies();
  const accessToken = cookieStore.get('access_token')?.value;
 
  if (!accessToken) {
    return Response.json({ error: 'No access token found' }, { status: 401 });
  }
 
  const credentials = `${process.env.NEXT_PUBLIC_VERCEL_APP_CLIENT_ID}:${process.env.VERCEL_APP_CLIENT_SECRET}`;
 
  const response = await fetch(
    'https://vercel.com/api/login/oauth/token/revoke',
    {
      method: 'POST',
      headers: {
        Authorization: `Basic ${Buffer.from(credentials).toString('base64')}`,
      },
      body: new URLSearchParams({
        token: accessToken,
      }),
    },
  );
 
  if (!response.ok) {
    const errorData = await response.json();
    console.error('Error revoking token:', errorData);
    return Response.json(
      { error: 'Failed to revoke access token' },
      { status: response.status },
    );
  }
 
  cookieStore.set('access_token', '', { maxAge: 0 });
  cookieStore.set('refresh_token', '', { maxAge: 0 });
 
  return Response.json({}, { status: response.status });
}

The token introspection endpoint validates an Access Token or Refresh Token and returns metadata about its state. Use this endpoint to check if a token is active before making API requests.

ParameterRequiredDescription
tokenYesThe token to validate (either Access Token or Refresh Token).

The endpoint returns a JSON response with token metadata:

Token Introspection response
{
  "active": true,
  "client_id": "cl_p4M3ExwwNx2qfEMWQHZfoajUbbYiTR4i",
  "token_type": "bearer",
  "exp": 1757367451,
  "iat": 1757363851,
  "sub": "XLrCnEgbKhsyfbiNR7E849p",
  "iss": "https://vercel.com",
  "jti": "6cd20f0f-0ce2-408b-a21b-63445bccb69a",
  "session_id": "44c44cd9-6b1a-4a16-9296-cc9aea3f1800"
}

The example below shows how to validate a token in Next.js:

app/api/validate-token/route.ts
import { cookies } from 'next/headers';
 
interface IntrospectionResponse {
  active: boolean;
  aud?: string;
  client_id?: string;
  token_type?: 'bearer';
  exp?: number;
  iat?: number;
  sub?: string;
  iss?: string;
  jti?: string;
  session_id?: string;
}
 
export async function GET(): Promise<Response> {
  try {
    const cookieStore = await cookies();
    const token = cookieStore.get('access_token')?.value;
 
    if (!token) {
      return Response.json({ error: 'No access token found' }, { status: 401 });
    }
 
    const introspectResponse = await fetch(
      'https://vercel.com/api/login/oauth/token/introspect',
      {
        method: 'POST',
        body: new URLSearchParams({ token }),
      }
    );
 
    if (!introspectResponse.ok) {
      return Response.json(
        { error: 'Failed to introspect token' },
        { status: 500 }
      );
    }
 
    const introspectionData: IntrospectionResponse =
      await introspectResponse.json();
 
    if (!introspectionData.active) {
      return Response.json({ error: 'Token is not active' }, { status: 401 });
    }
 
    return Response.json({
      message: 'Token is valid',
      tokenInfo: introspectionData,
    });
  } catch (error) {
    console.error('Token validation error:', error);
    return Response.json({ error: 'Internal server error' }, { status: 500 });
  }
}

The user info endpoint returns the consented OpenID claims about the signed-in user. You must authenticate to this endpoint by including an access token as a bearer token in the Authorization header.

The endpoint returns a JSON response with consented OpenID claims:

User Info Endpoint response
{
  "sub": "345e869043f1e55f8bdc837c",
  "email": "user@example.com",
  "email_verified": true,
  "name": "John Doe",
  "preferred_username": "john-doe",
  "picture": "https://vercel.com/api/www/avatar/avatar-42…"
}

The example below shows how to request user info in Next.js:

app/api/user-info/route.ts
import { cookies } from 'next/headers';
 
interface UserInfoResponse {
  sub: string;
  email?: string;
  email_verified?: boolean;
  name?: string;
  preferred_username?: string;
  picture?: string;
}
 
export async function GET(): Promise<Response> {
  try {
    const cookieStore = await cookies();
    const token = cookieStore.get('access_token')?.value;
 
    if (!token) {
      return Response.json({ error: 'No access token found' }, { status: 401 });
    }
 
    const userInfoResponse = await fetch(
      // User Info
      'https://vercel.com/api/login/oauth/userinfo',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );
 
    if (!userInfoResponse.ok) {
      return Response.json(
        { error: 'Failed to fetch user info' },
        { status: 500 }
      );
    }
 
    const userInfoData: UserInfoResponse =
      await userInfoResponse.json();
 
    return Response.json({
      userInfo: userInfoData,
    });
  } catch (error) {
    console.error('Error fetching user info:', error);
    return Response.json({ error: 'Internal server error' }, { status: 500 });
  }
}

Was this helpful?

supported.