Security
React-admin lets you secure your admin app with the authentication strategy of your choice. Since there are many possible strategies (Basic Auth, JWT, OAuth, etc.), react-admin delegates authentication logic to an authProvider
.
Enabling Auth Features
By default, react-admin apps don’t require authentication. To restrict access to the admin, pass an authProvider
to the <Admin>
component.
// in src/App.js
import authProvider from './authProvider';
const App = () => (
<Admin authProvider={authProvider}>
...
</Admin>
);
Once an admin has an authProvider
, react-admin enables a new page on the /login
route, which displays a login form asking for a username and password.
Anatomy Of An authProvider
What’s an authProvider
? Just like a dataProvider
, an authProvider
is an object that handles authentication and authorization logic. It exposes methods that react-admin calls when needed, and that you can call manually through specialized hooks. The authProvider
methods must return a Promise. The simplest authProvider
is:
const authProvider = {
// send username and password to the auth server and get back credentials
login: params => Promise.resolve(),
// when the dataProvider returns an error, check if this is an authentication error
checkError: error => Promise.resolve(),
// when the user navigates, make sure that their credentials are still valid
checkAuth: params => Promise.resolve(),
// remove local credentials and notify the auth server that the user logged out
logout: () => Promise.resolve(),
// get the user's profile
getIdentity: () => Promise.resolve(),
// get the user permissions (optional)
getPermissions: () => Promise.resolve(),
};
Find an existing Auth Provider in the List of Available Auth Providers, or write your own by following the Building Your Own Auth Provider instructions.
Sending Credentials To The API
The authProvider
handles the authentication logic, but it’s the dataProvider
’s responsibility to use the credentials when communicating with the API.
As explained in the Data providers documentation, simpleRestProvider
and jsonServerProvider
take an httpClient
as second parameter. That’s the place where you can change request headers, cookies, etc.
For instance, if the authProvider
stores an authorization token in localStorage, here is how you can tweak the dataProvider
to pass this token as an Authorization
header:
import { fetchUtils, Admin, Resource } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const { token } = JSON.parse(localStorage.getItem('auth'));
options.headers.set('Authorization', `Bearer ${token}`);
return fetchUtils.fetchJson(url, options);
};
const dataProvider = simpleRestProvider('http://localhost:3000', httpClient);
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
...
</Admin>
);
Now the admin is secured: The user can be authenticated and use their credentials to communicate with a secure API.
If you have a custom REST client, don’t forget to add credentials yourself.
Allowing Anonymous Access
As long as you add an authProvider
, react-admin restricts access to all the pages declared in the <Resource>
components. If you want to allow anonymous access, you can set the disableAuthentication
prop in the page components.
For instance, to let anonymous users access the post list view:
const PostList = () => (
<List disableAuthentication>
// ...
</List>
);
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="posts" list={PostList} />
</Admin>
);
disableAuthentication
is available on the following components and hooks:
<Create>
,<CreateBase>
,<CreateController>
anduseCreateController
<Edit>
,<EditBase>
,<EditController>
anduseEditController
<List>
,<ListBase>
,<ListController>
anduseListController
<Show>
,<ShowBase>
,<ShowController>
anduseShowController
Disabling Anonymous Access
Some pages in react-admin apps may allow anonymous access. For that reason, react-admin starts rendering the page layout before knowing if the user is logged in. If all the pages require authentication, this default behaviour creates an unwanted “flash of UI” for users who never logged in, before the authProvider
redirects them to the login page.
If you know your app will never accept anonymous access, you can force the app to wait for the authProvider.checkAuth()
to resolve before rendering the page layout, by setting the <Admin requireAuth>
prop.
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider} requireAuth>
<Resource name="posts" list={PostList} />
</Admin>
);
Restricting Access To Custom Pages
When you add custom pages, they are accessible to anonymous users by default. To make them only accessible to authenticated users, call the useAuthenticated
hook in the custom page:
import { Admin, CustomRoutes, useAuthenticated } from 'react-admin';
const MyPage = () => {
useAuthenticated(); // redirects to login if not authenticated
return (
<div>
...
</div>
)
};
const App = () => (
<Admin authProvider={authProvider}>
<CustomRoutes>
<Route path="/foo" element={<MyPage />} />
<Route path="/anoonymous" element={<Baz />} />
</CustomRoutes>
</Admin>
);
Alternatively, you can use the <Authenticated>
component, e.g. if you can’t modify the page component, or if you want to add authentication in the <Route element>
prop:
import { Admin, CustomRoutes, Authenticated } from 'react-admin';
const MyPage = () => {
return (
<div>
...
</div>
)
};
const App = () => (
<Admin authProvider={authProvider}>
<CustomRoutes>
<Route path="/foo" element={<Authenticated><MyPage /></Authenticated>} />
<Route path="/anoonymous" element={<Baz />} />
</CustomRoutes>
</Admin>
);
Using External Authentication Providers
Instead of the built-in Login page, you can opt for an external authentication provider, such as Auth0, Cognito, or any other OAuth-based service. These services all require a callback URL in the app, to redirect users after login.
React-admin provides a default callback URL at /auth-callback
. This route calls the authProvider.handleCallback
method on mount. This means it’s the authProvider
’s job to use the params received from the callback URL to authenticate future API calls.
For instance, here’s what a simple authProvider for Auth0 might look like:
import { Auth0Client } from './Auth0Client';
export const authProvider = {
async login() { /* Nothing to do here, this function will never be called */ },
async checkAuth() {
const isAuthenticated = await Auth0Client.isAuthenticated();
if (isAuthenticated) {
return;
}
// not authenticated: redirect the user to the Auth0 service,
// where they will be redirected back to the app after login
Auth0Client.loginWithRedirect({
authorizationParams: {
redirect_uri: `${window.location.origin}/auth-callback`,
},
});
},
// A user logged successfully on the Auth0 service
// and was redirected back to the /auth-callback route on the app
async handleCallback() {
const query = window.location.search;
if (query.includes('code=') && query.includes('state=')) {
try {
// get an access token based on the query paramaters
await Auth0Client.handleRedirectCallback();
return;
} catch (error) {
console.log('error', error);
throw error;
}
}
throw new Error('Failed to handle login callback.');
},
async logout() {
const isAuthenticated = await client.isAuthenticated();
// need to check for this as react-admin calls logout in case checkAuth failed
if (isAuthenticated) {
return client.logout({
returnTo: window.location.origin,
});
}
},
...
}
It’s up to you to decide when to redirect users to the third party authentication service, for instance:
- Directly in the
AuthProvider.checkAuth()
method as above; - When users click a button on a custom login page
Handling Refresh Tokens
Refresh tokens are an important security mechanism. In order to leverage them, you should decorate the dataProvider
and the authProvider
so that they can check whether the authentication must be refreshed and actually refresh it.
You can use the addRefreshAuthToDataProvider
and addRefreshAuthToAuthProvider
functions for this purpose. They accept a dataProvider
or authProvider
respectively and a function that should refresh the authentication token if necessary:
// in src/refreshAuth.js
import { getAuthTokensFromLocalStorage } from './getAuthTokensFromLocalStorage';
import { refreshAuthTokens } from './refreshAuthTokens';
export const refreshAuth = () => {
const { accessToken, refreshToken } = getAuthTokensFromLocalStorage();
if (accessToken.exp < Date.now().getTime() / 1000) {
// This function will fetch the new tokens from the authentication service and update them in localStorage
return refreshAuthTokens(refreshToken);
}
return Promise.resolve();
}
// in src/authProvider.js
import { addRefreshAuthToAuthProvider } from 'react-admin';
import { refreshAuth } from 'refreshAuth';
const myAuthProvider = {
// ...Usual AuthProvider methods
};
export const authProvider = addRefreshAuthToAuthProvider(myAuthProvider, refreshAuth);
// in src/dataProvider.js
import { addRefreshAuthToDataProvider } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
import { refreshAuth } from 'refreshAuth';
const baseDataProvider = simpleRestProvider('http://path.to.my.api/');
export const dataProvider = addRefreshAuthToDataProvider(baseDataProvider, refreshAuth);
Customizing The Login Component
Using authProvider
is enough to implement a full-featured authorization system if the authentication relies on a username and password.
But what if you want to use an email instead of a username? What if you want to use a Single-Sign-On (SSO) with a third-party authentication service? What if you want to use two-factor authentication?
For all these cases, it’s up to you to implement your own LoginPage
component, which will be displayed under the /login
route instead of the default username/password form. Pass this component to the <Admin>
component:
// in src/App.js
import { Admin } from 'react-admin';
import MyLoginPage from './MyLoginPage';
const App = () => (
<Admin loginPage={MyLoginPage} authProvider={authProvider}>
...
</Admin>
);
By default, the login page displays a gradient background. If you just want to change the background, you can use the default Login page component and pass an image URL as the backgroundImage
prop.
// in src/MyLoginPage.js
import { Login } from 'react-admin';
const MyLoginPage = () => (
<Login
// A random image that changes everyday
backgroundImage="https://source.unsplash.com/random/1600x900/daily"
/>
);
If you want to build a Login page from scratch, you’ll need the useLogin
hook.
// in src/MyLoginPage.js
import { useState } from 'react';
import { useLogin, useNotify, Notification } from 'react-admin';
const MyLoginPage = ({ theme }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const login = useLogin();
const notify = useNotify();
const handleSubmit = e => {
e.preventDefault();
login({ email, password }).catch(() =>
notify('Invalid email or password')
);
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
name="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</form>
);
};
export default MyLoginPage;
Customizing The Logout Component
// in src/MyLogoutButton.js
import * as React from 'react';
import { forwardRef } from 'react';
import { useLogout } from 'react-admin';
import MenuItem from '@mui/material/MenuItem';
import ExitIcon from '@mui/icons-material/PowerSettingsNew';
// It's important to pass the ref to allow Material UI to manage the keyboard navigation
const MyLogoutButton = forwardRef((props, ref) => {
const logout = useLogout();
const handleClick = () => logout();
return (
<MenuItem
onClick={handleClick}
ref={ref}
// It's important to pass the props to allow Material UI to manage the keyboard navigation
{...props}
>
<ExitIcon /> Logout
</MenuItem>
);
});
export default MyLogoutButton;
Tip: By default, react-admin redirects the user to ‘/login’ after they log out. This can be changed by passing the url to redirect to as parameter to the logout()
function:
// in src/MyLogoutButton.js
// ...
- const handleClick = () => logout();
+ const handleClick = () => logout('/custom-login');
To use it, you must provide a custom UserMenu
:
import MyLogoutButton from './MyLogoutButton';
const MyUserMenu = () => <UserMenu><MyLogoutButton /></UserMenu>;
const MyAppBar = () => <AppBar userMenu={<MyUserMenu />} />;
const MyLayout = ({ children }) => (
<Layout appBar={MyAppBar}>
{children}
</Layout>
);
const App = () => (
<Admin layout={MyLayout}>
// ...
</Admin>
);