Protect Backend Endpoints

Learn how the auth dependency can be used to protect authenticated APIs.

Previously, during the Wristband SDK setup, we created an auth dependency named require_session_auth. This section explains how to use it to ensure that protected endpoints are accessible only to authenticated users.

Using the Auth Dependency to Protect Endpoints

To protect an endpoint from unauthenticated access, add require_session_auth as a dependency, as shown below

# src/routes/protected_routes.py
from fastapi import APIRouter, Depends, Response, status
from auth.wristband import require_session_auth

router = APIRouter()

# Use the require_session_auth dependency in your dependencies list to verify that 
# incoming requests have a valid session.
@router.get("/protected-api", dependencies=[Depends(require_session_auth)])
async def protected_api() -> Response:
    return { "message": "This is a protected endpoint" }

Now, if somebody tries to call this API without a valid session, a 401 Unauthorized response will be returned.

How to Send CSRF Tokens for Protected Endpoints

Endpoints that use the require_session_auth dependency, automatically enforce CSRF protection using the Synchronizer Token pattern.

💡

What is Cross-Site Request Forgery (CSRF)?

CSRF is a type of web security vulnerability where an attacker tricks a user into unknowingly submitting a request to a web application where they're already authenticated. Since browsers automatically include cookies with requests, the malicious request appears legitimate to the server. This allows attackers to perform actions on behalf of the user without their consent.

To pass CSRF protection checks, each request must include a valid CSRF token in the X-CSRF-TOKEN header. The CSRF token can be obtained from the CSRF-TOKEN cookie that is returned from the server.

📘

Note

The CSRF-TOKEN cookie is automatically created by the Wristband session middleware, which was configured previously .

If a request doesn't include a valid X-CSRF-TOKENheader, the require_session_auth dependency returns a 403 Forbidden response.

Below are examples showing how to include the CSRF token in the X-CSRF-TOKEN header for popular JavaScript HTTP clients.

Axios

Axios has built-in support for sending CSRF tokens; you just need to specify the xsrfCookieName and xsrfHeaderNamewhen creating the Axios client:

// api-client.ts
import axios from 'axios';

const apiClient = axios.create({
  baseURL: `<backend-apis-base-url>`,
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  xsrfCookieName: 'CSRF-TOKEN',
  xsrfHeaderName: 'X-CSRF-TOKEN',
});

export { apiClient };

Fetch

Fetch doesn't have automatic handling for CSRF, so you'll have to implement the logic of extracting the CSRF token from the cookie and passing it in the X-CSRF-TOKEN request header from scratch:

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]*)'));
  return match ? decodeURIComponent(match[2]) : null;
}

async function executeApiCall() {
  const csrfToken = getCookie('CSRF-TOKEN');

  try {
    const response = await fetch('/api/protected-endpoint', {
      credentials: 'include',
      headers: {
        'X-CSRF-TOKEN': csrfToken ?? ''
      }
    });

    ...
  } catch (error) {
    ...
  }
}

Handling 401 and 403 Responses in Frontend Code

When your frontend makes calls to APIs protected by the require_session_auth dependency, the following error responses could come back:

  • 401 Unauthorized: This response will be returned if the session is missing or invalid.
  • 403 Forbidden: This response will be returned if the value in the X-CSRF-TOKEN request header is invalid.

Your frontend needs to handle these error responses gracefully in order to ensure a smooth user experience. Below are common patterns for handling 401 and 403 errors in a JavaScript frontend.

Pattern 1: Use an Axios Interceptor

If you're using Axios, you can create a response interceptor to handle 401 and 403 error responses. In the example below, the user is redirected to the Login Endpoint if a 401 or 403 response is detected.

// api-client.ts
import axios from 'axios';
import { redirectToLogin } from '@wristband/react-client-auth';

const apiClient = axios.create({
  baseURL: '<backend-apis-base-url>',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  xsrfCookieName: 'CSRF-TOKEN',
  xsrfHeaderName: 'X-CSRF-TOKEN',
});

// If a 401 or 403 response is detected, redirect the user to your Login Endpoint.
const unauthorizedAccessInterceptor = (error: unknown) => {
  if (axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 403)) {
    redirectToLogin('<your-login-endpoint-url>');
    return;
  }
  return Promise.reject(error);
};

apiClient.interceptors.response.use(undefined, unauthorizedAccessInterceptor);

export { apiClient };

Pattern 2: Explicitly Catch Errors When Making API Calls

To handle 401 and 403 errors with more precision, you can explicitly catch them when calling your backend APIs. This allows for custom error-handling logic to be created for each API call.

import axios from 'axios';
import { redirectToLogin } from '@wristband/react-client-auth';

async function executeApiCall() {
  try {
    const response = await axios.get('<your-server-api-url>');
    alert('Success!');
  } catch (error: unknown) {
    if (axios.isAxiosError(error) && (error.response?.status === 401 || error.response?.status === 403)) {
      redirectToLogin('<your-login-endpoint-url>');
    } else {
      console.error('Unexpected error:', error);
      alert('Something went wrong!');
    }
  }
}
import { redirectToLogin } from '@wristband/react-client-auth';

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]*)'));
  return match ? decodeURIComponent(match[2]) : null;
}

async function executeApiCall() {
  const csrfToken = getCookie('CSRF-TOKEN');

  try {
    const response = await fetch('/api/protected-endpoint', {
      credentials: 'include',
      headers: {
        'X-CSRF-TOKEN': csrfToken ?? ''
      }
    });

    if (!response.ok) {
      if (response.status === 401 || response.status === 403) {
        redirectToLogin('<your-login-endpoint-url>');
        return;
      }

      const errorText = await response.text();
      throw new Error(`HTTP error! status: ${response.status}, Message: ${errorText}`);
    }

    ...
  } catch (error) {
    ...
  }
}

What’s Next

Now that you've finished protecting your backend endpoints, let's run some final tests to ensure everything is working.