diff --git a/admin/authentication-support.md b/admin/authentication-support.md index 48b90bc2f2c..af5cc650271 100644 --- a/admin/authentication-support.md +++ b/admin/authentication-support.md @@ -4,33 +4,38 @@ API Platform Admin delegates the authentication support to React Admin. Refer to [the chapter dedicated to authentication in the React Admin documentation](https://marmelab.com/react-admin/Authentication.html) for more information. -In short, you have to tweak the data provider and the API documentation parser like this: +## HydraAdmin + +The authentication layer for [HydraAdmin component](https://api-platform.com/docs/admin/components/#hydra) +consists of a few parts, which need to be integrated together. + +### Authentication + +Add the Bearer token from `localStorage` to request headers. ```typescript -// components/admin/Admin.tsx +const getHeaders = () => + localStorage.getItem("token") + ? { Authorization: `Bearer ${localStorage.getItem("token")}` } + : {}; +``` -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"; +Extend the Hydra fetch function with custom headers for authentication. -const getHeaders = () => localStorage.getItem("token") ? { - Authorization: `Bearer ${localStorage.getItem("token")}`, -} : {}; +```typescript 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`. + +```typescript const RedirectToLogin = () => { const introspect = useIntrospection(); @@ -40,10 +45,18 @@ const RedirectToLogin = () => { } return ; }; +``` + +### API Documentation Parsing + +Extend the `parseHydraDocumentaion` function from the [API Doc Parser library](https://github.com/api-platform/api-doc-parser) +to handle the documentation parsing. Customize it to clear +expired tokens when encountering unauthorized `401` response. + +```typescript const apiDocumentationParser = (setRedirectToLogin) => async () => { try { setRedirectToLogin(false); - return await parseHydraDocumentation(ENTRYPOINT, { headers: getHeaders }); } catch (result) { const { api, response, status } = result; @@ -51,23 +64,54 @@ const apiDocumentationParser = (setRedirectToLogin) => async () => { throw result; } - // Prevent infinite loop if the token is expired localStorage.removeItem("token"); - setRedirectToLogin(true); - return { - api, - response, - status, - }; + return { api, response, status }; } }; -const dataProvider = (setRedirectToLogin) => baseHydraDataProvider({ - entrypoint: ENTRYPOINT, - httpClient: fetchHydra, - apiDocumentationParser: apiDocumentationParser(setRedirectToLogin), -}); +``` + +### Data Provider + +Initialize the hydra data provider with custom headers and the documentation parser. + +```typescript +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. + +```typescript +// 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); @@ -78,7 +122,11 @@ const Admin = () => { API Platform Admin - + {redirectToLogin ? ( } /> @@ -93,8 +141,133 @@ const Admin = () => { ); -} +}; export default Admin; ``` -For the implementation of the auth provider, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/main/pwa/utils/authProvider.tsx). +### Additional Notes + +For the implementation of the admin component, you can find a working example in the [API Platform's demo application](https://github.com/api-platform/demo/blob/4.0/pwa/components/admin/Admin.tsx). + +## OpenApiAdmin + +This section explains how to set up and customize the [OpenApiAdmin component](https://api-platform.com/docs/admin/components/#openapi) 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](https://github.com/Serind/ra-data-simple-rest). + +**File:** `src/components/jsonDataProvider.tsx` +```typescript +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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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` +```typescript +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(token); + + return Promise.resolve({ + id: "", + fullName: decoded.username, + avatar: "", + }); + }, + getPermissions: () => Promise.resolve(""), +}; + +export default authProvider; +``` + +### Export OpenApiAdmin Component + +**File:** `src/App.tsx` +```typescript +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 () => ( + +); +```