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.
NoteThe
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-TOKEN
header, 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 xsrfHeaderName
when 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 theX-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) {
...
}
}
Updated about 2 hours ago
Now that you've finished protecting your backend endpoints, let's run some final tests to ensure everything is working.