byfareska / hawk
Hawk HTTP authentication (MAC-signed requests and bewit-signed URIs) for modern PHP — a maintained fork of dflydev/hawk
Requires
- php: >=8.4
- ext-hash: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-02 16:08:23 UTC
README
Hawk is an HTTP authentication scheme using a message authentication code (MAC) algorithm to provide partial HTTP request cryptographic verification. — hawk README
About this fork
This is a maintained fork of dflydev/hawk by Beau Simensen and Dragonfly Development Inc. The original package was last released in 2013 and is no longer developed; the MIT licence and the original authorship are kept intact, and the protocol behaviour is unchanged.
What the fork changes:
- Runs on modern PHP. Requires PHP 8.4+, declares strict types, uses typed properties and promoted constructors. The original relied on dynamic properties, which PHP 8.2 deprecated and PHP 9 will reject outright.
- No dependencies. Nonces now come from
random_bytes()instead ofircmaxell/random-lib's low strength generator, which is built onmt_rand()and explicitly documented there as unsuitable for cryptography. - Constant-time MAC comparison everywhere, via
hash_equals(). The client used to compare response MACs with!==. extis escaped in the normalized string, as the Hawk specification requires. Without it, a newline insideextshifts the field boundaries, so two different requests can produce the same MAC.- Header values are escaped, and CR/LF are rejected. The original interpolated attribute values straight between quotes, so a value containing a quote or a newline produced a malformed — or injected — header.
- Malformed input is refused, not crashed on. A short bewit, a non-numeric
timestamp or an unknown credentials id used to raise PHP warnings or a
TypeError— a 500 driven by request content — instead of an authentication failure. - Sensitive parameters are marked as such, so the shared key stays out of
stack traces and
var_dump()output.
Wire compatibility is preserved: a bewit or an Authorization header issued by
dflydev/hawk verifies here and vice versa. The single exception is ext
escaping, which only affects requests that actually use ext — and which brings
this implementation in line with the reference one.
See CHANGELOG.md for the full list.
Installation
Through Composer as byfareska/hawk:
composer require byfareska/hawk
Requires PHP 8.4 or newer. No runtime dependencies.
Client
Building a Client
The Client has a few required dependencies. It is generally easier to
construct a Client by using the ClientBuilder. A Client can be built
without setting anything to get sane defaults.
Simple ClientBuilder Example
<?php // Simple example $client = Byfareska\Hawk\Client\ClientBuilder::create() ->build()
Complete ClientBuilderExample
<?php // A complete example $client = Byfareska\Hawk\Client\ClientBuilder::create() ->setCrypto($crypto) ->setTimeProvider($timeProvider) ->setNonceProvider($nonceProvider) ->setLocaltimeOffset($localtimeOffset) ->build()
Creating a Request
In order for a client to be able to sign a request, it needs to know the credentials for the user making the request, the URL, method, and optionally payload and content type of the request.
All available options include:
- payload: The body of the request
- content_type: The content-type for the request
- nonce: If a specific nonce should be used in favor of one being generated automatically by the nonce provider.
- ext: An ext value specific for this request
- app: The app for this request (Oz specific)
- dlg: The delegated-by value for this request (Oz specific)
Create Request Example
<?php $request = $client->createRequest( $credentials, 'http://example.com/foo/bar?whatever', 'POST', array( 'payload' => 'hello world!', 'content_type' => 'text/plain', ) ); // Assuming a hypothetical $headers object that can be used to add new headers // to an outbound request, we can add the resulting 'Authorization' header // for this Hawk request by doing: $headers->set( $request->header()->fieldName(), // 'Authorization' $request->header()->fieldValue() // 'Hawk id="12345", mac="ad8c9f', ...' );
The Client Request Object
The Request represents everything the client needs to know about a request
including a header and the artifacts that were used to create the request.
- header(): A
Headerinstance that represents the request - artifacts(): An
Artifactsinstance that contains the values that were used in creating the request
The header is required to be able to get the properly formatted Hawk authorization header to send to the server. The artifacts are useful in the case that authentication will be done on the server response.
Authenticate Server Response
Hawk provides the ability for the client to authenticate a server response to ensure that the response sent back is from the intended target.
All available options include:
- payload: The body of the response
- content_type: The content-type for the response
Authenticate Response Example
<?php // Assuming a hypothetical $headers object that can be used to get headers sent // back as the response of a user agent request, we can get the value for the // 'Server-Authorization' header. $header = $headers->get('Server-Authorization'); // We need to use the original credentials, the original request, the value // for the 'Server-Authorization' header, and optionally the payload and // content type of the response from the server. $isAuthenticatedResponse = $client->authenticate( $credentials, $request, $header, array( 'payload' => '{"message": "good day, sir!"}', 'content_type' => 'application/json', ) );
Complete Client Example
<?php // Create a set of Hawk credentials $credentials = new Byfareska\Hawk\Credentials\Credentials( 'afe89a3x', // shared key 'sha256', // default: sha256 '12345' // identifier, default: null ); // Create a Hawk client $client = Byfareska\Hawk\Client\ClientBuilder::create() ->build(); // Create a Hawk request based on making a POST request to a specific URL // using a specific user's credentials. Also, we're expecting that we'll // be sending a payload of 'hello world!' with a content-type of 'text/plain'. $request = $client->createRequest( $credentials, 'http://example.com/foo/bar?whatever', 'POST', array( 'payload' => 'hello world!', 'content_type' => 'text/plain', ) ); // Ask a really useful fictional user agent to make a request; note that the // request we are making here matches the details that we told the Hawk client // about our request. $response = Fictional\UserAgent::makeRequest( 'POST', 'http://example.com/foo/bar?whatever', array( 'content_type' => 'text/plain', $request->header()->fieldName() => $request->header()->fieldValue(), ), 'hello world!' ); // This part is optional but recommended! At this point if we have a successful // response we could just look at the content and be done with it. However, we // are given the tools to authenticate the response to ensure that the response // we were given came from the server we were expecting to be talking to. $isAuthenticatedResponse = $client->authenticate( $credentials, $request, $response->headers->get('Server-Authorization'), array( 'payload' => $response->getContent(), 'content_type' => $response->headers->get('content-type'), ) ); if (!$isAuthenticatedResponse) { die("The server did a very bad thing..."); } // Huzzah!
Bewit
Hawk supports a method for granting third-parties temporary access to individual resources using a query parameter called bewit.
The return value is a string that represents the bewit. This string should be
added to a requested URI by appending it to the end of the URI. If the URI has
query parameters already, the bewit should have &bewit= appended to the front
of it. If the URI does not have query parameters already, the bewit should
have ?bewit= appended to the front of it.
Client Bewit Example
<?php $bewit = $client->createBewit( $credentials, 'https://example.com/posts?foo=bar', 300 // ttl in seconds );
Server
Building a Server
The Server has a few required dependencies. It is generally easier to
construct a Server by using the ServerBuilder. A Server can be built
without setting anything but the credentials provider to get sane defaults.
Simple ServerBuilder Example
<?php $credentialsProvider = function ($id) { if ('12345' === $id) { return new Byfareska\Hawk\Credentials\Credentials( 'afe89a3x', // shared key 'sha256', // default: sha256 '12345' // identifier, default: null ); } }; // Simple example $server = Byfareska\Hawk\Server\ServerBuilder::create($credentialsProvider) ->build()
Complete ServerBuilderExample
<?php $credentialsProvider = function ($id) { if ('12345' === $id) { return new Byfareska\Hawk\Credentials\Credentials( 'afe89a3x', // shared key 'sha256', // default: sha256 '12345' // identifier, default: null ); } }; // A complete example $server = Byfareska\Hawk\Server\ServerBuilder::create($credentialsProvider) ->setCrypto($crypto) ->setTimeProvider($timeProvider) ->setNonceValidator($nonceValidator) ->setTimestampSkewSec($timestampSkewSec) ->setLocaltimeOffsetSec($localtimeOffsetSec) ->build()
Authenticating a Request
In order for a server to be able to authenticate a request, it needs to be able to build the same MAC that the client did. It does this by getting the same information about the request that the client knew about when it signed the request.
In particular, the authorization header should include the ID. This ID is used to retrieve the credentials (notably the key) in order to calculate the MAC based on the rest of the request information.
Authenticate Example
<?php // Get the authorization header for the request; it should be in the form // of 'Hawk id="...", mac="...", [...]' $authorization = $headers->get('Authorization'); try { $response = $server->authenticate( 'POST', 'example.com', 80, '/foo/bar?whatever', 'text/plain', 'hello world!' $authorization ); } catch(Byfareska\Hawk\Server\UnauthorizedException $e) { // If authorization is incorrect (invalid mac, etc.) we can catch an // unauthorized exception. throw $e; } // The credentials associated with this request. This is where one could access // the ID for the user that made this request. $credentials = $response->credentials(); // The artifacts associated with this request. This is where one could access // things like the 'ext', 'app', and 'dlg' values sent with the request. $artifacts = $response->artifacts();
The Server Response Object
The Response represents everything the server needs to know about a request
including the credentials and artifacts that are associated with the request.
- credentials()
- artifacts()
Creating a Response Header
Hawk provides the ability for the server to sign the response to provide the client with a way to authenticate a server response.
All available options include:
- payload: The body of the request
- content_type: The content-type for the request
- ext: An ext value specific for this request
Create Response Header Example
<?php // Using the same credentials and artifacts from the server authenticate // response, we can create a 'Server-Authorization' header. $header = $server->createHeader($credentials, $artifacts, array( 'payload' => '{"message": "good day, sir!"}', 'content_type' => 'application/json', )); // Set the header using PHP's header() function. header(sprintf("%s: %s", $header->fieldName(), $header->fieldValue()));
Complete Server Example
<?php // Create a simple credentials provider $credentialsProvider = function ($id) { if ('12345' === $id) { return new Byfareska\Hawk\Credentials\Credentials( 'afe89a3x', // shared key 'sha256', // default: sha256 '12345' // identifier, default: null ); } }; // Create a Hawk server $server = Byfareska\Hawk\Server\ServerBuilder::create($credentialsProvider) ->build() // Get the authorization header for the request; it should be in the form // of 'Hawk id="...", mac="...", [...]' $authorization = $headers->get('Authorization'); try { $response = $server->authenticate( 'POST', 'example.com', 80, '/foo/bar?whatever', 'text/plain', 'hello world!' $authorization ); } catch(Byfareska\Hawk\Server\UnauthorizedException $e) { // If authorization is incorrect (invalid mac, etc.) we can catch an // unauthorized exception. throw $e; } // Huzzah! Do something at this point with the request as we now know that // it is an authenticated Hawk request. // // ... // // Ok we are done doing things! Assume based on what we did we ended up deciding // the following payload and content type should be used: $payload = '{"message": "good day, sir!"}'; $contentType = 'application/json'; // Create a Hawk header to sign our response $header = $server->createHeader($credentials, $artifacts, array( 'payload' => $payload, 'content_type' => $contentType, )); // Send some headers header(sprintf("%s: %s", 'Content-Type', 'application/json')); header(sprintf("%s: %s", $header->fieldName(), $header->fieldValue())); // Output our payload print $payload;
Bewit
Hawk supports a method for granting third-parties temporary access to individual resources using a query parameter called bewit.
Bewit authentication should only occur for GET and HEAD requests. The return
value of an authenticated bewit is a Server Response object.
Server Bewit Example
<?php $response = $server->authenticateBewit( 'example.com', 443, '/posts?bewit=ZXhxYlpXdHlrRlpJaDJEN2NYaTlkQVwxMzY4OTk2ODAwXE8wbWhwcmdvWHFGNDhEbHc1RldBV3ZWUUlwZ0dZc3FzWDc2dHBvNkt5cUk9XA' );
Crypto
Byfareska\Hawk\Crypto\Crypto
Tools for calculation of and comparison of MAC values.
- calculatePayloadHash($payload, $algorithm, $contentType)
- calculateMac($type, CredentialsInterface $credentials, Artifacts $attributes)
- calculateTsMac($ts, CredentialsInterface $credentials)
- fixedTimeComparison($a, $b)
Compares two strings in constant time. Kept under its original name; it now delegates tohash_equals()rather than looping over the bytes by hand.
Only sha256 and sha512 are accepted as MAC algorithms (Crypto::ALGORITHMS).
Anything else is rejected with Byfareska\Hawk\Exception\InvalidArgumentException
instead of reaching hash_hmac(), which would raise a ValueError.
Byfareska\Hawk\Crypto\Artifacts
A container for all of the pieces of data that may go into the creation of a MAC.
Credentials
Byfareska\Hawk\Credentials\CredentialsInterface
Represents a valid set of credentials.
- key(): Used to calculate the MAC
- algorithm(): The algorithm used to calculate hashes
- id(): An identifier (e.g. username) for whom the key belongs
In some contexts only the key may be known.
Byfareska\Hawk\Credentials\Credentials
A simple implementation of CredentialsInterface.
<?php $credentials = new Byfareska\Hawk\Credentials\Credentials( $key, // shared key $algorithm, // default: sha256 $id // identifier, default: null );
Header
Byfareska\Hawk\Header\Header
- fieldName(): The name for the header field
- fieldValue(): The value for the header field
- attributes(): The attributes used to build the field value
Byfareska\Hawk\Header\HeaderFactory
-
create($fieldName, array $attributes = null)
Creates a Hawk header for a given field name for a set of attributes. -
createFromString($fieldName, $fieldValue, array $requiredKeys = null)
Creates a Hawk header for a given field name from a Hawk value string. For example, 'Hawk id="foo", mac="1234"' would be an example of a Hawk value string. This is useful for converting a header value coming in off the wire.Throws:
- Byfareska\Hawk\Header\FieldValueParserException
- Byfareska\Hawk\Header\NotHawkAuthorizationException
Byfareska\Hawk\Header\HeaderParser
-
parseFieldValue($fieldValue, array $requiredKeys = null)
Parses a field value string into an associative array of attributes.Throws:
- Byfareska\Hawk\Header\FieldValueParserException
- Byfareska\Hawk\Header\NotHawkAuthorizationException
Byfareska\Hawk\Header\FieldValueParserException
Indicates that a string claims to be a Hawk string but it cannot be completely parsed. This is mostly a sign of a corrupted or malformed header value.
Byfareska\Hawk\Header\NotHawkAuthorizationException
Indicates that the string has nothing to do with Hawk. Currently means that the string does not start with 'Hawk'.
Exceptions
Every exception thrown by this library implements
Byfareska\Hawk\Exception\HawkException, so a caller can catch the whole
package with one catch instead of falling back to \Throwable:
Byfareska\Hawk\Server\UnauthorizedException— the request did not authenticate. This is a normal outcome, not a failure.Byfareska\Hawk\Header\NotHawkAuthorizationException,Byfareska\Hawk\Header\FieldValueParserException— the field value could not be read as a Hawk header.Byfareska\Hawk\Exception\InvalidArgumentException— misuse of the API (an unsupported algorithm, a relative URI, a backslash in a bewit field).
License
MIT, see LICENSE. Copyright is shared: the original work is © 2013 Dragonfly Development Inc., the fork © 2026 Byfareska.