cutelyst  4.5.1
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
Authentication

Plugins and classes to authenticate users. More...

Collaboration diagram for Authentication:

Classes

class  Cutelyst::Authentication
 Main class to manage user authentication. More...
 
class  Cutelyst::AuthenticationCredential
 Abstract class to validate authentication credentials like user name and password. More...
 
class  Cutelyst::AuthenticationRealm
 Combines user store and credential validation into a named realm. More...
 
class  Cutelyst::AuthenticationStore
 Abstract class to retrieve user data from a store. More...
 
class  Cutelyst::AuthenticationUser
 Container for user data retrieved from an AuthenticationStore. More...
 
class  Cutelyst::CredentialHttp
 Use HTTP basic authentication to authenticate a user. More...
 
class  Cutelyst::CredentialPassword
 Use password based authentication to authenticate a user. More...
 
class  Cutelyst::StoreHtpasswd
 Authentication data store using a flat file. More...
 
class  Cutelyst::StoreMinimal
 Minimal in memory authentication data store. More...
 

Detailed Description

The way to manage user authentication in Cutelyst is to combine a AuthenticationStore that provides the user data from for example a database together with a AuthenticationCredential validator that checks the password or other information to legitimate a user in an AuthenticationRealm that is added to the main plugin class Authentication in your application’s init() method.

Implementation example

You can add multpiple AuthenticationRealms to be used for different parts of your application. We will use one realm for the website and one for API access in this example. We will assume that you store your user data in a MySQL/MariaDB database with the following layout for your users table:

Row Type
id INTEGER PRIMARY KEY
username TEXT
password TEXT
email_address TEXT
first_name TEXT
last_name TEXT
active INTEGER

In order to persist the authenticated user over multiple requests, you also need the Session plugin. Include it together with the Authentication plugin in your CMakeLists.txt file. For this example we also need the Cutelyst::Sql plugin.

target_link_libraries(MyApp
...
Cutelyst::Utils::Sql
...
)
Main class to manage user authentication.
Plugin providing methods for session management.
Definition: session.h:161

Create an authentication store

In order to get the user data from the database we have to create our own subclass of AuthenticationStore and reimplement the findUser() method. We will call it AuthStoreSql in this example.

The header file authstoresql.h looks like the following:

#ifndef AUTHSTORESQL_H
#define AUTHSTORESQL_H
#include <Cutelyst/Plugins/Authentication/authenticationstore.h>
using namespace Cutelyst;
class AuthStoreSql : public AuthenticationStore
{
public:
AuthStoreSql();
~AuthStoreSql() override = default;
AuthenticationUser findUser(Context *c, const ParamsMultiMap &userinfo) override;
private:
QString m_idField;
};
#endif // AUTHSTORESQL_H
QMultiMap< QString, QString > ParamsMultiMap
The Cutelyst namespace holds all public Cutelyst API.

Our implementation in authstoresql.cpp:

#include "authstoresql.h"
#include <Cutelyst/Plugins/Utils/Sql/Sql>
AuthStoreSql::AuthStoreSql() : AuthenticationStore()
{
m_idField = "username";
}
AuthenticationUser AuthStoreSql::findUser(Context *c, const ParamsMultiMap &userinfo)
{
// get the user name from the userinfo that might be
// populated by HTML form input
const QString username = userinfo[m_idField];
QSqlQuery query = CPreparedSqlQueryThread(u"SELECT * FROM users WHERE username = :username"_qs);
query.bindValue(u":username"_qs, username);
if (query.exec() && query.next()) {
const QVariant userId = query.value("id");
qDebug() << "FOUND USER ->" << userId.toInt();
AuthenticationUser user(userId);
const int columns = query.record().count();
// collect column headers
for (int i = 0; i < columns; ++i) {
cols << query.record().fieldName(i);
}
// fill the user object
for (int i = 0; i < columns; ++i) {
user.inser(cols.at(i), query.value(i));
}
return user;
}
qDebug() << query.lastError().text();
return {};
}
#define CPreparedSqlQueryThread(str)
Definition: sql.h:245
QList::const_reference at(qsizetype i) const const
QString text() const const
void bindValue(const QString &placeholder, const QVariant &val, QSql::ParamType paramType)
bool exec()
QSqlError lastError() const const
bool next()
QSqlRecord record() const const
QVariant value(const QString &name) const const
int count() const const
QString fieldName(int index) const const
int toInt(bool *ok) const const

Configure authentication

We now have to glue all the stuff together in our application’s init method. Example myapp.cpp:

