Implementing digest authentication

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() noexcept
	{
	}

	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::http::content_type_header::name,
		    "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() noexcept
	{
	}

	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;
}

Creating a 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::serverauths), 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.

Note

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.

Note

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.

Invoking 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.

Using both digest and basic authentication

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.

Note

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.

Proxy authentication

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().