Api Platform conference
Register now

Authentication Support

Table of Contents

API Platform Admin delegates the authentication support to React Admin. Refer to the chapter dedicated to authentication in the React Admin documentation for more information.

# HydraAdmin

The authentication layer for HydraAdmin component consists of a few parts, which need to be integrated together.

# Authentication

Add the Bearer token from localStorage to request headers.

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

Extend the Hydra fetch function with custom headers for authentication.

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

# Login Redirection

Redirect users to a /login path, if no token is available in the localStorage.

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

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

# API Documentation Parsing

Extend the parseHydraDocumentaion function from the API Doc Parser library to handle the documentation parsing. Customize it to clear expired tokens when encountering unauthorized 401 response.

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 };
  }
};

# Data Provider

Initialize the hydra data provider with custom headers and the documentation parser.

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

# Export Admin Component

Export the Hydra admin component, and track the users’ authentication status.

// components/admin/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";

// Auth, Parser, Provider calls
const getHeaders = () => {...};
const fetchHydra = (url, options = {}) => {...};
const RedirectToLogin = () => {...};
const apiDocumentationParser = (setRedirectToLogin) => async () => {...};
const dataProvider = (setRedirectToLogin) => {...};

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>
    </>
  );
};
export default Admin;

# Additional Notes

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 authentication layer. It covers:

  • Creating a custom HTTP Client
  • Data and rest data provider configuration
  • Implementation of an auth provider

# Data Provider & HTTP Client

Create a custom HTTP client to add authentication tokens to request headers. Configure the openApiDataProvider, and inject the custom HTTP client into the Simple REST Data Provider for React-Admin.

File: src/components/jsonDataProvider.tsx

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 jsonDataProvider = 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.

The getAccessToken function retrieves the JWT token stored in the browser.

# Authentication and Authorization

Create and export an authProvider object that handles authentication and authorization logic.

File: src/components/authProvider.tsx

interface JwtPayload {
    exp?: number;
    iat?: number;
    roles: string[];
    username: string;
}

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: "",
            fullName: decoded.username,
            avatar: "",
        });
    },
    getPermissions: () => Promise.resolve(""),
};

export default authProvider;

# Export OpenApiAdmin Component

File: src/App.tsx

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

export default () => (
  <OpenApiAdmin
    entrypoint={API_ENTRYPOINT_PATH}
    docEntrypoint={API_DOCS_PATH}
    dataProvider={jsonDataProvider}
    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