You have been redirected from an outdated version of the article. Below is the content available on this topic. To view the old article click here.

Two Factor Authentication

Structr supports the TOTP (Time-Based One-Time Password) flavor of two-factor authentication and supports multiple configuration options to enable two-factor-authentication. They can be configured via structr.conf or the configuration servlet (http://<your-structr>/structr/config).

Note: Because the TOTP algorithm is used, it is important that the clocks of both factors (server and mobile device) are synced to a NTP server so they are as close as possible.

Configuration Options

security.twofactorauthentication.level
  • 0 = off
    Two-Factor Authentication is turned off completely
  • 1 = optional (Default)
    Two-Factor Authentication can be turned on on a per-user basis.
  • 2 = forced
    Every user is forced to use Two-Factor Authentication
security.twofactorauthentication.issuer

The issuer parameter is a string value indicating the provider or service this account is associated with, URL-encoded according to RFC 3986. If the issuer parameter is absent, issuer information may be taken from the issuer prefix of the label. If both issuer parameter and issuer label prefix are present, they should be equal.

Valid values corresponding to the label prefix examples above would be: issuer=Example, issuer=Provider1, and issuer=Big%20Corporation.

Older Google Authenticator implementations ignore the issuer parameter and rely upon the issuer label prefix to disambiguate accounts. Newer implementations will use the issuer parameter for internal disambiguation, it will not be displayed to the user. We recommend using both issuer label prefix and issuer parameter together to safely support both old and new Google Authenticator versions.

security.twofactorauthentication.algorithm

The algorithm may have the values:

  • SHA1 (Default)
  • SHA256
  • SHA512

Warning: Changing this setting after users are already confirmed will effectively lock them out. Set <User>.twoFactorConfirmed to false to show them a new QR code.

security.twofactorauthentication.digits

OPTIONAL: The digits parameter may have the values 6 or 8, and determines how long of a one-time passcode to display to the user. The default is 6.

Warning: Changing this setting after users are already confirmed may lock them out. Set <User>.twoFactorConfirmed to false to show them a new QR code.

security.twofactorauthentication.period

OPTIONAL only if type is totp: The period parameter defines a period that a TOTP code will be valid for, in seconds. The default value is 30.

Warning: Changing this setting after users are already confirmed may lock them out. Set <User>.twoFactorConfirmed to false to show them a new QR code.

security.twofactorauthentication.logintimeout

Defines how long the two-factor login time window in seconds is. After entering the username and password the user has this amount of time to enter a two factor token before he has to re-authenticate via password. Default is 30.

security.twofactorauthentication.loginpage

The application page where the user enters the current two factor token. Until the user has verified his two factor token (by logging in once), the user is shown a QR code after every login attempt. Default is /twofactor

Note: To allow maximum flexibility, this page is part of the web-application and not part of Structr.

security.twofactorauthentication.whitelistedIPs

A comma-separated (,) list of IPs for which two factor authentication is disabled.

User configuration

There are three main properties you can change to control the two factor authentication behaviour for a specific user node.

  • User.twoFactorConfirmed
    This is automatically set to true after a user authenticates via two-factor authentication. If this is set to false the user will always be shown the QR code for him to scan.
  • User.isTwoFactorUser
    Controls if the user wants to authenticate via two factor authentication. This only works if the setting TwoFactor.level is set to 1. If the setting is set to 2, the flag will automatically be set to true after a user logs in.
  • User.twoFactorSecret
    This is the secret which is used to generate tokens. It is automatically generated for every user.

Example implementation

  1. Create the basic login page as per usual
    • If this is done using Edit Mode Binding no changes are necessary
    • If the login is done with custom JavaScript we need to react to status code 202 like so:
    202: function(resp, txt, xhr) {
      var redirectUrl = xhr.getResponseHeader('twoFactorLoginPage') + '?token=' + xhr.getResponseHeader('token');
    
      var qrdata = xhr.getResponseHeader('qrdata');
      if (qrdata) {
        redirectUrl += '&qrdata=' + qrdata;
      }
    
      window.location.href = redirectUrl;
    }
    

    Login Page

  2. Create the TwoFactor.loginpage with the following HTML nodes with the given Edit Mode Binding configuration. (since the qrdata was transferred as URL-safe base64 and img tags only support regular base64 data URIs we need to convert it).
    Note: Since this form uses Edit Mode Binding it is highly advised to create real HTML nodes in structr for the below HTML.
    <div data-structr-meta-hide-conditions="empty(request.qrdata)">
      <img id="qrimage" src="data:image/png;base64, ${str_replace(str_replace(request.qrdata, '_', '/'), '-', '+')}" style="margin-left: calc(50% - 100px);">
      <div>Scan this QR code to use two factor authentication</div>
    </div>
    
    <form action="#" id="loginForm">
      <input name="twoFactorToken" type="hidden" value="${request.token}" data-structr-name="twoFactorToken">
      <div class="form-group">
        <label class="control-label" for="password">Two Factor Code</label>
        <input class="form-control" name="twoFactorCode" required="" type="text" data-structr-name="twoFactorCode">
      </div>
      <button data-structr-action="login" data-structr-attributes="twoFactorToken, twoFactorCode" data-structr-return="/dashboard">Login</button>
    </form>
    

    Example Two Factor Login Page

  3. After a successful password-login, the user is redirected to the two-factor authentication page where he has to enter a one-time-password (OTP)
  4. If the OTP was correct, the user is allowed to continue (and will not be shown the QR code the next time he logs in)

Search results for "Two Factor Authentication"

User Configuration

There are three main properties you can change to control the two factor authentication behaviour for a specific user node.

User.twoFactorConfirmed This is automatically set to true after a user authenticates via two-factor authentication. If this is set to false the user will always be shown the QR code for him to scan.
User.isTwoFactorUser Controls if the user wants to authenticate via two factor authentication. This only works if the setting TwoFactor.level is set to 1. If the setting is set to 2, the flag will automatically be set to true after a user logs in.
User.twoFactorSecret This is the secret which is used to generate tokens. It is automatically generated for every user.

Application Configuration

security.twofactorauthentication.whitelistedIPs

A comma-separated (,) list of IPs for which two factor authentication is disabled. IPv4 and IPv6 are supported.

Security Settings

The Security Settings tab contains all settings controlling the security properties of a Structr application. In particular they include properties such as two factor authentication, password policies and let’s encrypt configurations. The Security Settings are subdivided into the following subsections:

Two Factor Authentification

security.twofactorauthentification.level Sets whether 2FA is enabled, optional or disabled. Valid values are: forced, optional, off
security.twofactorauthentification.issuer Name of the 2FA issuer. Must be URL-compliant in order to generate valid QR codes.
security.twofactorauthentification.algorithm Respected by the most recent Google Authenticator implementations.
security.twofactorauthentification.digits Amount of digits in the 2FA code.
security.twofactorauthentification.period Defines the period that a TOTP code will be valid for, in seconds. Respected by the most recent Google Authenticator implementations.
security.twofactorauthentification.logintimeout Defines how long the two-factor login time window in seconds is. After entering the username and password the user has this amount of time to enter a two factor token before he has to re-authenticate via password.
security.twofactorauthentification.loginpage The application page where the user enters the current two factor token.
security.twofactorauthentification.whitelistedips A comma-separated (,) list of IPs for which two factor authentication is disabled. Both IPv4 and IPv6 are supported. CIDR notation is also supported. For example: 192.168.0.1/24 would whitelist 192.168.0.1 - 192.168.0.254

Example Implementation

<div id="qrimage-wrapper">
<img id="qrimage" style="margin-left: calc(50% - 100px);">
<div>To use two factor authentication, scan this QR code with an authenticator app on your smartphone.</div>

<div class="text-sm mt-6">
<div>
<b>Android: </b>
<a class="cursor-pointer hover:text-blue-400 text-blue-700" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US">Google Authenticator</a> on Google Playstore
</div>
<div>
<b>Apple iOS: </b>
<a class="cursor-pointer hover:text-blue-400 text-blue-700" href="https://apps.apple.com/us/app/google-authenticator/id388497605">Google Authenticator</a> on App Store
</div>
</div>
</div>

<form action="#" id="twoFactorForm">
<input id="twoFactorToken" type="hidden" value="${request.token}">
<div class="my-6">
<label class="block text-sm font-medium leading-5 text-gray-700">Two Factor Code</label>
<input id="twoFactorCode" class="appearance-none block w-full px-3 py-2 bg-blue-100 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5">
</div>
<button type="submit" id="login-button" class="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-indigo active:bg-blue-700 transition duration-150 ease-in-out">Login</button>
</form>

<script>
document.addEventListener('DOMContentLoaded', () => {

// get all required elements
const qrimageWrapper = document.getElementById('qrimage-wrapper');
const token = document.getElementById('twoFactorToken').value;
const codeInput = document.getElementById('twoFactorCode').value;
const loginButton = document.getElementById('login-button');

let qrdata = (new URLSearchParams(location.search)).get('qrdata');

if (!qrdata) {
// remove qr code placeholder if user is not shown qr code
qrimageWrapper.remove();
} else {
// transform url-safe qr code to regular base64 to display as image
qrimageWrapper.querySelector('#qrimage').src = 'data:image/png;base64, ' + qrdata.replaceAll('_', '/').replaceAll('-', '+');
}

document.getElementById('twoFactorForm').addEventListener('submit', async (event) => {

event.preventDefault();

loginButton.disabled = true;

const response = await fetch('/structr/rest/login', {
method: 'POST',
body: JSON.stringify({
twoFactorToken: token,
twoFactorCode: codeInput
})
});

if (response.ok) {

loginButton.textContent = 'Login successful';
window.location.href = '/';

} else {

let buttonText = 'Login failed - is device time correct?';

let reason = response.headers.get('reason');

if (reason === 'wrongTwoFactorCode') {
buttonText = 'Two Factor Code is not correct';
}

loginButton.disabled = false;
loginButton.textContent = buttonText;

window.setTimeout(function() {
loginButton.textContent = 'Login';
loginButton.disabled = false;
}, 2000);
}
});
});
</script>