Authentication Support

Table of Contents

API Platform Admin delegates the authentication support to React Admin.

Refer to the Auth Provider Setup documentation for more information.

Tip: Once you have set up the authentication, you can also configure React Admin to perform client-side Authorization checks. Refer to the Authorization documentation for more information.

# HydraAdmin

Enabling authentication support for <HydraAdmin> component consists of a few parts, which need to be integrated together.

In the following steps, we will see how to:

  • Make authenticated requests to the API (i.e. include the Authorization header)
  • Redirect users to the login page if they are not authenticated
  • Clear expired tokens when encountering unauthorized 401 response

# Make Authenticated Requests

First, we need to implement a getHeaders function, that will add the Bearer token from localStorage (if there is one) to the Authorization header.

const getHeaders = () =>
    localStorage.getItem("token")
        ? { Authorization: `Bearer ${localStorage.getItem("token")}` }
        : {};

Then, extend the Hydra fetch function to use the getHeaders function to add the Authorization header to the requests.

import { fetchHydra as baseFetchHydra } from "@api-platform/admin";

const fetchHydra = (url, options = {}) =>
    baseFetchHydra(url, {
        ...options,
        headers: getHeaders,
    });

# Redirect To Login Page

Then, we’ll create a <RedirectToLogin> component, that will redirect users to the /login route if no token is available in the localStorage, and call the dataProvider’s introspect function otherwise.

import { Navigate } from "react-router-dom";
import { useIntrospection } from "@api-platform/admin";

const RedirectToLogin = () => {
    const introspect = useIntrospection();

    if (localStorage.getItem("token")) {
        introspect();
        return <></>;
    }
    return <Navigate to="/login" />;
};

# Clear Expired Tokens

Now, we will extend the parseHydraDocumentaion function (imported from the @api-platform/api-doc-parser library).

We will customize it to clear expired tokens when encountering unauthorized 401 response.

import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
import { ENTRYPOINT } from "config/entrypoint";

const apiDocumentationParser = (setRedirectToLogin) => async () => {
    try {
        setRedirectToLogin(false);
        return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders });
    } catch (result) {
        const { api, response, status } = result;
        if (status !== 401 || !response) {
            throw result;
        }

        localStorage.removeItem("token");
        setRedirectToLogin(true);

        return { api, response, status };
    }
};

# Extend The Data Provider

Now, we can initialize the Hydra data provider with the custom fetchHydra (with custom headers) and apiDocumentationParser functions created earlier.

import { hydraDataProvider as baseHydraDataProvider } from "@api-platform/admin";
import { ENTRYPOINT } from "config/entrypoint";

const dataProvider = (setRedirectToLogin) =>
    baseHydraDataProvider({
        entrypoint: ENTRYPOINT,
        httpClient: fetchHydra,
        apiDocumentationParser: apiDocumentationParser(setRedirectToLogin),
    });

# Update The Admin Component

Lastly, we can stitch everything together in the Admin component.

// src/Admin.tsx

import Head from "next/head";
import { useState } from "react";
import { Navigate, Route } from "react-router-dom";
import { CustomRoutes } from "react-admin";
import {
    fetchHydra as baseFetchHydra,
    HydraAdmin,
    hydraDataProvider as baseHydraDataProvider,
    useIntrospection,
} from "@api-platform/admin";
import { parseHydraDocumentation } from "@api-platform/api-doc-parser";
import authProvider from "utils/authProvider";
import { ENTRYPOINT } from "config/entrypoint";

// Functions and components created in the previous steps:
const getHeaders = () => {...};
const fetchHydra = (url, options = {}) => {...};
const RedirectToLogin = () => {...};
const apiDocumentationParser = (setRedirectToLogin) => async () => {...};
const dataProvider = (setRedirectToLogin) => {...};

export const Admin = () => {
  const [redirectToLogin, setRedirectToLogin] = useState(false);

  return (
    <>
      <Head>
        <title>API Platform Admin</title>
      </Head>

      <HydraAdmin
        dataProvider={dataProvider(setRedirectToLogin)}
        authProvider={authProvider}
        entrypoint={window.origin}
      >
        {redirectToLogin ? (
          <CustomRoutes>
            <Route path="/" element={<RedirectToLogin />} />
            <Route path="/:any" element={<RedirectToLogin />} />
          </CustomRoutes>
        ) : (
          <>
            <Resource name=".." list="..">
            <Resource name=".." list="..">
          </>
        )}
      </HydraAdmin>
    </>
  );
};

# Example Implementation

For the implementation of the admin component, you can find a working example in the API Platform’s demo application.

# OpenApiAdmin

This section explains how to set up and customize the <OpenApiAdmin> component to enable authentication.

In the following steps, we will see how to:

  • Make authenticated requests to the API (i.e. include the Authorization header)
  • Implement an authProvider to redirect users to the login page if they are not authenticated, and clear expired tokens when encountering unauthorized 401 response

# Making Authenticated Requests

First, we need to create a custom httpClient to add authentication tokens (via the the Authorization HTTP header) to requests.

We will then configure openApiDataProvider to use ra-data-simple-rest, a simple REST dataProvider for React Admin, and make it use the httpClient we created earlier.

// src/dataProvider.ts

const getAccessToken = () => localStorage.getItem("token");

const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
    options.headers = new Headers({
        ...options.headers,
        Accept: "application/json",
    }) as Headers;

    const token = getAccessToken();
    options.user = { token: `Bearer ${token}`, authenticated: !!token };

    return await fetchUtils.fetchJson(url, options);
};

const dataProvider = openApiDataProvider({
    dataProvider: simpleRestProvider(API_ENTRYPOINT_PATH, httpClient),
    entrypoint: API_ENTRYPOINT_PATH,
    docEntrypoint: API_DOCS_PATH,
});

Note: The simpleRestProvider provider expect the API to include a Content-Range header in the response. You can find more about the header syntax in the Mozilla’s MDN documentation: Content-Range.

Note: The getAccessToken function retrieves the JWT token stored in the browser’s localStorage. Replace it with your own logic in case you don’t store the token that way.

# Creating The AuthProvider

Now let’s create and export an authProvider object that handles authentication and authorization logic.

// src/authProvider.ts

interface JwtPayload {
    sub: string;
    username: string;
}

const getAccessToken = () => localStorage.getItem("token");

const authProvider = {
    login: async ({ username, password }: { username: string; password: string }) => {
        const request = new Request(API_AUTH_PATH, {
            method: "POST",
            body: JSON.stringify({ email: username, password }),
            headers: new Headers({ "Content-Type": "application/json" }),
        });

        const response = await fetch(request);

        if (response.status < 200 || response.status >= 300) {
            throw new Error(response.statusText);
        }

        const auth = await response.json();
        localStorage.setItem("token", auth.token);
    },
    logout: () => {
        localStorage.removeItem("token");
        return Promise.resolve();
    },
    checkAuth: () => (getAccessToken() ? Promise.resolve() : Promise.reject()),
    checkError: (error: { status: number }) => {
        const status = error.status;
        if (status === 401 || status === 403) {
            localStorage.removeItem("token");
            return Promise.reject();
        }

        return Promise.resolve();
    },
    getIdentity: () => {
        const token = getAccessToken();

        if (!token) return Promise.reject();

        const decoded = jwtDecode<JwtPayload>(token);

        return Promise.resolve({
            id: decoded.sub,
            fullName: decoded.username,
            avatar: "",
        });
    },
    getPermissions: () => Promise.resolve(""),
};

export default authProvider;

# Updating The Admin Component

Finally, we can update the Admin component to use the authProvider and dataProvider we created earlier.

// src/Admin.tsx

import { OpenApiAdmin } from "@api-platform/admin";
import authProvider from "./authProvider";
import dataProvider from "./dataProvider";
import { API_DOCS_PATH, API_ENTRYPOINT_PATH } from "./config/api";

export default () => (
    <OpenApiAdmin
        entrypoint={API_ENTRYPOINT_PATH}
        docEntrypoint={API_DOCS_PATH}
        dataProvider={dataProvider}
        authProvider={authProvider}
    />
);

You can also help us improve the documentation of this page.

Made with love by

Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.

Learn more

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop