Before you start 
Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server .
 
Step by step guide 
This tutorial shows how to create a custom digest authorization checker. In the tutorial the authorization data is stored in PostgreSQL database, and information of an authorized user (i.e. Authorization header) is passed to the HTTP handler.
Authentication credentials checking logic is set in base class server::handlers::auth::digest::AuthChecker. This sample simply defines derived class samples::digest_auth::AuthChecker, that can operate with user data and unnamed nonce pool. Digest authentication logic and hashing logic is out of scope of this tutorial. For reference, read RFC2617 .
 
PostgreSQL Table 
Let's make a table to store users data:
DROP  SCHEMA IF  EXISTS  auth_schema CASCADE;
 
CREATE  SCHEMA IF  NOT  EXISTS  auth_schema;
 
CREATE  EXTENSION IF  NOT  EXISTS  "uuid-ossp" ;
 
CREATE  TABLE  IF  NOT  EXISTS  auth_schema.users (
    username TEXT NOT  NULL ,
    nonce TEXT NOT  NULL ,
    timestamp  TIMESTAMPTZ NOT  NULL  DEFAULT  NOW(),
    nonce_count integer  NOT  NULL  DEFAULT  0 ,
    ha1 TEXT NOT  NULL ,
    PRIMARY  KEY(username)
);
 
CREATE  TABLE  IF  NOT  EXISTS  auth_schema.unnamed_nonce (
    id uuid NOT  NULL ,
    nonce TEXT NOT  NULL ,
    creation_time TIMESTAMPTZ NOT  NULL ,
    PRIMARY  KEY(id),
    UNIQUE (nonce)
);
 
Authorization Checker 
To implement an authorization checker derive from server::handlers::auth::digest::AuthChecker and override the virtual functions:
#include "auth_digest.hpp" 
#include "user_info.hpp" 
 
#include "sql/queries.hpp" 
 
#include <algorithm> 
#include <optional> 
#include <string_view> 
 
 
namespace  samples::digest_auth {
 
using  TimePoint = std::chrono::time_point<std::chrono::system_clock>;
 
class  AuthChecker final
 public :
  using  AuthDigestSettings =
 
  AuthChecker(const  AuthDigestSettings& digest_settings, std::string realm,
              const ::components::ComponentContext& context,
              const  SecdistConfig& secdist_config)
      : server::handlers::auth::digest::AuthCheckerBase(
            digest_settings, 
std ::move(realm), secdist_config),
 
        pg_cluster_(context.FindComponent<
components ::Postgres>(
"auth-database" )
 
                        .GetCluster()),
        nonce_ttl_(digest_settings.nonce_ttl) {}
 
  std::optional<UserData> FetchUserData(
      const  std::string& username) const override ;
 
  void  SetUserData(const  std::string& username, const  std::string& nonce,
                   std::int64_t nonce_count,
                   TimePoint nonce_creation_time) const override ;
 
  void  PushUnnamedNonce(std::string nonce) const override ;
 
  std::optional<TimePoint> GetUnnamedNonceCreationTime(
      const  std::string& nonce) const override ;
 
 private :
  const  std::chrono::milliseconds nonce_ttl_;
};
The authorization in base class calls the following functions:
Returns user data from database: std::optional<UserData> AuthChecker::FetchUserData(
    const  std::string& username) const  {
                           uservice_dynconf::sql::kSelectUser, username);
 
  if  (res.IsEmpty()) return  std::nullopt;
 
  auto  user_db_info = res.
AsSingleRow <UserDbInfo>(storages::postgres::kRowTag);
 
  return  UserData{HA1{user_db_info.ha1}, user_db_info.nonce,
                  user_db_info.timestamp.GetUnderlying(),
                  user_db_info.nonce_count};
}
 
Inserts user data into database: void  AuthChecker::SetUserData(const  std::string& username,
                              const  std::string& nonce,
                              std::int64_t nonce_count,
                              TimePoint nonce_creation_time) const  {
                       uservice_dynconf::sql::kUpdateUser, nonce,
                       nonce_count, username);
}
 
Pushes unnamed nonce into database using kInsertUnnameNonce query: void  AuthChecker::PushUnnamedNonce(std::string nonce) const  {
  auto  res = pg_cluster_->Execute(
      uservice_dynconf::sql::kInsertUnnamedNonce,
}
 
Pops unnamed nonce from database and gets it's creation time using kSelectUnnamedNonce query: std::optional<TimePoint> AuthChecker::GetUnnamedNonceCreationTime(
    const  std::string& nonce) const  {
  auto  res =
                           uservice_dynconf::sql::kSelectUnnamedNonce, nonce);
 
  if  (res.IsEmpty()) return  std::nullopt;
 
}
 
kInsertUnnameNonce query looks like this: 
    "WITH expired AS( " 
    "  SELECT id FROM auth_schema.unnamed_nonce WHERE creation_time <= $1 " 
    "LIMIT 1 " 
    "), " 
    "free_id AS ( " 
    "SELECT COALESCE((SELECT id FROM expired LIMIT 1), " 
    "uuid_generate_v4()) AS id " 
    ") " 
    "INSERT INTO auth_schema.unnamed_nonce (id, nonce, creation_time) " 
    "SELECT " 
    "  free_id.id, " 
    "  $2, " 
    "  $3 " 
    "FROM free_id " 
    "ON CONFLICT (id) DO UPDATE SET " 
    "  nonce=$2, " 
    "  creation_time=$3 " 
    "  WHERE auth_schema.unnamed_nonce.id=(SELECT free_id.id FROM free_id " 
    "LIMIT 1) " ,
  
 
 
