Handle Token Refresh

You can use refresh tokens to get new access tokens when they expire.

After a user successfully authenticates, Wristband will redirect to your application's Callback Endpoint. As part of the Callback Endpoint, a call is made to Wristband's Token Endpoint to get the access and refresh token for the user. We haven't used these tokens in this quickstart guide since we're relying entirely on the user's session cookie to authenticate requests. However, depending on your architecture, there may be situations where you'll need to utilize the user's access token. Listed below are some common examples of when you'd want to use the access token:

  1. Identity Federation - If your server needs to make API calls to other downstream services (for example, if you have a microservice architecture), then the access token can be used as a bearer token to authenticate requests to those downstream services. Typically, the access token will be placed in the Authorization header of the outbound request. When the downstream service receives the request, it can validate the access token and derive the authenticated user's claims from the token.
  2. Providing Access Tokens To Frontend Clients - In some cases, frontend code running in the browser may need to use the access token to authenticate with backend APIs rather than the session cookie. For example, WebSockets tend to use an access token for authentication rather than cookies. Also, there may be some APIs that the frontend needs to call that don't support cookie authentication but instead require an access token. In cases where the frontend does need to utilize access tokens, we don't recommend storing the access token in the browser since that makes it vulnerable to XSS attacks. Instead, we'd recommend using the Token-Mediating Backend pattern, which stores the tokens on the backend but allows the frontend to retrieve the access token from the backend.
  3. Session Revocation - In this quickstart guide, we've been using stateless sessions, where all the session information is stored within the session cookie. This approach is nice because it's simple to implement and doesn't require any server-side storage for storing session data. However, one downside to this approach is that it's not possible for an outside party to immediately revoke the user's session since it's stored within the user's browser. One workaround to this limitation is to create a short-lived access token (for example, have the token expire after 5 minutes) and then couple the session's validity with the access token's validity (i.e., if the access token expires, then the session should become invalid). Under normal circumstances, the refresh token can be used to retrieve new access tokens to keep the session valid; however, we can manually revoke the refresh token to make the user's session invalid once their current access token expires. While the session invalidation is not immediate, it should take effect within a small amount of time, assuming the access token expiration time is short.

If you decide to utilize the access token within your application, you'll need to update the middleware to refresh the token. Otherwise, you can skip this section.


Refresh Tokens in NextJS Middleware

Update your middleware to call the wristbandAuth.refreshTokenIfExpired() function. This function will handle refreshing the access token if it has expired. If the access token can't be refreshed because the refresh token is no longer valid, then a 401 response is returned.

//
// Middleware for Both Page Router and App Router
// Location -> src/middleware.ts
//
import { NextRequest, NextResponse } from 'next/server';
import { wristbandAuth } from '@/wristband-auth';
import { middlewareGetSession } from '@/session/session';
import { isCsrfTokenValid, setCsrfTokenCookie } from '@/utils/csrf';

const PROTECTED_API_PATH_PREFIXES = ['/api/v1'];
const PROTECTED_PAGE_PATH_PREFIXES = ['/settings', '/account', '/profile'];
const ALL_PROTECTED_PATH_PREFIXES = [...PROTECTED_API_PATH_PREFIXES, ...PROTECTED_PAGE_PATH_PREFIXES];

function isProtectedRequest(req: NextRequest) {
  return ALL_PROTECTED_PATH_PREFIXES.some(prefix => req.nextUrl.pathname.startsWith(prefix));
}

function isCsrfProtectedRequest(req: NextRequest) {
  return PROTECTED_API_PATH_PREFIXES.some(prefix => req.nextUrl.pathname.startsWith(prefix)) 
  && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
}

function unauthorizedResponse(req: NextRequest) {
  return req.nextUrl.pathname.startsWith('/api/')
    ? NextResponse.json({ statusText: 'Unauthorized' }, { status: 401 })
    : NextResponse.redirect('<replace-with-your-application-login-endpoint>');
}

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();

  if (!isProtectedRequest(req)) {
    return res;
  }

  const session = await middlewareGetSession(req, res);
  
  // We'll need to pull the expiresAt and refreshToken values out of the session
  // in order to handle token refreshing.
  const { expiresAt, refreshToken, csrfSecret, isAuthenticated } = session;
  if (!isAuthenticated) {
    return unauthorizedResponse(req);
  }
  
  /* ***** BEGIN TOKEN REFRESH LOGIC ***** */
  
  try {
    // If the access token was not refreshed then tokenData will be null.
    const tokenData = await wristbandAuth.refreshTokenIfExpired(refreshToken, expiresAt);
    if (tokenData) {
      session.expiresAt = Date.now() + tokenData.expiresIn * 1000;
      session.accessToken = tokenData.accessToken;
      session.refreshToken = tokenData.refreshToken;
    }
  } catch (error) {
    console.log(`Token refresh failed: `, error);
    return unauthorizedResponse(req);
  }
  
  /* ***** END TOKEN REFRESH LOGIC ***** */
  
  if (isCsrfProtectedRequest(req)) {
    const isValidCsrf = await isCsrfTokenValid(req, csrfSecret);
    if (!isValidCsrf) {
      return NextResponse.json({ statusText: 'Forbidden' }, { status: 403 });
    }
  }
  
  await setCsrfTokenCookie(csrfSecret, res);
  await session.save();

  return res;
}

export const config = {
  matcher: ['/((?!_next|fonts|examples|[\\w-]+\\.\\w+).*)'],
};


What’s Next

Let's make sure the token refresh logic is working.