Protect Backend Endpoints

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

Previously, during the Wristband SDK setup, we we configured an auth middleware chain with the app.RequireAuthentication middleware function. This section explains how to use it to ensure that protected endpoints are accessible only to authenticated users.

Using the Auth Middleware to Protect Endpoints

To protect an endpoint from unauthenticated access, apply the auth middleware chain as shown below.

// Auth middleware chain configured in "main.go":
// authMiddlewares := goauth.Middlewares{
//   app.RequireAuthentication
// }

// Protected endpoint handler
protectedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"message": "This is a protected route"}`))
})

// Apply middleware to the registered protected endpoint
http.Handle("/api/protected", authMiddlewares.Apply(protectedHandler))

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


Accessing Session Data in Protected Handlers

When using RequireAuthentication middleware, the session is automatically added to the request context. You can access it using goauth.SessionFromContext:

Accessing Session in Handler

func protectedHandler(w http.ResponseWriter, r *http.Request) {
    session := goauth.SessionFromContext(r.Context())
    if session == nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    // Access session data
    userID := session.UserInfo.Sub
    tenantID := session.TenantID
    accessToken := session.AccessToken

    // Use the access token for downstream API calls
    response := map[string]string{
        "userId":   userID,
        "tenantId": tenantID,
        "message":  "Hello from protected endpoint",
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

Handling 401 Responses in Frontend Code

When your frontend makes calls to APIs protected by the authentication middleware, the following error response could come back:

  • 401 Unauthorized: This response will be returned if the session is missing or invalid.

Your frontend needs to handle this error response gracefully to ensure a smooth user experience. Below are common patterns for handling 401 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 error responses. In the example below, the user is redirected to the Login Endpoint if a 401 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' },
  withCredentials: true,
});

// If a 401 response is detected, redirect the user to your Login Endpoint.
const unauthorizedAccessInterceptor = (error: unknown) => {
  if (axios.isAxiosError(error) && error.response?.status === 401) {
    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 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) {
      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.