#include <Cutelyst/Plugins/Session/Session>
#include <Cutelyst/Plugins/Authentication/authentication.h>
#include <Cutelyst/Plugins/Authentication/credentialpassword.h>
#include <Cutelyst/Plugins/Authentication/credentialhttp.h>
#include "authstoresql.h"
bool MyApp::init()
{
...
// add the session plugin to persist the logged in user
// over multiple requests
new Session(this);
// add the Authentication plugin
auto auth = new Authentication(this);
// create our authentication store
auto authStore = std::make_shared<AuthStoreSql>();
// create password credential validator for website login
// using PBKDF2 hashed passwords
auto credWeb = std::make_shared<CredentialPassword>();
credWeb->setPasswordType(CredentialPassword::Hashed);
// add a realm for website login using our auth store
// and the credentials from above
auth->addRealm(authStore, credWeb, "website");
// create HTTP basic auth credential validator for API
// authentication usin PBKDF2 hashed passwords
auto credApi = std::make_shared<CredentialHttp>();
credApi->setPasswordType(CredentialHttp::Hashed);
// add a realm for API login using our auth store
// and the credentials from above
auth->addRealm(authStore, credApi, "api");
...
}

In the controllers

Let’s assume you have an admin area below /admin and API routes below /api. Your admin area login form is at /admin/login.

In your admin controller header admin.h:

#ifndef ADMIN_H
#define ADMIN_H
#include <Cutelyst/Controller>
using namespace Cutelyst;
class Admin : public Controller
{
Q_OBJECT
public:
explicit Admin(QObject *parent = nullptr);
~Admin() override = default;
// something like the main admin dashboard at /admin
C_ATTR(index, :Path)
void index(Context *c);
// will take login data at /admin/login provided by
// a HTML form
C_ATTR(login, :Local)
void login(Context *c);
// will log out a logged in user when requesting /admin/logout
C_ATTR(logout, :Local)
void logout(Context *c);
private:
// will be called for all routes below /admin and will
// contain our check for existence of a logged in user
C_ATTR(Auto, :Private)
bool Auto(Context *c);
};
#endif // ADMIN_H

Implementation in admin.cpp:

#include "admin.h"
#include <Cutelyst/Plugins/Authentication/authentication.h>
Admin::Admin(QObject *parent) : Controller(parent)
{
}
void Admin::index(Context *c)
{
// implement the admin dashboard or something else
}
// this will now be the place to get login data from a
// HTML form
void Admin::login(Context *c)
{
// get the username and the password from the form
const QString username = c->request()->bodyParam("username");
const QString password = c->request()->bodyParam("password");
// if the username and password values were found in the form
// try to authenticate the user
if (!username.isNull() && !password.isNull()) {
// attempt to log the user in in the website realm
if (Authentication::authenticate(c, { {"username", username}, {"password", password} }, "website")) {
// if successful, then let them use the admin area
c->response()->redirect(c->uriFor(c->controller("Admin")->actionFor("index")));
return;
} else {
// set an error message
c->setStash("error_msg", "Bad username or password.");
}
} else if (!Authentication::userInRealm(c, "website")) {
// set an error message
c->setStash("error_msg", "Empty username or password.");
}
// if either of above don't work out, send to the login page
c->setStash("template", "login.html");
}
void Admin::logout(Context *c)
{
// clear the user's state
Authentication::logout(c);
// send the user to the starting point
c->response()->redirect(c->uriFor("/"));
}
bool Admin::Auto(Context *c)
{
// Allow unauthenticated users to reach the login page
if (c->action() == c->controller("Admin")->actionFor("login")) {
return true;
}
// if a user doesn't exist, force login
if (!Authentication::userInRealm(c, "website")) {
// redirect the user to the login page
c->response()->redirect(c->uriFor("/admin/login"));
// return false to cancel 'post-auto' processing and prevent use of application
return false;
}
// user found, so return true to continue with processing after this 'auto'
return true;
}
bool isNull() const const

Our API root controller api.h:

#ifndef API_H
#define API_H
#include <Cutelyst/Controller>
using namespace Cutelyst;
class Api : public Controller
{
... other parts
private:
// will be called for all routes below /api and will
// contain our authentication check
C_ATTR(Auto, :Private)
bool Auto(Context *c);
}
#endif // API_H

Implementation in api.cpp:

#include "api.h"
#include <Cutelyst/Plugins/Authentication/authentication.h>
bool Api::Auto(Context *c)
{
// We directly return the authentication result to further
// process the request or not. If authentication fails, the
// used CredentialHttp will automatically set a 401 status
// code to the response
return Authentication::authenticate(c, {}, "api");
}
static bool authenticate(Context *c, const ParamsMultiMap &userinfo, const QString &realm=QLatin1String(defaultRealm))