For Wt 4.11.0

1. Prerequisites

In this tutorial, we use an example as a hands-on introduction to the Wt authentication module. This example is included in the Wt distribution, in examples/feature/auth1.

This introduction assumes that you have a reasonable understanding of Wt itself, in particular its stateful session model and the widget concept. If you haven’t done so yet, you may want to go through the Wt tutorial first.

2. Introduction

The authentication module implements the logic and widgets involved in getting users registered on your application, and letting them sign in. Note that this module is entirely optional, and simply implemented on top of Wt.

The module implements authentication. Its main purpose is to securely authenticate a user to sign in to your application. From your application, you will interact with the authentication module using a Wt::Auth::Login object, which you typically hold in your application object. It indicates the user currently signed in (if any), and propagates authentication events.

How you use this information for authorization or to customize the user experience is out of the scope of the module. Because of Wt’s built-in security features, with strong session hijacking mitigation, this is as straightforward as one can conceive it.

Currently, the module provides the following features, which can be separately enabled, configured or customized:

  • Password authentication, using best practices including salted hashing with strong cryptographic hash functions (such as bcrypt) and password strength checking.

  • Remember me functionality, again using best practices, by associating authentication tokens stored in cookies to a user.

  • Verified email addresses using the typical confirmation email process.

  • Lost password functionality that uses the verified email address to prompt a user to enter a new password.

  • Authentication using 3rd party Identity Providers, using OAuth 2.0 (including standard implementations for Facebook, Google, and generic OpenID Connect) and SAML.

  • Registration logic, which includes also the logic needed to merge new (federated login) identities into existing user profiles. For example, if a user previously registered using a username and password, they may later also authenticate using for example their Google Account and this new identity is added to his existing account.

  • Multi Factor logic, allowing for more security when logging in. An additional factor ensures that more than one type of evidence is required from the user when they log in. By chosing the types of credentials fittingly, there is a large chance to stop any account compromises.

The logic for these features is implemented separately from the user interface components, which can be customized or completely replaced with your own widgets.

Obviously, the authentication logic needs to talk to a storage system, and it is designed to hook into a storage system using an abstract interface. A default implementation that leverages Wt::Dbo, Wt’s ORM, is provided.

3. Module organization

The following picture illustrates the main classes of the module.

auth

It uses a classical separation between Model classes and View classes (which are the widgets).

There are three types of model classes:

  • Service classes are designed to be shared across all sessions (they do not have any state besides configuration). They contain logic which does not require transient state in a session.

  • Session-bound model classes are usually kept in a session for the entire lifetime of a session (but don’t need to be).

  • Transient model classes play an active role in the user interface, and are instantiated in the context of certain view components. They implement logic which involves state while the user is progressing through the login and registration process.

4. Example

We’ll walk through a small example which is a basic application that uses the authentication module (included in the Wt distribution in examples/feature/auth1). It is about 200 lines of C++ (which we’ll discuss below), and has the following features:

  • Password-based authentication and registration

  • OAuth 2 login and registration, for Google and Facebook accounts

  • Password attempt throttling

  • Email verification and a lost password procedure

  • Remember-me tokens

  • And by virtue of Wt itself, falls back to plain HTML behavior if the browser does not support Ajax, strong security, spam resilience, etc.

This example should help you to understand how to add authentication support to a new or existing Wt project.

4.1. Setting up a user database

We will be using the default implementation for an authentication database using Wt::Dbo, with the default persistence classes for authentication. This database implementation is found in Wt::Auth::Dbo::UserDatabase, and it uses Wt::Auth::Dbo::AuthInfo as the persistence class for authentication information, which itself references two other persistence classes:

  • A user’s "identities" are stored in a separate table. An identity uniquely identifies a user. Traditionally, a user would have only a single identity which is their login name (which could be their email address). But a user may accumulate more identities, corresponding to accounts with 3rd party identity providers. Or by setting up MFA on their account. The default implementation of TOTP will always generate an identity of the name "multifactor". By allowing multiple identities, the user may identify using a choice of methods.

  • Authentication tokens are stored in a separate table. An authentication token usually corresponds to a "remember-me" cookie, and a user may have multiple "remember-me" cookies when using different computers.

In addition, we define a User type to which we can add the application data for a particular user (this could be address information, birthdate, preferences, user role, etc…​), and which we want to link up with the authentication system.

The definition and persistence mapping for (a currently empty) User type is as given below:

User.h
#include <Wt/Dbo/Types.h>
#include <Wt/WGlobal.h>

class User;
using AuthInfo = Wt::Auth::Dbo::AuthInfo<User>;

class User {
public:
  template<class Action>
  void persist(Action& a)
  {
  }
};

DBO_EXTERN_TEMPLATES(User)

We declare a type alias for AuthInfo, which links the authentication information persistence class to our custom User information persistence class.

Next, we define a session class, which encapsulates the connection to the database to store authentication information, and which also tracks the user currently logged in, in a web session. We choose to use the Wt::Dbo::Session class as a base class (which could just as well be an embedded member).

Later on, we’ll see how each web session will instantiate its own persistence/authentication Session object.

Session.h
#include <Wt/Auth/Login.h>
#include <Wt/Auth/UserDatabase.h>

#include <Wt/Dbo/Session.h>
#include <Wt/Dbo/ptr.h>

#include "User.h"

namespace dbo = Wt::Dbo;

using UserDatabase = Wt::Auth::Dbo::UserDatabase<AuthInfo>;

class Session : public dbo::Session
{
public:
  explicit Session(const std::string& sqliteDb);

  Wt::Auth::AbstractUserDatabase& users();
  Wt::Auth::Login& login() { return login_; }

  ...

private:
  std::unique_ptr<UserDatabase> users_;
  Wt::Auth::Login login_;

  ...
};

