Applications should use the more secure digest authentication when
the server is accessible over a non-secure network. This requires
linking with -lcxxtls
, and doing a little bit more work.
Here's a minimal example of using digest authentication. This example accepts digest authentication for username “citizenkane” and password “rosebud”:
#include <x/fdlistener.H> #include <x/netaddr.H> #include <x/http/fdserver.H> #include <x/http/serverauth.H> #include <x/http/form.H> #include <iterator> class serverObj : public x::http::fdserverimpl, virtual public x::obj { x::http::serverauth serverauth; public: serverObj(const x::http::serverauth &serverauthArg) : serverauth(serverauthArg) { } ~serverObj() { } void received(const x::http::requestimpl &req, bool hasbody) override; }; void serverObj::received(const x::http::requestimpl &req, bool hasbody) { x::http::responseimpl resp; std::string username; std::list<x::http::responseimpl::challenge_info> challenges; auto scheme=serverauth->check_authentication (req, resp, username, challenges, [] (gcry_md_algos algorithm, const std::string &realm, const std::string &username, const x::uriimpl &uri) { if (username != "citizenkane") return std::string(); return x::http::serverauth::base::compute_a1(algorithm, username, "rosebud", realm); }); if (hasbody) discardbody(); if (scheme == x::http::auth::unknown) x::http::responseimpl::throw_unauthorized(challenges); std::string content= "Congratulations, you've authenticated as " + username + "\n"; resp.append(x::mime::structured_content_header::content_type, "text/plain; charset=UTF-8"); send(resp, req, content.begin(), content.end()); } class serverfactoryObj : virtual public x::obj { x::http::serverauth serverauth; public: serverfactoryObj() : serverauth(x::http::serverauth::create("HTTP AuthServer", "/")) { } ~serverfactoryObj() { } x::ref<serverObj> create() { return x::ref<serverObj>::create(serverauth); } }; void serverauth() { std::list<x::fd> socketlist; x::netaddr::create("", 0)->bind(socketlist, false); auto listener=x::fdlistener::create(socketlist); listener->start(x::http::fdserver::create(), x::ref<serverfactoryObj>::create()); std::cout << "Listening on http://localhost:" << socketlist.front()->getsockname()->port() << std::endl << "Press ENTER when done: " << std::flush; std::string dummy; std::getline(std::cin, dummy); } int main() { try { serverauth(); } catch (const x::exception &e) { std::cerr << e << std::endl; exit(1); } return 0; }
x::http::serverauth
The same
x::http::serverauth
should be used by all server connection threads.
Its regular constructor takes two arguments, a realm label, and the
realm's protection space; this is a
std::set<x::uriimpl>
.
The realm label is a unique string that identifies the realm, and
is generally displayed by clients when they prompt for authentication.
More than one realm can be defined (different
x::http::serverauth
s), for different
URI hierarchies on the same server, and each one must
have a different label.
The protection space defines which URIs on the server are subject to authentication; which URI hierarchies are a part of the authentication realm.
std::set<x::uriimpl> protection_space; protection_space.insert("/private"); protection_space.insert("/mail"); auto auth=x::http::serverauth::create("Mailbox", protection_space);
This specifies that the application requires authentication for any
“/private/*
”
“/mail/*
”
URI.
When the client accesses one of these for the first time, gets an
authentication challenge, and provides valid authorization, the
client will automatically supply authorization for all subsequent
requests for any URI that falls within
the given protection space.
In the event that the server is accessible through different authorities, such as by http and https, and the application uses the same realm for each relative URI under each authority, the protection space should include both the relative and all the equivalent absolute URI aliases:
std::set<x::uriimpl> protection_space; protection_space.insert("/private"); protection_space.insert("/mail"); protection_space.insert("http://www.example.com/private"); protection_space.insert("http://www.example.com/mail"); protection_space.insert("https://www.example.com/private"); protection_space.insert("https://www.example.com/mail"); auto auth=x::http::serverauth::create("Mailbox", protection_space);
In this example, the application server handles both “http://www.example” and “https://www.example” URLs, and in either case “/private” and “/mail” falls within the authentication-protected space. After a client succesfully authenticates, the absolute URIs give sufficient notice for the client to automatically use the same authentication when it asks for the other URIs.
If, for example, the same server has an alias of
example.com
, those absolute
URI should be included too.
std::set<x::uriimpl> protection_space; auto auth=x::http::serverauth::create("Mailbox", "/private", "/mail", "http://www.example.com", "https://www.example.com");
This is an equivalent example that uses an alternative constructor
that takes a variadic list of URIs instead of
a std::set
. The protection space is derived
from the variadic list as follows:
URIs that do not specify an authority, relative to the authority root, are put into the protection space, as is.
URIs with an authority get combined with each non-authority URI, and the result added to the protection space.
Here, “http://www.example.com” and “https://www.example.com” get combined, individually, with “/private” and “/mail”, and together with them produce the final protection space.
This provides a convenient shortcut for defining the full protection space, by reducing it to a list of relative and absolute URIs, which get combined in the correct way for common implementations of HTTP authentication.
x::http::serverauth
's methods are thread-safe,
but it contains the following members that are not protected by
thread safe access. They can be set immediately after constructing
a x::http::serverauth
,
but should not be modified after the application server starts
accepting connections.
auth->algorithms.push_back(GCRY_MD_SHA1);
algorithms
is a
std::list
of
gcry_md_algos
, the native hash method handle
from the underlying libgcrypt library.
The formal specification of digest authentication
include only MD5
as the
digest hash function.
Because of that,
algorithms
gets initialized with
just a GCRY_MD_MD5
. This default actually
come from the x::http::serverauth::algorithms
property.
This is a list, and more than one algorithm can be added to it,
either by listing multiple algorithms,
separated by whitespaces or commas in the
x::http::serverauth::algorithms
property,
or by manually
adding to algorithms
,
as in the example above. This results in listing all hash functions
in the server's authentication challenge.
Unfortunately, testing showed that many clients have various bugs when the server's authentication challenge specifies multiple hash functions, of which the client understands only one. This should be used only in controlled situations, after testing for proper client support.
The user agent client implements a digest challenge that uses any hash method that's supported by the libgcrypt. When there are multiple hash methods to choose from, the one with the largest bit size gets selected.
auth->nonce_expiration=120;
This is how often each hash's
nonce, or “salt”, remains valid,
as a number of seconds. The default value is 60 seconds, set by the
x::http::serverauth::nonce_expiration
property.
This should be sufficient. The implementation of digest authentication
in LibCXX sends a new
nonce in response to every authentication request, and all nonces
remain valid until they expire;
so as long as there's at least one request in a minute, the client
always supplies a valid nonce with its digest authentication request.
Even after the nonce expires, this only results in a minor delay, from an extra round trip between the client and the server, as a new nonce gets established.
It should only be necessary to adjust the expiration when working with
clients that are known to have problems reauthenticating, and retrying,
automatically
after an HTTP POST
or a
PUT
gets kicked back with a stale nonce; and when
this can happen due the application's specific nature or behavior.
check_authentication
()
In the
received
()
method, the first step when
the requested URI falls within the
realm's protection space is to
construct an x::http::responseimpl
in anticipation of an successful authentication, then invoke
x::http::serverauth
instance's
check_authentication
()
method with the following arguments:
The request parameter to received
().
A reference to the x::http::responseimpl
.
A reference to a std::string
that gets
set to the name of the authenticated user or login ID.
A reference to a std::list<x::http::responseimpl::challenge_info>
.
In the event that the authentication fails, this gets used to
form the authentication challenge response to the client.
A functor or a lambda, described below.
check_authentication
()
returns an x::http::auth
.
A value of x::http::auth::unknown
indicates
a failed authentication, which should result in an authentication
challenge response. This is done by calling
x::http::responseimpl::throw_unauthorized
()
with the
std::list<x::http::responseimpl::challenge_info>
parameter that
check_authentication
()
initialized. This throws an exception that results in an
authentication challenge response.
Any other value from check_authentication
()
indicates a succeeded authentication, and indicates the employed
authentication scheme, which would be
x::http::auth::digest
.
check_authentication
() sets the
username
to the authenticated user's identity,
and adds additional headers to the
x::http::responseimpl
parameter.
The application should use the
x::http::responseimpl
to form the response
to the request, using send
(), as shown
in
http_authserver.C
.
check_authentication
() invokes its
functor/lambda parameter to validate the authentication request.
The functor/lambda does not get called if the request does not
carry an authorization header, in which case
check_authentication
() returns
x::http::auth::unknown
.
The first parameter to the functor/lambda specifies the digest hash
function, as a native libgcrypt handle.
This is typically GCRY_MD_MD5
for a standard digest
authentication. The second parameter is the authentication realm,
the same realm label that was passed to
x::http::serverauth
's constructor.
The third parameter is the username in the authentication request.
The fourth parameter is a partial URI. This is not
the full URI from the request, but the parameter
from the authorization header, which would typically be the same
as the request URI without the authority part.
If the request went through an intermediate proxy, this may or may not
match the corresponding parts of the entire request. This parameter
is provided for informational purposes.
The functor/lambda should return the so-called “A1 hash”
for the specified username. This is an intermediate hash derived from
the authentication parameters. As shown in the
http_authserver.C
example, the hash gets computed by calling
x::http::serverauth::base::compute_a1
().
The parameters to
x::http::serverauth::base::compute_a1
() are:
the hash algorithm, the username, the password associated with the
username, and the authentication realm. If the username is not valid
and there is no associated password, an empty string gets returned
from the functor/lambda.
The example looks for username “citizenkane”, and
calculates the A1 hash for the password “rosebud”.
Note that a different hash gets calculated from
the same username and password in a different realm,
or with a non-standard hash function other than
GCRY_MD_MD5
. It's possible to calculate the
A1 hashes in advance, for the supported realm and the hash method,
and destroy the actual passwords. Compromised A1 hashes result
in compromising only their realm, and will not compromise different
realms, even if their hashes are derived from the same usernames and
passwords.
It's possible to use both digest and basic authentication together.
This makes it possible to support both older clients that only
implement basic authentication, and current HTTP
clients that support the better security of digest authentication.
This is done by passing a second lambda/functor to
check_authentication
() or
check_proxy_authentication
:
auto scheme=serverauth->check_authentication (req, resp, username, challenges, [] (gcry_md_algos algorithm, const std::string &realm, const std::string &username, const x::uriimpl &uri) { if (username != "citizenkane") return std::string(); return x::http::serverauth::base::compute_a1(algorithm, username, "rosebud", realm); }, [] (const std::string &usercolonpassword) { return usercolonpassword == "citizenkane:rosebud"; });
The second lambda/functor gets called when the client responds with a basic scheme authorization response. The lambda/functor receives a single argument, the username and the password from the client's request, separated by a colon.
Testing showed that some clients were not able to properly handle mixed authentication challenges that include both basic and digest authentication scheme, when they are formatted strictly in accordance with RFC 2617.
It was necessary to make some modifications, in order to work around this, and other client bugs. At this time, LibCXX's implementation seems to work with the tested client, but sufficient client testing should be done before employing mixed basic and digest authentication schemes.
When the application functions as a proxy, proxy authentication gets
implemented in the same way, except that
check_proxy_authentication
() gets used instead
of
check_authentication
(), and a failed
authentication gets reported by
x::http::responseimpl::throw_proxy_authentication_required
()
instead of
x::http::responseimpl::throw_unauthorized
().