React Admin v3: Zoom in the Authentication Layer

François Zaninotto
François ZaninottoOctober 09, 2019
#react#react-admin#tutorial

React-admin v3 is already in beta. While we iron out the last details before it can be released as stable, let's explore what changes in this version.

React-admin v3 doesn't bring many new features, because the framework follows Semantic Versioning. New features will come in minor release (like 3.1, 3.2, etc). Major releases are useful for breaking changes, and the introduction of new programming paradigms. In this new version, we focused on improving the developer experience through the use of hooks. And the changes are pretty dramatic.

Let's see how it affects the Authentication utilities - how you restrict access to certain components, how to display specific messages to anonymous users, and how to write a custom Login form.

Restricting Access To A Custom Page

If you add custom pages to your admin, you may want to restrict them to authenticated users only.

In react-admin 2.x, you had to use a component for that, which expected the current location from react-router:

import { withRouter } from "react-router-dom";
import { Authenticated } from "react-admin";

const MyPage = ({ location }) => (
  <Authenticated location={location}>
    {" "}
    // redirects to login if not authenticated
    <div>...</div>
  </Authenticated>
);

export default withRouter(MyPage);

<Authenticated> calls the authProvider to check if the user is authenticated, and redirects to the login page if the authProvider returns a rejected Promise.

In react-admin v3, you can now use the new useAuthenticated() hook instead:

// in src/MyPage.js
import { useAuthenticated } from "react-admin";

const MyPage = () => {
  useAuthenticated(); // redirects to login if not authenticated
  return <div>...</div>;
};

export default MyPage;

The useAuthenticated hook is optimistic: it doesn't block rendering during the authProvider call. In the above example, the MyPage component renders even before getting the response from the authProvider. If the call returns a rejected promise, the hook redirects to the login page, but the user may have seen the content of the MyPage component for a brief moment.

No need to use useAuthenticated hook in the List or Edit pages. React-admin already restricts access to all the Resource and Dashboard pages. You only need to deal with access restriction for your custom pages.

React-admin v3 keeps the <Authenticated> component to ease the v3 upgrade, and to help restricting access to parts of a component without needing to split it in two.

Displaying Alternative content To Anonymous Users

To avoid rendering a component and force waiting for the authProvider response, use the useAuthState() hook instead of the useAuthenticated() hook. It returns an object with 3 properties:

  • loading: true just after mount, while the authProvider is being called. false once the authProvider has answered
  • loaded: the opposite of loading.
  • authenticated: undefined while loading. then true or false depending on the authProvider response.

You can then render different content depending on the authenticated status.

import { useAuthState } from 'react-admin';

const MyPage = () => {
    const { loading, authenticated } = useAuthState();
    if (loading) {
        return <Loading>;
    }
    if (authenticated) {
        return <AuthenticatedContent />;
    }
    return <AnonymousContent />;
};

There was no equivalent of useAuthState in react-admin v2, so it's a new feature.

Customizing The Login Component

By default, the react-admin Login page asks for a username and a 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.

The implementation of the LoginPage did change quite a bit. In v2, you had to dispatch a Redux action:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { userLogin } from 'react-admin';
import { MuiThemeProvider } from '@material-ui/core/styles';

class MyLoginPage extends Component {
    state = {
        email: '',
        password: '';
    }

    setEmail(email) {
        this.setState({ email });
    }

    setPassword(password) {
        this.setState({ password })
    }

    submit(e) {
        e.preventDefault();
        // Dispatch the userLogin action (injected by connect)
        this.props.userLogin(this.state);
    }

    render() {
        return (
            <MuiThemeProvider theme={this.props.theme}>
                <form onSubmit={this.submit}>
                    <input name="email" type="email" value={email} onChange={e => this.setEmail(e.target.value)} />
                    <input name="password" type="password" value={password} onChange={e => this.setPassword(e.target.value)} />
                </form>
            </MuiThemeProvider>
        );
    }
};

export default connect(undefined, { userLogin })(MyLoginPage);

In react-admin v3, Redux becomes an implementation detail, and you just need to use the useLogin hook.

// in src/MyLoginPage.js
import React, { useState } from "react";
import { useLogin, useNotify } from "react-admin";
import { ThemeProvider } from "@material-ui/styles";

const MyLoginPage = ({ theme }) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const login = useLogin();
  const notify = useNotify();
  const submit = e => {
    e.preventDefault();
    // call the login callback returned by the useLogin hook
    login({ email, password }).catch(() => notify("Invalid email or password"));
  };

  return (
    <ThemeProvider theme={theme}>
      <form onSubmit={submit}>
        <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>
    </ThemeProvider>
  );
};

export default MyLoginPage;

You may have noticed that in the v3 version requires a bit more work: You must handle the failure case and show a notification yourself (using the new useNotify hook). In v2, the login failure notification was handled by react-admin - but you couldn't easily customize it. In v3, it's up to you to handle login failure side effects, but as it's a one liner, it shouldn't be too much work.

New authProvider Signature

You may not notice it at first, but react-admin now uses a new authProvider signature based on an object rather than a function. You don't need to change anything in your v2 authProvider because we've added a compatibility layer.

As a reminder, here was the authProvider structure in v2:

// in src/authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from "react-admin";

export default (type, params) => {
  if (type === AUTH_LOGIN) {
    // ...
    return Promise.resolve();
  }
  if (type === AUTH_LOGOUT) {
    // ...
    return Promise.resolve();
  }
  if (type === AUTH_ERROR) {
    // ...
    return Promise.resolve();
  }
  if (type === AUTH_CHECK) {
    return Promise.resolve();
  }
  if (type === AUTH_GET_PERMISSIONS) {
    return Promise.resolve();
  }
  return Promise.reject("Unknown method");
};

In react-admin v3, here is how an authProvider looks like:

const authProvider = {
  login: params => Promise.resolve(),
  logout: params => Promise.resolve(),
  checkAuth: params => Promise.resolve(),
  checkError: error => Promise.resolve(),
  getPermissions: params => Promise.resolve(),
};

It's more idiomatic to JavaScript, and besides, it makes it possible to check the authProvider structure at compile time using TypeScript. React-admin exports an AuthProvider type for that purpose.

Conclusion

The move to react 16.8 and hooks opened a whole new world of developer experience. We believe the new react-admin hooks will allow better maintainability, will ease the learning curve, and facilitate reusability of custom business logic across components and projects. The Marmelab developers who already worked with react-admin v3 now think the v2 API is painful.

So don't wait too long, and make the switch to v3! It's in beta, but it's really worth it.

Did you like this article? Share it!