Notice the type alias for UserDatabase, which states that we will be using the Wt::Auth::Dbo::UserDatabase implementation using AuthInfo, for which we declared a type alias earlier on. You are of course free to provide another implementation for Wt::Auth::AbstractUserDatabase which is not based on Wt::Dbo.

We also embed a Wt::Auth::Login member here, which is a small model class that holds the current login information. The login/logout widgets will manipulate this login object, while the rest of our application will listen to login changes from this object to adapt to the user currently logged in.

The Session constructor sets up the database session.

Session.C (constructor)
#include "Session.h"
#include "User.h"

#include <Wt/Auth/Dbo/AuthInfo.h>

#include <Wt/Dbo/backend/Sqlite3.h>

using namespace Wt;

Session::Session(const std::string& sqliteDb)
{
  auto connection = std::make_unique<Dbo::backend::Sqlite3>(sqliteDb);
  setConnection(std::move(connection_));

  mapClass<User>("user");
  mapClass<AuthInfo>("auth_info");
  mapClass<AuthInfo::AuthIdentityType>("auth_identity");
  mapClass<AuthInfo::AuthTokenType>("auth_token");

  try {
    createTables();
    std::cerr << "Created database.\n";
  } catch (Wt::Dbo::Exception& e) {
    std::cerr << e.what() << '\n';
    std::cerr << "Using existing database\n";
  }

  users_ = std::make_unique<UserDatabase>(*this);
}

The example uses an SQLite3 database, a cuddly database convenient for development, and we map four persistence classes to tables.

We then create the data schema if needed, which will automatically issue the following SQL:

create table "user" (
  "id" integer primary key autoincrement,
  "version" integer not null
);

create table "auth_info" (
  "id" integer primary key autoincrement,
  "version" integer not null,
  "user_id" bigint,
  "password_hash" varchar(100) not null,
  "password_method" varchar(20) not null,
  "password_salt" varchar(20) not null,
  "status" integer not null,
  "failed_login_attempts" integer not null,
  "last_login_attempt" text,
  "email" varchar(256) not null,
  "unverified_email" varchar(256) not null,
  "email_token" varchar(64) not null,
  "email_token_expires" text,
  "email_token_role" integer not null,
  constraint "fk_auth_info_user"
    foreign key ("user_id") references "user" ("id")
    on delete cascade deferrable initially deferred
);

create table "auth_token" (
  "id" integer primary key autoincrement,
  "version" integer not null,
  "auth_info_id" bigint,
  "value" varchar(64) not null,
  "expires" text,
  constraint "fk_auth_token_auth_info"
    foreign key ("auth_info_id") references "auth_info" ("id")
    on delete cascade deferrable initially deferred
);

create table "auth_identity" (
  "id" integer primary key autoincrement,
  "version" integer not null,
  "auth_info_id" bigint,
  "provider" varchar(64) not null,
  "identity" varchar(512) not null,
  constraint "fk_auth_identity_auth_info"
    foreign key ("auth_info_id") references "auth_info" ("id")
    on delete cascade deferrable initially deferred
);

Notice the auth_info, auth_token and auth_identity tables that define the storage for our authentication system.

4.2. Configuring authentication

The service classes (Wt::Auth::AuthService, Wt::Auth::PasswordService, and Wt::Auth::OAuthService), can be shared between sessions and contain the configuration and logic which does not require transient session state.

A good location to add these service classes are inside a specialized Wt::WServer instance, of which you usually also have only one in a Wt process. You could also create a singleton for them. To keep the example simple, we will declare them simply as global variables (but within file scope): myAuthService, myPasswordService, and myOAuthServices.

Session.C (authentication services)
#include <Wt/Auth/AuthService.h>
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordService.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/Auth/PasswordVerifier.h>
#include <Wt/Auth/GoogleService.h>
#include <Wt/Auth/FacebookService.h>

namespace {
  Wt::Auth::AuthService myAuthService;
  Wt::Auth::PasswordService myPasswordService(myAuthService);
  std::vector<std::unique_ptr<Wt::Auth::OAuthService>> myOAuthServices;
}

void Session::configureAuth()
{
  myAuthService.setAuthTokensEnabled(true, "logincookie");
  myAuthService.setEmailVerificationEnabled(true);
  myAuthService.setEmailVerificationRequired(true);

  auto verifier = std::make_unique<Wt::Auth::PasswordVerifier>();
  verifier->addHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(7));
  myPasswordService.setVerifier(std::move(verifier));
  myPasswordService.setPasswordThrottle(std::make_unique<Wt::Auth::AuthThrottle>());
  myPasswordService.setStrengthValidator(
    std::make_unique<Wt::Auth::PasswordStrengthValidator>());

  if (Wt::Auth::GoogleService::configured()) {
    myOAuthServices.push_back(std::make_unique<Wt::Auth::GoogleService>(myAuthService));
  }

  if (Wt::Auth::FacebookService::configured()) {
    myOAuthServices.push_back(std::make_unique<Wt::Auth::FacebookService>(myAuthService));
  }

  for (const auto& oAuthService : myOAuthServices) {
    oAuthService->generateRedirectEndpoint();
  }
}

Wt::Auth::AbstractUserDatabase& Session::users()
{
  return *users_;
}

const Wt::Auth::AuthService& Session::auth()
{
  return myAuthService;
}

const Wt::Auth::PasswordService& Session::passwordAuth()
{
  return myPasswordService;
}

std::vector<const Wt::Auth::OAuthService *> Session::oAuth()
{
  std::vector<const Auth::OAuthService *> result;
  result.reserve(myOAuthServices.size());
  for (const auto& auth : myOAuthServices) {
    result.push_back(auth.get());
  }
  return result;
}

The AuthService is configured to support "remember-me" functionality, and email verification.

The PasswordService needs a hash function to safely store passwords. You can actually define more than one hash function, which is useful only if you want to migrate to a new hash function while still supporting existing passwords. When a user logs in, and they are not using the "preferred" hash function, their password will be rehashed with the preferred one. In this example, we will use bcrypt, which is included as a hash function in Wt::Auth.