Authorization Factory 
Authorization checkers are produced by authorization factories derived from server::handlers::auth::AuthCheckerFactoryBase :
 
namespace  samples::digest_auth {
 
class  CheckerFactory final
 public :
  server::handlers::auth::AuthCheckerBasePtr operator()(
      const ::components::ComponentContext& context,
};
 
class  CheckerProxyFactory final
 public :
  server::handlers::auth::AuthCheckerBasePtr operator()(
      const ::components::ComponentContext& context,
};
 
}  
 Factories work with component system and parse handler-specific authorization configs:
server::handlers::auth::AuthCheckerBasePtr CheckerFactory::operator()(
    const ::components::ComponentContext& context,
  const  auto & digest_auth_settings =
      context
          .FindComponent<
          .GetSettings();
 
  return  std::make_shared<AuthChecker>(
      digest_auth_settings, auth_config["realm" ].As<std::string>({}), context,
}
Each factory should be registered at the beginning of the main() function via server::handlers::auth::RegisterAuthCheckerFactory  function call:
int  main(int  argc, const  char * const  argv[]) {
      "digest" , std::make_unique<samples::digest_auth::CheckerFactory>());
      "digest-proxy" ,
      std::make_unique<samples::digest_auth::CheckerProxyFactory>());
 
Static config 
That factory is invoked on each HTTP handler with the matching authorization type:
        handler-hello:
            path: /v1/hello
            task_processor: main-task-processor
            method: GET
            auth:           # Authorization config for this handler
                types:
                  - digest  # Authorization type that was specified in main()
                realm: registred@userver.com
        handler-hello-proxy:
            path: /v1/hello-proxy
            task_processor: main-task-processor
            method: GET
            auth:           # Authorization config for this handler
                types:
                  - digest-proxy
                realm: registred@userver.com
Digest settings are set using server::handlers::auth::digest::AuthCheckerSettingsComponent  and universal for each handler, which uses digest authentication:
        auth-digest-checker-settings:
            algorithm: MD5
            qops:
              - auth
            is-proxy: false
            is-session: false
            domains:
              - /v1/hello
            nonce-ttl: 1000s
        auth-digest-checker-settings-proxy:
            algorithm: MD5
            qops:
              - auth
            is-proxy: true
            is-session: false
            domains:
              - /v1/hello
            nonce-ttl: 1000s
You also need to provide a secret as a value to the http_server_digest_auth_secret key using components::Secdist .
        secdist: {}                                             # Component that stores configuration of hosts and passwords
        default-secdist-provider:                               # Component that loads configuration of hosts and passwords
            config: '/etc/digest_auth/secdist.json'             # Values are supposed to be stored in this file
            missing-ok: true                                    # ... but if the file is missing it is still ok
            environment-secrets-key: SERVER_DIGEST_AUTH_SECRET  # ... values will be loaded from this environment value
Secret is used to generate the value for the derivatives.
 
int main() 
Aside from calling server::handlers::auth::RegisterAuthCheckerFactory  for authorization factory registration, the main() function should also construct the component list and start the daemon:
  const  auto  component_list =
          .Append<samples::digest_auth::Hello>()
          .Append<samples::digest_auth::Hello>("handler-hello-proxy" )
          .Append<components::TestsuiteSupport>()
          .Append<server::handlers::TestsControl>()
          .Append<components::Secdist>()
          .Append<server::handlers::auth::digest::AuthCheckerSettingsComponent>(
              "auth-digest-checker-settings-proxy" )
          .Append<
 
Functional testing 
Functional tests  for the service could be implemented using the testsuite. To do that you have to:
Provide PostgreSQL schema to start the database: DROP  SCHEMA IF  EXISTS  auth_schema CASCADE;
 
CREATE  SCHEMA IF  NOT  EXISTS  auth_schema;
 
CREATE  EXTENSION IF  NOT  EXISTS  "uuid-ossp" ;
 
CREATE  TABLE  IF  NOT  EXISTS  auth_schema.users (
    username TEXT NOT  NULL ,
    nonce TEXT NOT  NULL ,
    timestamp  TIMESTAMPTZ NOT  NULL  DEFAULT  NOW(),
    nonce_count integer  NOT  NULL  DEFAULT  0 ,
    ha1 TEXT NOT  NULL ,
    PRIMARY  KEY(username)
);
 
CREATE  TABLE  IF  NOT  EXISTS  auth_schema.unnamed_nonce (
    id uuid NOT  NULL ,
    nonce TEXT NOT  NULL ,
    creation_time TIMESTAMPTZ NOT  NULL ,
    PRIMARY  KEY(id),
    UNIQUE (nonce)
);
 
Tell the testsuite to start the PostgreSQL database by adjusting the samples/digest_auth_service/tests/conftest.py  
Prepare the DB test data samples/digest_auth_service/postgresql/data/test_data.sql  
Write the test: import  pytest
 
import  auth_utils
 
 
@pytest.mark.pgsql('auth', files=['test_data.sql']) 
async def  test_authenticate_base(service_client):
    response = await service_client.get('/v1/hello' )
    assert  response.status == 401
 
    authentication_header = response.headers['WWW-Authenticate' ]
    auth_directives = auth_utils.parse_directives(authentication_header)
 
    auth_utils.auth_directives_assert(auth_directives)
 
    challenge = auth_utils.construct_challenge(auth_directives)
    auth_header = auth_utils.construct_header('username' , 'pswd' , challenge)
 
    response = await service_client.get(
        '/v1/hello' , headers={'Authorization' : auth_header},
    )
    assert  response.status == 200
    assert  'Authentication-Info'  in  response.headers
 
 
 
 
 
Full sources 
See the full example: