Authenticating Visitors with Auth0

[New in 1.7.0] Auth0 is an OpenID Connect service provider and, as such, suitable for covering the visitor authentication part on a Scrivito-based website. Auth0 also supports third-party authentication services so that visitors can log in via the account of their choice, e.g. Google.

In this tutorial, we are going to provide you with the knowledge and the code needed to enable visitors to log in to the Scrivito Example App via Auth0. We will be using Auth0’s client library and provide some wrappers around the Auth0 API to make it easier to use.

Prerequisites

To follow along, you’ll need a locally installed Scrivito Example App as well as an Auth0 account and an Auth0 client configuration. The latter can be obtained by creating an application of the “Single Page Web Application” type via your Auth0 dashboard.

You will need your Auth0 client ID and secret as well as the domain (as a URL, i.e. https://*.auth0.com) for setting up the visitor authentication provider in your Scrivito CMS’s dashboard.

Next, make your application known to Auth0. So, on your Auth0 dashboard, add your application's domain to the “Allowed Web Origins” (e.g. http://localhost:8080 for a local development environment), and your homepage to the “Allowed Callback URLs” as well as the “Allowed Logout URLs” (e.g. http://localhost:8080/).

Then, in your Example App, add your Auth0 client ID and domain to your “.env” file and make webpack recognize them:

.env
Copy
SCRIVITO_TENANT=...

# From your client application settings at Auth0.com:
AUTH0_CLIENT_ID=...
AUTH0_DOMAIN=....auth0.com
webpack.config.js
Copy
  const plugins = [
    new webpack.EnvironmentPlugin({
      // ...
      // Value is taken from .env
      AUTH0_CLIENT_ID: "",
      AUTH0_DOMAIN: "",
      // ...

Finally, before diving into the code, make sure that the Auth0 client library has been installed:

Copy
npm install --save auth0-js

Create the React component for logging in and out

For logging in or out, our simple React component, LoginWithAuth0, renders the corre­spond­ing link, depending on the current login status. The component maintains the login status using a state variable, isLoggedIn. For the component to be rerendered when the login status changes, isLoggedIn is set via two callbacks provided after mounting, notifyOnLogin and notifyOnTokenFailure.

SHOW LOGINWITHAUTH0 COMPONENT
src/Components/LoginWithAuth0.js
Copy
import * as React from "react";
import {
  loginVisitor,
  logoutVisitor,
  notifyOnLogin,
  notifyOnTokenFailure,
} from "../Auth/VisitorIdentityProvider";

export class LoginWithAuth0 extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isLoggedIn: false };
    this.signin = this.signin.bind(this);
    this.logout = this.logout.bind(this);
  }

  componentDidMount() {
    notifyOnLogin(() => this.setState({ isLoggedIn: true }));
    notifyOnTokenFailure(() => this.setState({ isLoggedIn: false }));
  }

  render() {
    return this.state.isLoggedIn ? (
      <a href="#" className="text-danger strong" onClick={this.logout}>
        Log out
      </a>
    ) : (
      <a href="#" className="text-white strong" onClick={this.signin}>
        Sign in
      </a>
    );
  }

  signin(e) {
    e.stopPropagation();
    e.preventDefault();
    loginVisitor();
  }

  logout(e) {
    e.stopPropagation();
    e.preventDefault();
    logoutVisitor();
  }
}

Note that logged-in editors always have access to restricted content; they don’t need to additionally sign in as a visitor.

LoginWithAuth0 imports loginVisitor, logoutVisitor and two state-change callback functions, notifyOnLogin and notifyOnTokenFailure, from an integration script that connects the component at the top level with our low-level authentication functionality we will provide further down.

The integration layer

Our integration script, VisitorIdentityProvider, instantiates an authentication object, auth0, from the Auth class provided further down and defines two callbacks for handling login success and failure (onTokenSuccess and onTokenFailure).

In addition to the above-mentioned functions needed by our component to interact with the authentication part, the script exports getVisitorAuthentication, a function that determines whether the visitor has logged in – or is about to be logged in – or not. We will later call this functiongetVisitorAuthentication from within the app’s initialization code to switch visitor authentication mode on or off in the Scrivito Example App.

SHOW SCRIVITO’S VISITORIDENTITYPROVIDER INTEGRATION
src/Auth/VisitorIdentityProvider.js
Copy
import * as Scrivito from "scrivito";
import { Auth } from "./Auth";

const auth0 = new Auth({
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  redirectUri: `${window.location.origin}/`,
});

auth0.onTokenSuccess(() => {
  Scrivito.setVisitorIdToken(auth0.getIdToken());
});

auth0.onTokenFailure(() => {
  auth0.logout();
});

export function loginVisitor() {
  auth0.login();
}

export function logoutVisitor() {
  auth0.logout();
}

export function notifyOnLogin(callback) {
  auth0.onTokenSuccess(() => {
    callback();
  });
}

export function notifyOnTokenFailure(callback) {
  auth0.onTokenFailure(() => {
    callback();
  });
}

export function getVisitorAuthentication() {
  if (Scrivito.isEditorLoggedIn()) {
    return false;
  }
  const hash = window.location.hash;

  if (hash && hash.substr(0, 13) === "#access_token") {
    auth0.handleAuthentication();
    history.pushState("", document.title, window.location.pathname);
    return true;
  }

  if (auth0.isLoggedIn()) {
    auth0.renewSession();
    return true;
  }

  return false;
}

In the onTokenSuccess callback, the token is passed to the SDK using Scrivito.setVisitorIdToken causing restricted content to become available to the visitor.

getVisitorAuthentication is called when the app is initialized and Scrivito.configure is called (in “src/config/scrivito.js”) in order to, among other things, activate visitor authentication mode. getVisitorAuthentication only takes account of authentication if no editor is logged in (Scrivito.isEditorLoggedIn). If this is the case, it determines the login status from the URL hash or via the isLoggedIn helper (provided below) and triggers the creation of the authentication session. Otherwise, false is returned to indicate that authentication is not in process.

Managing authentication sessions 

Based on code Auth0 had provided some time ago (which is no longer available), we’ve compiled and adapted the interface functions needed by our above integration layer to establish, renew or end authentication sessions:

SHOW AUTH0 CLIENT INTERFACE
src/Auth/Auth.js
Copy
import { WebAuth } from "auth0-js";

const ONE_DAY_MILLIS = 24 * 60 * 60 * 1000;

export class Auth {
  constructor({ domain, clientId, redirectUri }) {
    this.webAuth = new WebAuth({
      domain,
      clientID: clientId,
      redirectUri,
      responseType: "token id_token",
      scope: "openid profile email",
      leeway: 30,
    });
    this.tokenSuccessCallbacks = [() => this.refreshSessionBeforeExpiration()];
    this.tokenFailureCallbacks = [];
    this.logoutCallbacks = [() => this.clearSessionRenewal()];

    this.accessToken = null;
    this.idToken = null;
    this.expiresAt = 0;
  }

  // token lifecycle hooks

  onTokenSuccess(callback) {
    return addEntry(this.tokenSuccessCallbacks, callback);
  }

  onTokenFailure(callback) {
    return addEntry(this.tokenFailureCallbacks, callback);
  }

  onLogout(callback) {
    return addEntry(this.logoutCallbacks, callback);
  }

  // state

  getIdToken() {
    return this.idToken;
  }

  isLoggedIn() {
    return localStorage.getItem("isLoggedIn") === "true";
  }

  // life cycle

  login() {
    this.webAuth.authorize({});
  }

  handleAuthentication() {
    this.webAuth.parseHash({}, (err, authResult) => {
      this.handleAuthResult("login", authResult, err);
    });
  }

  renewSession() {
    this.webAuth.checkSession({}, (err, authResult) => {
      this.handleAuthResult("revive", authResult, err);
    });
  }

  logout(returnTo = window.location.origin) {
    this.accessToken = null;
    this.idToken = null;
    this.expiresAt = 0;

    localStorage.removeItem("isLoggedIn");

    this.webAuth.logout({ returnTo });
    this.logoutCallbacks.forEach(callback => callback());
  }

  // private parts

  setSession({ accessToken, idToken, expiresIn }) {
    localStorage.setItem("isLoggedIn", "true");

    this.accessToken = accessToken;
    this.idToken = idToken;
    this.expiresAt = expiresIn * 1000 + new Date().getTime();

    this.tokenSuccessCallbacks.forEach(callback => callback());
  }

  handleAuthResult(action, authResult, err) {
    const error = this.validateAuthResult(authResult, err);
    if (error) {
      this.tokenFailureCallbacks.forEach(callback => callback(action));
      return;
    }

    this.setSession(authResult);
  }

  validateAuthResult(authResult, err) {
    const missing = [];
    if (authResult) {
      ["accessToken", "idToken"].forEach(p => {
        if (!authResult[p]) {
          missing.push(p);
        }
      });
    } else {
      missing.push("no authResult");
    }

    if (!missing.length) {
      return;
    }

    return (
      (err && err.error) ||
      new Error(
        `Failed to obtain session (no error). Missing: ${missing.join(", ")}`
      )
    );
  }

  refreshSessionBeforeExpiration() {
    this.clearSessionRenewal();

    const beforeExpiration = this.expiresAt - new Date().getTime() - 10000;
    const millisUntilRenewal =
      beforeExpiration > ONE_DAY_MILLIS ? ONE_DAY_MILLIS : beforeExpiration;
    this.expirationTimeout = setTimeout(() => {
      this.renewSession();
    }, millisUntilRenewal);
  }

  clearSessionRenewal() {
    if (this.expirationTimeout) {
      clearTimeout(this.expirationTimeout);
      this.expirationTimeout = undefined;
    }
  }
}

function addEntry(list, entry) {
  list.push(entry);
  return () => {
    const index = list.indexOf(entry);
    list.splice(index, 1);
  };
}

Auth0’s client library provides the API needed via the WebAuth class. An instance of this class, webAuth, is created when an instance of Auth is created in the integration layer above. There’s two things we would like to elaborate on for a better understanding:

Other parts of the application (like our component above) can register callbacks to get notified on visitor authentication status changes (tokenSuccess, tokenFailure, logout). This keeps any listener code out of this module. The callbacks are accumulated in arrays that are iterated if an event fires. See the addEntry and logout, setSession, and handleAuthResult functions.

The login status is persisted in the setSession and logout functions by setting or, respectively, clearing the isLoggedIn item in the browser’s local storage. This item is queried by the isLoggedIn function, which is called wherever the status needs to be determined, e.g. in our above LoginWithAuth0 component.

Enabling visitor authentication at startup

As mentioned above, every time the login status changes, the visitor’s access to restricted content changes as well. Therefore, the app needs to be restarted so that it gets initialized with visitor authentication mode switched on or off. This is achieved by calling Scrivito.configure and setting the visitorAuthentication property in the configuration object passed in:

config/scrivito.js
Copy
import * as Scrivito from "scrivito";
import { getVisitorAuthentication } from '../Auth/VisitorIdentityProvider';

config = {
  tenant: process.env.SCRIVITO_TENANT,
  // ...
  visitorAuthentication: getVisitorAuthentication(),
};

// ...

Scrivito.configure(config);

The getVisitorAuthentication function is defined in the interface script above where further details are given.

We’re done – let’s try it out!

Using the LoginWithAuth0 component

All that is left to do is to render our component for logging in and out. As we did in our tutorial about logging in with Google, we’ve added it to the Example App’s navigation:

src/Components/Navigation/FullNavigation.js
Copy
// Other imports
import LoginWithAuth0 from "../LoginWithAuth0";

class FullNavigation extends React.Component {
⋮
          <Collapse isOpen={this.state.expanded} navbar>
            <div className="navbar-collapse">
              <LoginWithAuth0 />
              <Nav
                closeExpanded={this.closeExpanded}
                expanded={this.state.expanded}
              />
            </div>
          </Collapse>
⋮

Congratulations!

This is even more than just the basic functionality for editor login via Auth0 (session renewal is also included)! If you additionally would like to utilize the logged-in user’s profile, e.g. to display the visitor’s name, see the documentation at Auth0 for details.

Auth0’s authentication services are highly configurable. They not only let you define users, roles, rules, hooks, and a lot more, but also support a host of “social connections” you can activate to let visitors log in with them. In order to fully integrate such services, a client ID and secret need to be obtained from each and then specified in your Auth0 “social connections” setup.