We also enable password attempt throttling: this mitigates brute force password guessing attempts.

Finally, we also use two (but later, perhaps more) OAuthService classes. You need one service per identity provider. In this case, we add Google and Facebook as identity providers.

4.3. The user interface

We create a specialized WApplication which contains our authentication session, and instantiates an AuthWidget. This widget shows a login or logout form (depending on the login status), and also hooks into default forms for registration, lost passwords, and handling of email-sent tokens in URLs).

User interface
#include <Wt/WApplication.h>
#include <Wt/WBootstrap2Theme.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WServer.h>

#include <Wt/Auth/AuthWidget.h>
#include <Wt/Auth/PasswordService.h>

#include "model/Session.h"

class AuthApplication : public Wt::WApplication {
public:
  explicit AuthApplication(const Wt::WEnvironment& env)
    : Wt::WApplication(env),
      session_(appRoot() + "auth.db")
  {
    session_.login().changed().connect(this, &AuthApplication::authEvent);

    root()->addStyleClass("container");
    setTheme(std::make_shared<Wt::WBootstrap2Theme>());

    useStyleSheet("css/style.css");

    auto authWidget = std::make_unique<Wt::Auth::AuthWidget>(
            Session::auth(), session_.users(), session_.login());

    authWidget->model()->addPasswordAuth(&Session::passwordAuth());
    authWidget->model()->addOAuth(Session::oAuth());
    authWidget->setRegistrationEnabled(true);

    authWidget->processEnvironment();

    root()->addWidget(std::move(authWidget));
  }

  void authEvent() {
    if (session_.login().loggedIn()) {
      const Wt::Auth::User& u = session_.login().user();
      log("notice")
        << "User " << u.id()
        << " (" << u.identity(Wt::Auth::Identity::LoginName) << ")"
        << " logged in.";
    } else
      log("notice") << "User logged out.";
  }

private:
  Session session_;
};

The last part is our main function where we setup the application server:

Application server setup
std::unique_ptr<Wt::WApplication> createApplication(const Wt::WEnvironment &env)
{
  return std::make_unique<AuthApplication>(env);
}

int main(int argc, char **argv)
{
  try {
    Wt::WServer server{argc, argv, WTHTTP_CONFIGURATION};

    server.addEntryPoint(Wt::EntryPointType::Application, createApplication);

    Session::configureAuth();

    server.run();
  } catch (Wt::WServer::Exception& e) {
    std::cerr << e.what() << '\n';
  } catch (Wt::Dbo::Exception &e) {
    std::cerr << "Dbo exception: " << e.what() << '\n';
  } catch (std::exception &e) {
    std::cerr << "exception: " << e.what() << '\n';
  }
}

5. Multi-Factor Authentication (MFA)

As of Wt 4.11.0 MFA is now also supported. Simply speaking this is an approach to authentication that takes more than a singular point of evidence the user that is performing the login event, is indeed who they claim to be. Thus offering an additional layer of security, reducing the chance an account will be compromised. There are various ways to implement MFA. With minimal configuration, the included TOTP approach can be used by developers. The Wt API does allow for the MFA process to be completely customized, so that your own preferred methods can be implemented.

5.1. Overview of TOTP

TOTP is a well-known and often used implementation when it comes to MFA. Its specification can be found here. It is a way to generate a unique secret for an application that is only valid for a limited time. This reduces the risk when a combination of a username/password is compromised. Any attacker will have a more limited time to gain access. The TOTP code will often have expired by that point.

The approach looks like this:

  • the server securely generates a random secret for an individual user and shares it with the user (typically through a QR code) to store in an authentication app.

  • from this secret key, we can by means of the TOTP algorithm create temporary codes.

  • upon an authentication event, this code will be provided by the user to the server. The server will use the same algorithm to compute the code from the shared secret, and verifies that the codes correspond. Each interval of 30 seconds will have a different code. Any moment within the same interval (e.g. 11:45:07 and 11:45:24) will have the same code.

5.2. Default Configuration

While the specification doesn’t enforce certain values, it does serve some recommendations, most of which were taken as defaults for Wt.

The chosen defaults are:

  • the time step is of length 30 seconds

  • the generated code is 6 digits long

  • the generated secret key is 32 characters long

To start using MFA in Wt, the configuration is minimal. This will enable TOTP by default. AuthService needs to be configured such that setMfaProvider() is set. This is the only requirement to activate the functionality. However, at this point is will not always be shown to users.

Then a developer has two options now:

  • either they enforce the usage of MFA globally, meaning each user is required to enter an MFA challenge (see: setMfaRequired()).

  • or they manage this on a per-user basis. They will either need to mark a user as being subject to MFA in some way, or allow the user a way to configure the feature for themselves (see: hasMfaStep()).

For simplicity we will consider the enabled and required case first. When configured in such a way, each user will encounter the TotpProcess after they logged in using username/password. This process' functionality is twofold:

  • setup: first the TOTP secret needs to be set up for the user, and an initial validation round is enforced. This ensures that the user was able to correctly use the generated secret key (shown as both a string, and a QR code for convenience). The user is required to put this secret key into an authenticator app, or browser extension that is able to generate code from the secret. They will enter the code the app or extension returns to them. At this point the framework knows the user now has their MFA step correctly configured, and it will save it to the database. This is done by means of an Identity. The identity’s name will be equal to what was configured on AuthService, and its value will be the secret key.[1]

  • validation: once set up the secret key and QR code are never again exposed to the user. Rather they will only be shown an input field where they need to put the code generated by their authenticator app or extension.

Both these possible view are constructed by the TotpProcess, but the view itself is managed by createMfaView().

The example can be found here: examples/authentication/mfa/totp.

5.3. Note on throttling

