The Working Principles of Visitor Authentication

[New in 1.7.0] This guide provides a general approach to adding visitor authentication to a Scrivito-based web application and displaying restricted content only to logged-in visitors.

Scrivito allows visitors to authenticate using identity providers supporting OpenID Connect. As a Scrivito CMS administrator, you can set up identity providers and make them known to your CMS via the dashboard. Editors can then restrict pages to logged-in visitors using Scrivito’s editing interface.

To make a Scrivito-powered application fit for visitor authentication and handling restricted content, three aspects need to be taken care of:

  1. Offering a method for signing in and logging out
  2. Authenticating visitors using OpenId Connect
  3. Integrating visitor authentication with Scrivito

1Logging in and out

To enable the visitors of your website to log in and out, corresponding actions are needed. They can be realized using a simple react component that renders the right action based on the current authentication state:

Copy
  render() {
    return this.state.isLoggedIn
      ? (<a className="visitor_authentication logout" onClick={ this.logout }>Logout</a>)
      : (<a className="visitor_authentication signin" onClick={ this.signin }>Sign In</a>);
  }

How these actions can be implemented, depends on the OpenId Connect library used.

Note that the isLoggedIn state needs to be initialized from a persistent source, which is usually offered by the OpenId Connect library. In an abstract way, this could be realized as follows:

Copy
  componentDidMount() {
    this.unsubscribeObservation = observeVisitorLoggedIn(
      isLoggedIn => { this.setState({ isLoggedIn }); }
    );
  }

  componentWillUnmount() {
    if (this.unsubscribeObservation) {
      this.unsubscribeObservation();
      delete this.unsubscribeObservation;
    }
  }

2Authenticating visitors using OpenId Connect

An OpenId Connect provider is a service that provides a JWT (JSON web token) for an authenticated user. For an application to utilize such a service, it needs to communicate with it for various reasons:

  • Establish a session: Sign in a visitor (with support for failed attempts).
  • Restore a session: Keep the visitor signed in (e.g. after page reload).
  • Terminate a session: Log out (to prevent a visitor from remaining signed in, e.g. after page reload).
  • Obtain a token for a visitor from the provider as a proof of successful authentication.
  • Obtain a new token from the provider before the current token expires.

All these integrations are specific to the provider. Usually, a client library offers the required functions.

3Integrating visitor authentication with Scrivito

The integration includes the following procedures:

  • Making the initial decision whether there is an authenticated visitor already.
  • If an authenticated visitor exists, pass a valid token to Scrivito as soon as possible (no content is loaded and displayed until then).
  • Provide a new token if the one currently in use is about to expire.
  • If the visitor is considered authenticated but no token can be provided, notify the visitor and continue unauthenticated.

None of the above is needed if the Scrivito UI is present because editors never need to log in as visitors to get access to a restricted page. For checking whether an editor is logged in, Scrivito.isEditorLoggedIn is available.

Initial decision

Scrivito needs to be configured dynamically for each individual visitor, consistent with the visitor's authentication state. To this end, the authentication state needs to be persisted.

If, for example, no persisted logged-in state is present at the beginning, visitorAuthentication: false  must be passed to Scrivito.configure, and the login form should be offered where appropriate. However, if, according to the persisted state, the visitor is logged in, and you want this state to be kept alive until the user actively logs out, then visitorAuthentication: true must be specified, independently of whether a valid token exists. If not, a valid token must have existed before, and a new one should be obtained as soon as possible.

The information whether a visitor has already been successfully authenticated could be managed using a boolean flag persisted in the browser’s LocalStorage and updated after signing in and logging out.

Copy
Scrivito.configure({
  // ...
  visitorAuthentication: getVisitorAuthentication(),
});

function getVisitorAuthentication() {
  // do not persist a token in localStorage for security reasons
  return localStorage.getItem('isLoggedIn') === 'true';
}

Passing an ID token to Scrivito

An OpenId Connect service provides access tokens and ID tokens. Scrivito only requires the ID token. Different ways to obtain a token may exist, depending on the live cycle stage of the current session. The ID token can then be passed to Scrivito:

Copy
Scrivito.setVisitorIdToken(idToken);

Replacing a token before it expires

Once your app receives a token from the provider, its expiry time can be determined via the service’s API. Since restricted content is not accessible after the token in use has expired, your application should request a fresh token in a timely manner. A simple timeout will already work:

Copy
function refreshToken(at) {
  const millis = at - new Date().getTime();
  // optional: check millis for reasonable value before use with setTimeout
  return setTimeout(() => {
    obtainIdTokenFromClientLibary().then(newToken => {
      Scrivito.setVisitorIdToken(newToken);
    });
  }, millis);
}

Keep in mind that the token refresh action should be cancelled if the situation requires it, e.g. on log out:

Copy
function refreshTokenWithCancelSupport(at) {
  var timer = refreshToken(at);
  var cancelled = false;
  return () => {
    if (!cancelled) {
      cancelled = true; 
      clearTimeout(timer);
    }
  };
}

Handle failure to obtain a token

Once Scrivito has been configured with visitorAuthentication: true, no content is delivered until a valid token has been passed in. If there is a possibility that the token cannot be obtained quickly enough, you should consider to have your app notify the visitor about this and provide a means to continue browsing. A decent way to do this would be to clear the logged-in state and navigate to a non-restricted page:

Copy
function tokenDidNotArriveYet() {
  localStorage.removeItem('isLoggedIn');
  Scrivito.load(
    () => Scrivito.urlFor(Scrivito.Obj.root())
  ).then(
    url => window.location.assign(url)
  );
}