Api Platform conference
Register now
main Authentication Support

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