Since the MFA process is implemented very open-ended, and no set UI has been provided for it, a custom implementation of it will require developers to explicitly take throttling into account if they wish to use it.

The most important aspects of implementing this successfully are the methods:

  • setAuthenticated() which is to be called after each authentication event. It will ensure that the login failure or success is recorded in the database. This state can then be used by the actual delay calculation.

  • delayForNextAttempt() calculates the number of seconds the throttling needs to apply.

  • getAuthenticationThrottle() specifies some predefined rules on the throttling delay given a certain number of consecutive failed login attempts.

5.4. Custom implementation (similar to default)

THIS IS NOT A RECOMMENDED MFA STRATEGY, it is purely illustrative to demonstrate the API flexibility

We’ll now follow an example that deviates a little from the default. It is very similar to the TOTP implementation, and requires next to no set-up. It will generate a random PIN for each user, which they will have to remember (or write down!).

A proper approach to MFA requires that the user should use factors from different "sources". A password is an example of something they know. TOTP is an example of something they have. If the same source is used twice, this is not considered good practice. After all a user may write down a password. If using this approach, it is very likely they will also write down the PIN.

The example can be found here: examples/authentication/mfa/pin.

We’ll go over a couple important parts and set up an example that will:

  • require a password-based login

  • allow the MFA login to be remembered

We’ll go ahead and adapt the example Session from Configuring authentication. In the configureAuth method, we will simply add two more lines. Note that all other configuration remained the same, meaning auth tokens are enabled, and password authentication was configured.

