Managing Authentication in the Frontend
Learn how to manage frontend authentication state, protect authenticated routes and components, and handle unauthorized API responses in backend server integrations.
Overview
In a backend server integration, your backend is responsible for authenticating users with Wristband, managing application sessions, and securely storing access and refresh tokens.
However, your frontend application also needs to know whether a user is authenticated so it can display the appropriate content and handle authentication failures.
The frontend is responsible for three primary tasks:
- Retrieving Authentication State: Calling your backend's Session Endpoint to determine whether the user is authenticated and retrieve relevant session information.
- Protecting Routes and Components: Using authentication state to control access to authenticated pages and conditionally render UI components.
- Handling Unauthorized Responses: Detecting when a user's application session is no longer valid and redirecting them to login when reauthentication is required.
Simplify Frontend Authentication with Wristband's React SDKWristband provides the React Client Auth SDK to simplify authentication state management, route protection, and login redirects in React applications. You can also implement these patterns using your preferred frontend framework.
Retrieving Authentication State
Because application session cookies should be configured with the HttpOnly attribute, frontend JavaScript cannot directly access the application session cookie or its contents.
Instead, your frontend must call the application's Session Endpoint to determine whether the user is authenticated and retrieve the session information needed to initialize the UI.
The Session Endpoint performs the following steps:
- Receive the Session Request: The frontend calls the Session Endpoint, including the application's session cookie.
- Validate the Application Session: The backend retrieves and validates the user's session using the session cookie.
- Return Session Information: If the session is valid, the backend returns the session information needed by the frontend. Otherwise, it returns a
401 Unauthorizedresponse.
The frontend uses the response to initialize its authentication state and determine which pages and components to display.
Using the React Client Auth SDK
Framework-Agnostic Authentication PatternsThe following section use Wristband's React Client Auth SDK to demonstrate frontend authentication patterns. These same concepts can be implemented in other frontend frameworks using their respective state management and routing mechanisms.
For React applications, Wristband provides the @wristband/react-client-auth SDK to manage frontend authentication state.
The SDK includes a WristbandAuthProvider component that calls your backend's Session Endpoint, retrieves the user's session information, and makes authentication state available throughout your React application.
To configure the provider, specify the following URLs:
loginUrl: The URL of your application's Login Endpoint.sessionUrl: The URL of your application's Session Endpoint.
Place the provider at your application's root so its authentication state is available to all child components.
Example: Configuring the WristbandAuthProvider
import { WristbandAuthProvider } from '@wristband/react-client-auth';
function AppRoot() {
return (
<WristbandAuthProvider
loginUrl="/api/auth/login"
sessionUrl="/api/auth/session"
>
<App />
</WristbandAuthProvider>
);
}
Disabling Automatic RedirectsBy default, the
WristbandAuthProviderautomatically redirects users to your application's Login Endpoint when the Session Endpoint returns a401 Unauthorizedresponse. You can disable this behavior by settingdisableRedirectOnUnauthenticated={true}.When redirects are disabled, use the
isAuthenticatedstate returned by theuseWristbandAuthhook to determine whether the user is logged in.
The SDK also provides two hooks for accessing authentication information:
useWristbandAuth(): Provides authentication status, loading state, and functions for managing authentication state.useWristbandSession(): Provides access to user information, tenant information, and optional session metadata returned by the Session Endpoint.
For additional configuration options, see the React Client Auth SDK documentation.
Protecting Frontend Routes and Components
Frontend Route Protection Is Not a Substitute for Backend SecurityFrontend route guards and conditional rendering improve the user experience, but they do not provide a security boundary.
Your backend must independently validate the user's application session and enforce authorization checks for every protected API request. Hiding a page or component in the frontend does not prevent someone from calling its corresponding backend API directly.
Once your frontend has initialized the user's authentication state, it can use that information to control access to pages and components that require authentication.
There are two common approaches:
- Route Protection: Prevent unauthenticated users from accessing specific application pages, such as dashboards, account settings, or other authenticated screens.
- Conditional Rendering: Show or hide individual UI components based on whether the user is authenticated.
Protecting Routes
In React applications, you can implement a route guard that checks the user's authentication status before rendering protected pages.
The useWristbandAuth() hook provides two properties that are particularly useful:
isLoading: Indicates whether the application is still determining the user's authentication status.isAuthenticated: Indicates whether the frontend currently considers the user authenticated.
The following example demonstrates a route guard using React Router and the Wristband React Client Auth SDK.
Example: Protected Route
import { Outlet } from 'react-router-dom';
import {
useWristbandAuth,
redirectToLogin,
} from '@wristband/react-client-auth';
function ProtectedRoute() {
const { isLoading, isAuthenticated } = useWristbandAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return (
<button onClick={() => redirectToLogin('/api/auth/login')}>
Sign In
</button>
);
}
return <Outlet />;
}You can then wrap routes that require authentication with this component.
Example: Protecting Application Pages
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>When the application is still checking authentication status, the guard displays a loading state. Once authentication is confirmed, the protected page is rendered.
If the user is unauthenticated, the guard displays redirect to the login page.
Conditionally Rendering Components
You can also use authentication state to conditionally render individual components without protecting an entire route.
For example, your application might display different navigation options depending on whether a user is authenticated.
Example: Conditional Rendering
import {
useWristbandAuth,
redirectToLogin,
} from '@wristband/react-client-auth';
function Navigation() {
const { isLoading, isAuthenticated } = useWristbandAuth();
if (isLoading) {
return null;
}
return isAuthenticated ? (
<AuthenticatedNavigation />
) : (
<button onClick={() => redirectToLogin('/api/auth/login')}>
Sign In
</button>
);
}This pattern is useful for authentication-aware navigation, personalized content, and other UI elements that depend on the user's login status.
Handling 401 Unauthorized API Responses
Even after a user successfully authenticates, their application session may expire or become invalid while they're using the application. When this happens, subsequent requests to protected backend APIs may fail with a 401 Unauthorized response.
When a backend API receives a request without a valid application session, it should return a 401 Unauthorized response. The frontend is then responsible for handling the authentication failure.
The recommended approach is to redirect the user to your application's Login Endpoint when a protected API returns a 401 Unauthorized response. The Login Endpoint initiates a new authentication flow with Wristband. If the user still has an active Wristband authentication session, they may be automatically authenticated without needing to enter their credentials again.
To simplify error handling, frontend applications should use a centralized mechanism, such as an Axios response interceptor or a Fetch wrapper, to detect 401 responses and initiate the login redirect.
Handling 401 Unauthorized Responses with Axios
If your frontend uses Axios, you can configure a response interceptor to handle 401 Unauthorized responses centrally.
A response interceptor allows you to define common error-handling behavior without repeating the same logic in every API request.
Example: Axios Response Interceptor
// api-client.ts
import axios from 'axios';
import { redirectToLogin } from '@wristband/react-client-auth';
const apiClient = axios.create({
baseURL: '/api',
withCredentials: true,
});
let redirectingToLogin = false;
apiClient.interceptors.response.use(
(response) => response,
(error: unknown) => {
if (
axios.isAxiosError(error) &&
error.response?.status === 401 &&
!redirectingToLogin
) {
redirectingToLogin = true;
redirectToLogin('/api/auth/login');
}
return Promise.reject(error);
}
);
export { apiClient };With this interceptor configured, a protected API returning 401 Unauthorized triggers a redirect to your application's Login Endpoint.
The redirectingToLogin flag prevents multiple simultaneous failed API requests from initiating redundant redirects.
Other errors are propagated back to the calling code for appropriate handling.
Handling 401 Unauthorized Responses with Fetch
If your application uses the Fetch API, you can explicitly check HTTP response status codes and handle authentication failures.
Example: Fetch API
import { redirectToLogin } from '@wristband/react-client-auth';
async function getProjects() {
const response = await fetch('/api/projects', {
credentials: 'include',
});
if (response.status === 401) {
redirectToLogin('/api/auth/login');
return;
}
if (response.status === 403) {
throw new Error('Access denied');
}
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
}For larger applications, consider wrapping Fetch in a reusable API client that applies consistent authentication and error-handling behavior across requests.
Updated about 1 hour ago