So here we set the AuthService to be able to use MFA, and use the default name for it (resulting in "multifactor". We also then require MFA to be used by all users.

Session configuration
  myAuthService.setMfaProvider(Wt::Auth::Identity::MultiFactor);
  myAuthService.setMfaRequired(true);

The PinProcess will be our MFA process, and will inherit from AbstractMfaProcess. This base class dictates how an MFA process should behave, and what interface it ought to follow. This widget also shows a different way to define the UI from TotpProcess. The way this process is shown in the UI is managed by createMfaView().

We use a WCompositeWidget here that uses a WTemplate as its implementation. It’s a very minimal template that just binds a single widget. This is the widget generated by the create... virtual methods.

PinProcess implementation
class PinProcess final : public Wt::Auth::Mfa::AbstractMfaProcess
{
  public:
    PinProcess(const Wt::Auth::AuthService& authService, Wt::Auth::AbstractUserDatabase& users, Wt::Auth::Login& login)
      : Wt::Auth::Mfa::AbstractMfaProcess(authService, users, login)
    {
    }

We’ll then have to implement a couple methods that are purely virtual. These methods essentially dictate what the content is the two possible views of the MFA process hold.

The next example Custom implementation (far from default) will show a more alternative way as a demonstration.

Abstract methods
public:
  std::unique_ptr<Wt::WWidget> createSetupView() final;
  std::unique_ptr<Wt::WWidget> createInputView() final;

These methods dictate how the widget behaves in the regular AuthWidget flow. The create... functions generate the views that are to be displayed.

For convenience, and to ensure we do not repeat code too much, we have introduced the createBaseView() method. This generates the pointer to the template, allows that to use the id(), and tr() functions. The condition setCondition() is the main distinguishing factor between the two views. Only during the setup phase will the generated code be displayed to the user. After that they will have to supply the PIN code themselves.

Abstract methods implementation
std::unique_ptr<Wt::WTemplate> PinProcess::createBaseView(bool isSetup)
{
  auto view = std::make_unique<Wt::WTemplate>(Wt::WTemplate::tr("pin-template"));

  view->addFunction("id", &Wt::WTemplate::Functions::id);
  view->addFunction("tr", &Wt::WTemplate::Functions::tr);

  view->setCondition("if:is-setup", isSetup);

  view_ = view.get();
  return view;
}

std::unique_ptr<Wt::WWidget> PinProcess::createSetupView()
{
  auto view = createBaseView(true);
  createCodeGenerator();
  createCodeInput();
  createLoginButton();
  createRememberMe();
  return std::move(view);
}

std::unique_ptr<Wt::WWidget> PinProcess::createInputView()
{
  auto view = createBaseView(false);
  createCodeInput();
  createLoginButton();
  createRememberMe();
  return std::move(view);
}

You may have noticed the WTemplate::tr() call here. This will load content from a resource bundle, which can produce localised strings. For this to work, the application needs to be able to use these messages.

Application changes
  messageResourceBundle().use(appRoot() + "template");

That takes care of nearly all the required setup. We now only have to create the actual PinProcess further. Here we will create the actual widgets, and manage how the class receives, validates and responds to input.

PinProcess final steps (header)
protected:
  void createCodeGenerator();
  void createCodeInput();
  void createLoginButton();
  void createRememberMe();

  void checkCodeInput();
  void update();

private:
  const int NUMBER_OF_DIGITS = 5;

  Wt::WTemplate* view_ = nullptr;

  std::string currentCode_;

  Wt::WLineEdit* codeInput_;
  Wt::WCheckBox* rememberMeField_;

  Wt::Signal<Wt::Auth::Mfa::AuthenticationResult> authenticated_;

  Wt::WTemplate* impl() { return static_cast<Wt::WTemplate*>(implementation()); }
};

Some explanation for all methods:

  • createCodeGenerator will generate a random PIN code, from a random value, seeded by the current time. A number is created of length NUMBER_OF_DIGITS.

  • createCodeInput will add a WLineEdit to the template, where the user is able to insert the PIN code. Pressing enter will start the process to verify the input.

  • createLoginButton will add a WPushButton that also starts the check to ascertain the PIN is valid.

  • createRememberMe will add two things:

    • a WCheckBox that indicates whether "remember-me" is enabled or not, which can be set by the user to enable it. By default it will be disabled. This DOES require setAuthTokensEnabled() to be set to true.

    • it displays a label next to the WCheckBox that indicates how long the "remember-me" will last.

  • checkCodeInput will validate the input the user supplied in the WLineEdit. Either this code is already present in the class, or it is retrieved from the user’s identity using userIdentity(). The persisted or just created value is then checked against the user’s input. This input can either:

    • not match, in which case the failure is indicated to them visually by update.

    • match, in which case they are logged into the system, and the identity is added to the database, if it hasn’t been already. In both cases the authenticated signal is fired. Here we will also have to implement some custom logic if we wish to enable throttling. Developers can check how this is done on TotpProcess. Since the way the MFA process is implemented allows for any approach, this will require a developer to explicitly manage throttling.

  • update will indicate the failure to authenticate to the user by displaying a message and indicating the field red.

PinProcess final steps (implementation)
void PinProcess::createCodeGenerator()
{
  // Set seed for randomness to current time
  std::srand(std::time(0));

  std::string code;
  int value = 0;
  for (int i = 0; i < NUMBER_OF_DIGITS; ++i) {
    value = std::rand() / (RAND_MAX / 10);
    code += std::to_string(value);
  }

  currentCode_ = code;
  view_->bindString("code", code);
}

void PinProcess::createCodeInput()
{
  codeInput_ = view_->bindNew<Wt::WLineEdit>("input");
  codeInput_->enterPressed().connect([this] {
    checkCodeInput();
  });

  // Validation message
  view_->bindEmpty("code-info");
}

void PinProcess::createLoginButton()
{
  auto login = view_->bindNew<Wt::WPushButton>("login", tr("Wt.Auth.login"));
  login->clicked().connect([this] {
    checkCodeInput();
  });
}

void PinProcess::createRememberMe()
{
  view_->setCondition("if:remember-me", true);
  rememberMeField_ = view_->bindNew<Wt::WCheckBox>("remember-me");

  int days = baseAuth().mfaTokenValidity() / 24 / 60;

  Wt::WString info;
  if (days % 7 != 0) {
    info = Wt::WString::trn("Wt.Auth.remember-me-info.days", days).arg(days);
  } else if (days == 0) {
    info = Wt::WString::tr("Wt.Auth.remember-me-info.indefinite");
  } else {
    info = Wt::WString::trn("Wt.Auth.remember-me-info.weeks", days/7).arg(days/7);
  }

  view_->bindString("remember-me-info", info);
}

void PinProcess::checkCodeInput()
{
  const std::string enteredCode = codeInput_->text().toUTF8();
  auto savedCode = currentCode_.empty() ? userIdentity() : currentCode_;

  if (enteredCode != savedCode) {
    update();
    authenticated_.emit(Wt::Auth::Mfa::AuthenticationResult(Wt::Auth::Mfa::AuthenticationStatus::Failure, "The validation failed"));
  } else {
    createUserIdentity(savedCode);

    if (rememberMeField_->isChecked()) {
      setRememberMeCookie(login().user());
    }

    login().login(login().user());
    authenticated_.emit(Wt::Auth::Mfa::AuthenticationResult(Wt::Auth::Mfa::AuthenticationStatus::Success));
  }
}

void PinProcess::update()
{
  codeInput_->addStyleClass("is-invalid Wt-invalid");

  // Validation message and color
  view_->bindString("code-info", "Wrong PIN code");
  view_->bindString("label", "invalid-feedback");
}

To ensure that the correct widget is shown (and not the default TotpProcess), one needs to override createMfaProcess(). Here we will also take care of the additional authenticated signal. This will make sure that the application can listen to it.

AuthWidget overrides
std::unique_ptr<Wt::Auth::Mfa::AbstractMfaProcess> AuthWidget::createMfaProcess()
{
  auto process = std::make_unique<PinProcess>(*model()->baseAuth(), model()->users(), login());
  process->authenticated().connect([this](Wt::Auth::Mfa::AuthenticationResult res) {
    authenticated_.emit(res);
  });
  return std::move(process);
}

A user can now register, or using the default setup use the "admin:admin" or "user:user" accounts. They’ll notice that after they log in, or follow the link to register, they will be shown the PinProcess. Only after they enter the same PIN that is displayed to them, will they be authenticated.

One concern that is indicated in the documentation in a note on AbstractMfaProcess, is that the Login::changed() signal is fired BOTH when the user passes the normal username/password authentication step, AND when they pass the MFA step. This is why we make use of a custom signal authenticated.

5.5. Custom implementation (far from default)

THIS IS NOT A RECOMMENDED MFA STRATEGY, it is purely illustrative to demonstrate the API flexibility

The previous example offers a very basic case, with minimal configuration, but also with minimal advantage. We will now discuss another way to approach MFA, with a more custom widget. This takes some inspiration from the examples/qrlogin. While it is far from a good approach to MFA again, it will illustrate some more nuance, than the previous example, and guide any developer towards the way to implement fully custom MFA processes.

5.5.1. Overview of the application

So, what does this application do? You can find the complete source at examples/authentication/mfa/phone.

This requires your phone to be able to access your application.

Simply speaking, it will always display a QR code. This code can be scanned by your smartphone, which will direct it to an endpoint, managed by this application. Upon initially using this, some information of your phone will be stored in the database. This will not be stored as the identity, but in a more complex table. That is the end of the setup phase.

After each subsequent login, the user will then be prompted to scan a similar QR code, which that checks if the already saved data from the user’s phone corresponds with the current access. If it does, the MFA step will be successful.

This is again purely illustrative. The metrics taken from the phone are not a good way to uniquely identify a user. The phone can be changed out, a software update or a different browser may produce different results. So it is not a robust MFA step. It is only a way to illustrate what is necessary to ensure a proper MFA implementation that integrates well with Wt.

Important note: This will not work correctly if you use session tracking "Combined". If the QR code is then scanned on your phone, Wt will complain that no multi-session cookie is present in the browser. To get around this use the session tracking configuration of "URL". This is set in wt_config.xml. This exposes the session of the login to the QR code, and thus to any device that scans the QR code. After the MFA step this session ID is replaced, but this is still not a good idea. This example is purely illustrative and should NOT BE IMPLEMENTED in ANY production environment.

5.5.2. First configuration

We will again start from a very basic Session. This one will be identical to the one from above, except that it will map an extra class.

Like with the above example, a default "admin:admin", and "user:user" account are provided for convenience. The user account does not require the MFA step, whereas the admin account does. This is to avoid having MFA be forced on each user. We no longer set setMfaRequired() to be true. Now we define this for each user separately, using the MyUser::requires_mfa_ member.

mysession.cpp (extra mapping)
  mapClass<AuthEntry>("auth_entry");

This class will contain information about the phone that initially was used to set up MFA. And thus if subsequent requests are made through the "same phone". As the metrics gathered are very basic, the "same phone" is between heavy quotation marks. This can obviously be extended, but as said before. This is purely illustrative.

AuthEntry class
#pragma once

#include "myuser.h"

#include "Wt/Dbo/Dbo.h"

class AuthEntry : public Wt::Dbo::Dbo<AuthEntry>
{
public:
  AuthEntry() = default;
  AuthEntry(const Wt::Dbo::ptr<AuthInfo>& authInfo, const std::string& host, const std::string& userAgent, const std::string& language)
    : authInfo_(authInfo),
      host_(host),
      userAgent_(userAgent),
      language_(language)
  {
  }

  const std::string& host() const { return host_; }
  const std::string& userAgent() const { return userAgent_; }
  const std::string& language() const { return language_; }

  template<class Action>
  void persist(Action& a)
  {
    Wt::Dbo::belongsTo(a, authInfo_, "auth_info");

    Wt::Dbo::field(a, host_, "host");
    Wt::Dbo::field(a, userAgent_, "userAgent");
    Wt::Dbo::field(a, language_, "language");
  }

private:
  Wt::Dbo::ptr<AuthInfo> authInfo_;

  std::string host_;
  std::string userAgent_;
  std::string language_;
};

We then come to the widget that actually manages the login, which then inherits from AbstractMfaProcess. This we will call PhoneProcess for now.

It follows the same structure as the PinProcess, or TotpProcess, but differs how it show its information. This time we will use a WDialog to contain the widget and show it in the UI.

One other major difference is that we set up a WResource that is going to manage the interaction with our phone. We will call this class QrCodeHandler.

The header of the PhoneProcess looks like:

PhoneProcess.h
#pragma once

#include "mysession.h"
#include "qrcodehandler.h"

#include "Wt/Auth/Mfa/AbstractMfaProcess.h"

#include "Wt/WCheckBox.h"
#include "Wt/WDialog.h"
#include "Wt/WLineEdit.h"
#include "Wt/WTemplate.h"

class PhoneProcess final : public Wt::Auth::Mfa::AbstractMfaProcess
{
public:
  PhoneProcess(MySession& session);

  std::unique_ptr<Wt::WWidget> createSetupView() final;
  std::unique_ptr<Wt::WWidget> createInputView() final;

  void processEnvironment() final;

  void setUpUserIdentity();

  Wt::Signal<Wt::Auth::Mfa::AuthenticationResult>& authenticated() { return authenticated_; }

private:
  MySession& session_;

  std::unique_ptr<QrCodeHandler> qrHandler_;

  Wt::WTemplate* view_ = nullptr;

  bool doRememberMe_ = false;

  Wt::Signal<Wt::Auth::Mfa::AuthenticationResult> authenticated_;

  void createQRCode(bool isSetup);
  void createRememberMe();

  void createQRHandlerResource();
  std::unique_ptr<Wt::WTemplate> createBaseView(bool isSetup);
};

Its implementation will look like:

PhoneProcess.cpp
#include "phonewidget.h"

#include "qrcodepainter.h"

#include "Wt/Auth/AuthService.h"
#include "Wt/Auth/Login.h"

#include "Wt/WApplication.h"
#include "Wt/WPushButton.h"
#include "Wt/WWidget.h"

PhoneProcess::PhoneProcess(MySession& session)
  : AbstractMfaProcess(session.auth(), session.users(), session.login()),
    session_(session)
{
  createQRHandlerResource();
}

void PhoneProcess::processEnvironment()
{
  Wt::Auth::User user = processMfaToken();

  if (user.isValid()) {
    login().login(user, Wt::Auth::LoginState::Weak);
    authenticated_.emit(Wt::Auth::Mfa::AuthenticationResult(Wt::Auth::Mfa::AuthenticationStatus::Success));
    return;
  }
}

void PhoneProcess::setUpUserIdentity()
{
  createUserIdentity("verified");
}

void PhoneProcess::createQRHandlerResource()
{
  qrHandler_ = std::make_unique<QrCodeHandler>(session_);
  qrHandler_->allowSignin().connect([this] {
    {
      auto app = Wt::WApplication::instance();
      Wt::WApplication::UpdateLock lock(app);
      login().login(login().user());
      app->triggerUpdate();
    }

    authenticated_.emit(Wt::Auth::Mfa::AuthenticationResult(Wt::Auth::Mfa::AuthenticationStatus::Success));
    if (doRememberMe_) {
      setRememberMeCookie(login().user());
    }
  });
}

std::unique_ptr<Wt::WTemplate> PhoneProcess::createBaseView(bool isSetup)
{
  auto view = std::make_unique<Wt::WTemplate>(Wt::WTemplate::tr("phone-template"));

  view->addFunction("id", &Wt::WTemplate::Functions::id);
  view->addFunction("tr", &Wt::WTemplate::Functions::tr);

  view_ = view.get();
  return view;
}

std::unique_ptr<Wt::WWidget> PhoneProcess::createSetupView()
{
  auto view = createBaseView(true);

  createQRCode(true);
  createRememberMe();
  return std::move(view);
}

std::unique_ptr<Wt::WWidget> PhoneProcess::createInputView()
{
  auto view = createBaseView(false);

  createQRCode(false);
  createRememberMe();
  return std::move(view);
}

void PhoneProcess::createQRCode(bool isSetup)
{
  std::string handleUrl = Wt::WApplication::instance()->makeAbsoluteUrl(qrHandler_->url());
  if (isSetup) {
    handleUrl += "&issetup";
  }
  view_->bindNew<QrCodePainter>("code", handleUrl);
}

void PhoneProcess::createRememberMe()
{
  view_->setCondition("if:remember-me", true);
  auto rememberMe  = view_->bindNew<Wt::WCheckBox>("remember-me");
  rememberMe->changed().connect([this] { doRememberMe_ = true; });

  int days = baseAuth().mfaTokenValidity() / 24 / 60;

  Wt::WString info;
  if (days % 7 != 0) {
    info = Wt::WString::trn("Wt.Auth.remember-me-info.days", days).arg(days);
  } else if (days == 0) {
    info = Wt::WString::tr("Wt.Auth.remember-me-info.indefinite");
  } else {
    info = Wt::WString::trn("Wt.Auth.remember-me-info.weeks", days/7).arg(days/7);
  }

  view_->bindString("remember-me-info", info);
}

The way we generate the URL is not that important here, and will be omitted. What is important is the way that we choose to handle the incoming requests that come from any phone that scans the QR code. This will be handled by QrCodeHandler.

QrCodeHandler.h
#pragma once

#include "mysession.h"

#include "Wt/Http/Request.h"
#include "Wt/Http/Response.h"

#include "Wt/WResource.h"

class PhoneProcess;

class QrCodeHandler final : public Wt::WResource
{
public:
  QrCodeHandler(MySession& session, PhoneProcess* phoneWidget);

  void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) final;

  Wt::Signal<>& allowSignin() { return allowSignin_; }

private:
  MySession& session_;
  PhoneProcess* phoneWidget_ = nullptr;

  Wt::Signal<> allowSignin_;
};

This is simply a WResource that listens to an incoming request, tied to the session. A response will be generated that essentially contains either 200/400 status and a small string that indicates the result of the request. The handleRequest() method will check some headers of the incoming request, and check them against the database.

A request can have issetup appended to it, indicating the record is to be created still, and it’s the user’s first time using MFA. Otherwise the current request is compared to the existing record.

When creating a new record, or when a match has taken place, the response is a HTTP 200. Otherwise it will be 400.

QrCodeHandler.cpp
#include "qrcodehandler.h"

#include "authentry.h"

QrCodeHandler::QrCodeHandler(MySession& session, PhoneProcess* phoneWidget)
  : session_(session),
    phoneWidget_(phoneWidget)
{
}

void QrCodeHandler::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
{
  std::string userAgent = request.headerValue("User-Agent");

  if (userAgent.find("Mobile") == std::string::npos) {
    response.setStatus(400);
    return;
  }

  const Wt::Auth::User& user = session_.login().user();

  if (!user.isValid()) {
    response.setStatus(400);
    return;
  }

  std::string query = request.queryString();
  bool isSetup = query.find("issetup") != std::string::npos;

  std::string host = request.headerValue("Host");
  std::string language = request.headerValue("Accept-Language");

  Wt::Dbo::Transaction t(session_);
  const Wt::Dbo::ptr<AuthInfo>& authInfo = session_.find<AuthInfo>("where id = ?").bind(user.id());
  Wt::Dbo::ptr<AuthEntry> entry = session_.find<AuthEntry>("where auth_info_id = ?").bind(authInfo);

  if (!entry && !isSetup) {
    response.setStatus(400);
    return;
  } else if (!entry && isSetup) {
    session_.addNew<AuthEntry>(authInfo, host, userAgent, language);
    phoneWidget_->setUpUserIdentity();
    response.setStatus(200);
    response.out() << "An entry was created.";
    allowSignin_.emit();
    return;
  } else if (entry && isSetup) {
    response.setStatus(400);
    response.out() << "An entry already exists. Cannot set up a new entry.";
    return;
  }

  if (entry->host() == host && entry->userAgent() == userAgent && entry->language() == language) {
    response.setStatus(200);
    response.out() << "Validation has been accepted.";
    allowSignin_.emit();
  } else {
    response.setStatus(400);
    response.out() << "Validation has been denied.";
  }
}

At the end we emit a signal allowSignin_, if a valid request has taken place. This will indicate to the application that a successful MFA step was completed, and that the user can be properly logged in.

To ensure that we use the right widget, we again need to do some overriding of methods of AuthWidget. This is similar to the last example, where we specify which widget needs to be created in createMfaWidget(). But we do need one more entry. Since we no longer rely on the basic implementation, which would create an Identity, the application cannot rely on the default link:../reference/html/classWt_1_1Auth_1_1AuthWidget.html#a31f1e6343c9e298e071c9f525be29981[createMfaView()`]. We *can* opt to set the `+Identity as well, but this shows how one can get around that if desired. We then need logic to decide whether the setup or input state of the MFA process needs to be displayed.

The call to processMfaToken() makes sure that the remember-me functionality can be used.

We also perform somewhat of a hack here, by calling createUserIdentity(). Done throught the wrapper of setUpUserIdentity, so it can be performed in the QrCodeHandler. This identity is meant to keep track of whether the user can be identified by a certain MFA provider. The value that is attached to it is normally more descriptive, or contains some MFA specific stuff.

In the default TOTP approach for example, it will contain the secret key. This MFA approach however does not feature such a centralised key, but leans in the data in AuthEntry.

We can provide a custom implementation of processMfaToken() to take this into account, but this seems a little out of scope. Hence the choice to generate a "placeholder" identity.

Normally this identity should, for convenience’s sake, carry the same name as setMfaProvider(). However, this case also shows that the environment token (cookie) can be matched regardless of the identity name. This can be added as an extra requirement. But if a developer opts for this, do take into account that this may invalidate existing cookies for users that already clicked the "remember-me" checkbox.

MyAuthWidget.h overrides
#pragma once

#include "mysession.h"

#include "Wt/Auth/AuthWidget.h"

class MyAuthWidget final : public Wt::Auth::AuthWidget
{
public:
  MyAuthWidget(MySession& session);

  std::unique_ptr<Wt::Auth::Mfa::AbstractMfaProcess> createMfaProcess() final;

  void createMfaView() final;

private:
  MySession& session_;
};

With the following implementation:

MyAuthWidget.cpp
#include "myauthwidget.h"

#include "authentry.h"
#include "phoneprocess.h"

#include "Wt/Dbo/Transaction.h"

MyAuthWidget::MyAuthWidget(MySession& session)
  : Wt::Auth::AuthWidget(session.auth(), session.users(), session.login()),
    session_(session)
{
}

std::unique_ptr<Wt::Auth::Mfa::AbstractMfaProcess> MyAuthWidget::createMfaProcess()
{
  return std::make_unique<PhoneProcess>(session_);
}

void MyAuthWidget::createMfaView()
{
  phoneProcess_ = createMfaProcess();
  PhoneProcess* process = dynamic_cast<PhoneProcess*>(phoneProcess_.get());

  const Wt::Auth::User user = session_.login().user();

  Wt::Dbo::Transaction t(session_);
  Wt::Dbo::ptr<AuthEntry> hasEntry = session_.find<AuthEntry>("where auth_info_id = ?").bind(user.id());

  auto dialog = addChild(std::make_unique<Wt::WDialog>("MFA Phone Verification"));

  if (!hasEntry) {
    dialog->contents()->addWidget(std::move(process->createSetupView()));
    dialog->finished().connect([dialog] {
      dialog->parent()->removeChild(dialog);
    });
    dialog->show();
  } else {
    process->processEnvironment();
    if (login().state() != Wt::Auth::LoginState::RequiresMfa) {
      return;
    }

    dialog->contents()->addWidget(std::move(process->createInputView()));
    dialog->finished().connect([dialog] {
      dialog->parent()->removeChild(dialog);
    });
    dialog->show();
  }

  process->authenticated().connect([this, dialog](Wt::Auth::Mfa::AuthenticationResult result) {
    if (result.status() == Wt::Auth::Mfa::AuthenticationStatus::Success) {
      createLoggedInView();
      Wt::WApplication::instance()->triggerUpdate();
      dialog->accept();
    }
  });
}

We also made another important change, namely to hasMfaStep(), which is used to indicate whether a user should be displayed the MFA step or not. This can be used to enforce the MFA step to show, even if mfaRequired() is set to false (but it should be enabled mfaEnabled()).

MyAuthModel.h
#pragma once

#include "mysession.h"

#include "Wt/Auth/AuthModel.h"

class MyAuthModel : public Wt::Auth::AuthModel
{
public:
  MyAuthModel(MySession& session);

protected:
  bool hasMfaStep(const Wt::Auth::User& user) const final;

private:
  MySession& session_;
};

This will now check whether the user has the requires_mfa field set in the database, attached to them. This does also require the MyUser class to change slightly, to allow for this value to be set.

It has the following implementation:

MyAuthModel.cpp
#include "myauthmodel.h"

#include "Wt/Dbo/Transaction.h"
#include "authentry.h"

MyAuthModel::MyAuthModel(MySession& session)
  : Wt::Auth::AuthModel(session.auth(), session.users()),
    session_(session)
{
}

bool MyAuthModel::hasMfaStep(const Wt::Auth::User& user) const
{
  Wt::Dbo::Transaction t(session_);
  Wt::Dbo::Query<bool> result = session_.query<bool>("select requires_mfa from user join auth_info on auth_info.user_id = user.id where auth_info.id = ?")
                                .bind(user.id());
  return result.resultValue();
}

And thus also the following changes to the MyUser record.

MyUser changes
public:
  ...

  void setRequiresMfa(bool value)
  {
    requiresMfa_ = value;
  }

  template<class Action>
  void persist(Action& a)
  {
    ...
    Wt::Dbo::field(a, requiresMfa_, "requires_mfa");
    ...
  }

private:
  ...
  bool requiresMfa_ = false;
  ...

Do also note that there are a couple places where we call UpdateLock together with triggerUpdate(). This is necessary because we are no longer working in the main event loop. With a normal authentication widget, we will listen to the user to update the server. They will press "Login", or enter in the last field. This will prompt the server to enter its main loop, (see notify()) and respond to the request after the client submits it.

However, in this case we the server is listening not to actual user input, but to a response coming into the resource. As such, we need to update the UI based on what happens there. This may happen at any point, and from any thread of the application. Updates to the DOM tree should be done in a single thread (the main UI thread). This can be done by taking the application update lock.

To facilitate this we do several things:

  1. we attach a handler to the WCheckBox that is used to remember the MFA step. Which will prompt an update on the server side. There we keep track of the state in doRememberMe_.

  2. once we then catch an authentication event. We check the state of the boolean and act accordingly (setting a cookie if necessary, using setRememberMeCookie()). We also call triggerUpdate() before that, to ensure that we push out the change in sessionId(). If we would not, Wt would lose track of the session, and try to contact the session that was just renamed by its old ID.

  3. last we also ensure that showInputView() doesn’t pop up when it shouldn’t. We guard this by checking the login state() which is set either to LoginState::Weak in processEnvironment() if a token match is found, or LoginState::RequiresMfa otherwise.


These examples show you how implement MFA in a very basic, slightly more custom, and a very custom manner. Using the tools at your disposal, you are now able to implement your own method if you so desire.


1. This information is stored in plaintext in the database.