├── .gitignore ├── LICENSE ├── LICENSE-orig ├── README.md ├── composer.json ├── docs ├── Bewit.md ├── Client.md ├── Credentials.md ├── Getting Started.md ├── Header.md └── Server.md ├── phpunit.xml.dist ├── src ├── Client │ ├── Client.php │ ├── ClientBuilder.php │ ├── ClientInterface.php │ └── Request.php ├── Credentials │ ├── CallbackCredentialsProvider.php │ ├── Credentials.php │ ├── CredentialsInterface.php │ ├── CredentialsNotFoundException.php │ └── CredentialsProviderInterface.php ├── Crypto │ ├── Artifacts.php │ └── Crypto.php ├── Header │ ├── FieldValueParserException.php │ ├── Header.php │ ├── HeaderFactory.php │ ├── HeaderParser.php │ └── NotHawkAuthorizationException.php ├── Message │ └── Message.php ├── Nonce │ ├── CallbackNonceValidator.php │ ├── DefaultNonceProviderFactory.php │ ├── NonceProvider.php │ ├── NonceProviderInterface.php │ └── NonceValidatorInterface.php ├── Server │ ├── Response.php │ ├── Server.php │ ├── ServerBuilder.php │ ├── ServerInterface.php │ └── UnauthorizedException.php └── Time │ ├── ConstantTimeProvider.php │ ├── DefaultTimeProviderFactory.php │ ├── TimeProvider.php │ └── TimeProviderInterface.php └── tests ├── Client └── ClientTest.php ├── Credentials └── CredentialsTest.php ├── Crypto ├── ArtifactsTest.php └── CryptoTest.php ├── Header ├── HeaderFactoryTest.php └── HeaderParserTest.php ├── Message └── MessageTest.php └── Server └── ServerTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .DS_Store 3 | /.idea/ 4 | composer.lock 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Shiitz Garg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE-orig: -------------------------------------------------------------------------------- 1 | Originally based on works authored by Dragonfly Development Inc. License of original works: 2 | 3 | Copyright (c) 2013 Dragonfly Development Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hawk — A PHP Implementation 2 | =========================== 3 | 4 | > Hawk is an HTTP authentication scheme using a message authentication code 5 | > (MAC) algorithm to provide partial HTTP request cryptographic verification. 6 | > — [hawk README][0] 7 | 8 | This implementation is a fork of the PHP port of Hawk at [dflydev/dflydev-hawk][2] 9 | 10 | Usage 11 | ------- 12 | Hawk's library is divided into two main parts, Client and Server. See docs/Client.md 13 | and docs/Server.md for respective example on how to use them. 14 | 15 | License 16 | ------- 17 | 18 | The MIT License (See LICENSE) 19 | 20 | Originally based on [dflydev/dflydev-hawk][2] (licensed under MIT) 21 | 22 | 23 | [0]: https://github.com/hueniverse/hawk 24 | [1]: http://getcomposer.org/ 25 | [2]: https://github.com/dflydev/dflydev-hawk 26 | [3]: https://github.com/hueniverse/oz 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dragooon/hawk", 3 | "description": "Hawk", 4 | "keywords": [ 5 | "hawk", 6 | "authentication", 7 | "bewit", 8 | "protocol" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Dragonfly Development Inc.", 14 | "email": "info@dflydev.com", 15 | "homepage": "http://dflydev.com" 16 | }, 17 | { 18 | "name": "Beau Simensen", 19 | "email": "beau@dflydev.com", 20 | "homepage": "http://beausimensen.com" 21 | }, 22 | { 23 | "name": "Shitiz Garg", 24 | "email": "mail@dragooon.net", 25 | "homepage": "http://dragooon.net" 26 | } 27 | ], 28 | "autoload": { 29 | "psr-4": { 30 | "Dragooon\\Hawk\\": "src" 31 | } 32 | }, 33 | "require": { 34 | "ircmaxell/random-lib": "^1.0@dev", 35 | "php": ">=5.4.0" 36 | }, 37 | "require-dev": { 38 | "codeclimate/php-test-reporter": "~0.1@dev", 39 | "phpunit/phpunit": "~4.5", 40 | "squizlabs/php_codesniffer": "~2.3" 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "1.0.x-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/Bewit.md: -------------------------------------------------------------------------------- 1 | Bewit 2 | ------ 3 | 4 | Hawk supports a method for granting third-parties temporary access to individual 5 | resources using a query parameter called bewit. This can be used for granting access 6 | to protected resources such as images or videos to an user. 7 | 8 | ### Client 9 | 10 | The return value is a string that represents the bewit. This string should be 11 | added to a requested URI by appending it to the end of the URI. If the URI has 12 | query paramters already, the bewit should have `&bewit=` appended to the front 13 | of it. If the URI does not have query paramters already, the bewit should 14 | have `?bewit=` appended to the front of it. 15 | 16 | #### Client Bewit Example 17 | 18 | ```php 19 | createBewit( 22 | $credentials, 23 | 'https://example.com/posts?foo=bar', 24 | 300 // ttl in seconds 25 | ); 26 | ``` 27 | 28 | ### Server 29 | 30 | Bewit authentication should only occur for `GET` and `HEAD` requests. The return 31 | value of an authenticated bewit is a Server Response object. 32 | 33 | #### Server Bewit Example 34 | 35 | ```php 36 | authenticateBewit( 39 | 'example.com', 40 | 443, 41 | '/posts?bewit=ZXhxYlpXdHlrRlpJaDJEN2NYaTlkQVwxMzY4OTk2ODAwXE8wbWhwcmdvWHFGNDhEbHc1RldBV3ZWUUlwZ0dZc3FzWDc2dHBvNkt5cUk9XA' 42 | // This above bewit= parameter is generated by the client in the previous example 43 | ); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/Client.md: -------------------------------------------------------------------------------- 1 | Client 2 | ------ 3 | 4 | ### Building a Client 5 | 6 | The `Client` has a few required dependencies. It is generally easier to 7 | construct a `Client` by using the `ClientBuilder`. A `Client` can be built 8 | without setting anything to get sane defaults. 9 | 10 | #### Simple ClientBuilder Example 11 | 12 | ```php 13 | build() 18 | ``` 19 | 20 | #### Complete ClientBuilderExample 21 | 22 | ```php 23 | setCrypto($crypto) 28 | ->setTimeProvider($timeProvider) 29 | ->setNonceProvider($nonceProvider) 30 | ->setLocaltimeOffset($localtimeOffset) 31 | ->build() 32 | ``` 33 | 34 | ### Creating a Request 35 | 36 | In order for a client to be able to sign a request, it needs to know the 37 | credentials for the user making the request, the URL, method, and optionally 38 | payload and content type of the request. 39 | 40 | All available options include: 41 | 42 | * **payload**: The body of the request 43 | * **content_type**: The content-type for the request 44 | * **nonce**: If a specific nonce should be used in favor of one being generated 45 | automatically by the nonce provider. 46 | * **ext**: An ext value specific for this request 47 | * **app**: The app for this request ([Oz][3] specific) 48 | * **dlg**: The delegated-by value for this request ([Oz][3] specific) 49 | 50 | 51 | #### Create Request Example 52 | 53 | ```php 54 | createRequest( 57 | $credentials, 58 | 'http://example.com/foo/bar?whatever', 59 | 'POST', 60 | array( 61 | 'payload' => 'hello world!', 62 | 'content_type' => 'text/plain', 63 | ) 64 | ); 65 | 66 | // Assuming a hypothetical $headers object that can be used to add new headers 67 | // to an outbound request, we can add the resulting 'Authorization' header 68 | // for this Hawk request by doing: 69 | $headers->set( 70 | $request->header()->fieldName(), // 'Authorization' 71 | $request->header()->fieldValue() // 'Hawk id="12345", mac="ad8c9f', ...' 72 | ); 73 | 74 | ``` 75 | 76 | #### The Client Request Object 77 | 78 | The `Request` represents everything the client needs to know about a request 79 | including a header and the artifacts that were used to create the request. 80 | 81 | * **header()**: A `Header` instance that represents the request 82 | * **artifacts()**: An `Artifacts` instance that contains the values that were 83 | used in creating the request 84 | 85 | The **header** is required to be able to get the properly formatted Hawk 86 | authorization header to send to the server. The **artifacts** are useful in the 87 | case that authentication will be done on the server response. 88 | 89 | 90 | ### Authenticate Server Response 91 | 92 | Hawk provides the ability for the client to authenticate a server response to 93 | ensure that the response sent back is from the intended target. 94 | 95 | All available options include: 96 | 97 | * **payload**: The body of the response 98 | * **content_type**: The content-type for the response 99 | 100 | 101 | #### Authenticate Response Example 102 | 103 | ```php 104 | get('Server-Authorization'); 110 | 111 | // We need to use the original credentials, the original request, the value 112 | // for the 'Server-Authorization' header, and optionally the payload and 113 | // content type of the response from the server. 114 | $isAuthenticatedResponse = $client->authenticate( 115 | $credentials, 116 | $request, 117 | $header, 118 | array( 119 | 'payload' => '{"message": "good day, sir!"}', 120 | 'content_type' => 'application/json', 121 | ) 122 | ); 123 | ``` 124 | 125 | ### Complete Client Example 126 | 127 | ```php 128 | build(); 140 | 141 | // Create a Hawk request based on making a POST request to a specific URL 142 | // using a specific user's credentials. Also, we're expecting that we'll 143 | // be sending a payload of 'hello world!' with a content-type of 'text/plain'. 144 | $request = $client->createRequest( 145 | $credentials, 146 | 'http://example.com/foo/bar?whatever', 147 | 'POST', 148 | array( 149 | 'payload' => 'hello world!', 150 | 'content_type' => 'text/plain', 151 | ) 152 | ); 153 | 154 | // Ask a really useful fictional user agent to make a request; note that the 155 | // request we are making here matches the details that we told the Hawk client 156 | // about our request. 157 | $response = Fictional\UserAgent::makeRequest( 158 | 'POST', 159 | 'http://example.com/foo/bar?whatever', 160 | array( 161 | 'content_type' => 'text/plain', 162 | $request->header()->fieldName() => $request->header()->fieldValue(), 163 | ), 164 | 'hello world!' 165 | ); 166 | 167 | // This part is optional but recommended! At this point if we have a successful 168 | // response we could just look at the content and be done with it. However, we 169 | // are given the tools to authenticate the response to ensure that the response 170 | // we were given came from the server we were expecting to be talking to. 171 | $isAuthenticatedResponse = $client->authenticate( 172 | $credentials, 173 | $request, 174 | $response->headers->get('Server-Authorization'), 175 | array( 176 | 'payload' => $response->getContent(), 177 | 'content_type' => $response->headers->get('content-type'), 178 | ) 179 | ); 180 | 181 | if (!$isAuthenticatedResponse) { 182 | die("The server did a very bad thing..."); 183 | } 184 | 185 | // Huzzah! 186 | ``` -------------------------------------------------------------------------------- /docs/Credentials.md: -------------------------------------------------------------------------------- 1 | Credentials 2 | ----------- 3 | 4 | Credentials are used to identify any user with valid keys to authenticate via Hawk. See 5 | `Dragooon\Hawk\Credentials\CredentialsInterface` for the field's information 6 | 7 | ### Simple Example 8 | 9 | A simple implementation of `CredentialsInterface`. 10 | 11 | ```php 12 | hawkKey, 37 | $user->hawkAlgo, 38 | $user->id 39 | ); 40 | } 41 | ); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/Getting Started.md: -------------------------------------------------------------------------------- 1 | Getting Started 2 | ------ 3 | 4 | This guide covers the basic authentication by a server and making request by a client, for more advanced usage see the 5 | respective docs. 6 | 7 | ### Installation 8 | 9 | The library can be installed using composer, currently it's not listed on packagist. An example composer.json: 10 | 11 | ```json 12 | { 13 | "repositories": [ 14 | { 15 | "type": "vcs", 16 | "url": "https://github.com/Dragooon/php-hawk" 17 | } 18 | ], 19 | "require": { 20 | "Dragooon/hawk": "dev-master" 21 | } 22 | } 23 | ``` 24 | 25 | Including the above in composer.json and including vendor/autoload.php into your PHP scripts should allow you to 26 | start using the library. See composer documentation for more information 27 | 28 | ### Server 29 | 30 | For a server to be able to authenticate Hawk requests, you need to use Dragooon\Hawk\Server\Server library. The 31 | constructor requires 6 arguments 32 | 33 | - **Crypto**: The cryptographic library, Dragooon\Hawk\Crypto\Crypto should be enough 34 | - **CredentialsProvider**: A Credentials provider to be able to load any authenticating user by ID, see docs/Credentials.md 35 | for more information 36 | - **TimeProvider**: An object to provide current time information, default implementation in Dragooon\Hawk\Time\TimeProvider 37 | simply returns the current UNIX timestamp 38 | - **NonceValidator**: A validator for Nonce values. See official Hawk documentation for more infromation on Nonce values 39 | - **TimestampSkewSec**: Difference (in seconds) to allow for two different timestamps to be considered equal. Defaults 40 | to 60 41 | - **LocaltimeOffsetSec**: Offset by which current timestamp is shifted before validating 42 | 43 | The above arguments can be simplified using ServerBuilder which loads the default objects and values and simply requires 44 | the Credentials Provider 45 | 46 | Example: 47 | 48 | ```php 49 | $credentialsProvider = new Dragooon\Hawk\Credentials\CallbackCredentialsProvider( 50 | function($id) { 51 | if ('12345' === $id) { 52 | return new Dragooon\Hawk\Credentials\Credentials( 53 | 'afe89a3x', // shared key 54 | 'sha256', // default: sha256 55 | '12345' // identifier, default: null 56 | ); 57 | } 58 | } 59 | ); 60 | 61 | $server = Dragooon\Hawk\Server\ServerBuilder::create($credentialsProvider) 62 | ->build() 63 | ``` 64 | 65 | ###### Checking whether a request is using Hawk protocol or not 66 | 67 | Any Hawk request is authenticated by checking the Authorization HTTP header. It should start from Hawk to check for 68 | validity. 69 | 70 | Example request from a client: 71 | 72 | ``` 73 | GET /resource/1?b=1&a=2 HTTP/1.1 74 | Host: example.com:8000 75 | Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=" 76 | ``` 77 | 78 | Example to see if a request is a Hawk request: 79 | 80 | ```php 81 | $authorization = $_SERVER['HTTP_AUTHORIZATION']; // HTTP's Authorization passed by the client 82 | 83 | if ($server->checkRequestForHawk($authorization) { 84 | // This is a hawk request 85 | } 86 | else { 87 | // This is something else 88 | } 89 | ``` 90 | 91 | ###### Authenticating a client 92 | 93 | Now that we're aware whether it's a Hawk request or not, we'd need to authenticate a client. 94 | 95 | ```php 96 | try { 97 | $response = $server->authenticate( 98 | 'POST', 99 | 'example.com', 100 | 80, 101 | '/foo/bar?whatever', 102 | 'text/plain', 103 | 'hello world!' 104 | $authorization 105 | ); 106 | // The client authenticated if no exceptions were thrown 107 | } 108 | } catch(Dragooon\Hawk\Server\UnauthorizedException $e) { 109 | // The client failed to authenticate 110 | } 111 | 112 | $credentials = $response->credentials(); // This can be used to see which client's ID is it to identify the user 113 | ``` 114 | 115 | Remember that the id parameter is always passed in the Authorization header, this same ID is passed to 116 | CredentialsProvider in order to identify which client the server needs to authenticate. 117 | 118 | ### Client 119 | 120 | A client is any node making request to a server using Hawk protocol. An example request generated by a client is given 121 | above. Dragooon\Hawk\Client\Client requires 4 arguments by default 122 | 123 | - **Crypto**: The cryptographic library, Dragooon\Hawk\Crypto\Crypto should be enough 124 | for more information 125 | - **TimeProvider**: An object to provide current time information, default implementation in Dragooon\Hawk\Time\TimeProvider 126 | simply returns the current UNIX timestamp 127 | - **NonceValidator**: A validator for Nonce values. See official Hawk documentation for more infromation on Nonce values 128 | - **LocaltimeOffsetSec**: Offset by which current timestamp is shifted before validating 129 | 130 | The above arguments can be simplified by using Dragooon\Hawk\Client\ClientBuilder which doesn't require any arguments 131 | by default 132 | 133 | ```php 134 | $client = Dragooon\Hawk\Client\ClientBuilder::create() 135 | ->build(); 136 | ``` 137 | 138 | ###### Creating a request 139 | 140 | A client can create a request simply by 141 | 142 | ```php 143 | $request = $client->createRequest( 144 | $credentials, // Instance of Dragooon\Hawk\Credentials\CredentialsInterface 145 | 'http://example.com/foo/bar?whatever', 146 | 'GET', 147 | array( 148 | ) 149 | ); 150 | 151 | // Once the request has been created, 152 | $headerField = $request->header()->fieldName(); // Field name, for example "Authorization" 153 | $headerValue = $request->header()->fieldValue(); // Value for the above field 154 | ``` 155 | 156 | The above header field value can be passed to the server in the request using your favourite HTTP library, cURL for 157 | example: 158 | 159 | ```php 160 | $headers = array(); 161 | $headers[] = $request->header()->fieldName() . ': ' . $request->header()->fieldValue(); 162 | 163 | $ch = curl_init(); 164 | curl_setopt($ch, CURLOPT_URL, "http://example.com"); 165 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 166 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 167 | 168 | $response = curl_exec($ch); 169 | 170 | curl_close($ch); 171 | ``` -------------------------------------------------------------------------------- /docs/Header.md: -------------------------------------------------------------------------------- 1 | Header 2 | ------ 3 | 4 | The classes under the namespace Header are used to represent a request's header field, 5 | used for authorization and authentication by Hawk. 6 | 7 | An example Header for authorization by a client is: 8 | 9 | ```Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", hash="Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=", ext="some-app-ext-data", mac="aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw="``` 10 | 11 | And an example Header by server for payload and identity validation is: 12 | 13 | ```Server-Authorization: Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"``` 14 | 15 | -------------------------------------------------------------------------------- /docs/Server.md: -------------------------------------------------------------------------------- 1 | Server 2 | ------ 3 | 4 | ### Building a Server 5 | 6 | The `Server` has a few required dependencies. It is generally easier to 7 | construct a `Server` by using the `ServerBuilder`. A `Server` can be built 8 | without setting anything but the crendetials provider to get sane defaults. 9 | 10 | #### Simple ServerBuilder Example 11 | 12 | ```php 13 | build() 30 | ``` 31 | 32 | #### Complete ServerBuilderExample 33 | 34 | ```php 35 | setCrypto($crypto) 52 | ->setTimeProvider($timeProvider) 53 | ->setNonceValidator($nonceValidator) 54 | ->setTimestampSkewSec($timestampSkewSec) 55 | ->setLocaltimeOffsetSec($localtimeOffsetSec) 56 | ->build() 57 | ``` 58 | 59 | ### Authenticating a Request 60 | 61 | In order for a server to be able to authenticate a request, it needs to be able 62 | to build the same MAC that the client did. It does this by getting the same 63 | information about the request that the client knew about when it signed the 64 | request. 65 | 66 | In particular, the authorization header should include the ID. This ID is used 67 | to retrieve the credentials (notably the key) in order to calculate the MAC 68 | based on the rest of the request information. 69 | 70 | #### Authenticate Example 71 | 72 | ```php 73 | get('Authorization'); 78 | 79 | try { 80 | $response = $server->authenticate( 81 | 'POST', 82 | 'example.com', 83 | 80, 84 | '/foo/bar?whatever', 85 | 'text/plain', 86 | 'hello world!' 87 | $authorization 88 | ); 89 | } catch(Dragooon\Hawk\Server\UnauthorizedException $e) { 90 | // If authorization is incorrect (invalid mac, etc.) we can catch an 91 | // unauthorized exception. 92 | throw $e; 93 | } 94 | 95 | // The credentials associated with this request. This is where one could access 96 | // the ID for the user that made this request. 97 | $credentials = $response->credentials(); 98 | 99 | // The artifacts associated with this request. This is where one could access 100 | // things like the 'ext', 'app', and 'dlg' values sent with the request. 101 | $artifacts = $response->artifacts(); 102 | ``` 103 | 104 | #### The Server Response Object 105 | 106 | The `Response` represents everything the server needs to know about a request 107 | including the credentials and artifacts that are associated with the request. 108 | 109 | * **credentials()** 110 | * **artifacts()** 111 | 112 | 113 | ### Creating a Response Header 114 | 115 | Hawk provides the ability for the server to sign the response to privde the 116 | client with a way to authenticate a server response. 117 | 118 | All available options include: 119 | 120 | * **payload**: The body of the request 121 | * **content_type**: The content-type for the request 122 | * **ext**: An ext value specific for this request 123 | 124 | 125 | #### Create Response Header Example 126 | 127 | ```php 128 | createHeader($credentials, $artifacts, array( 133 | 'payload' => '{"message": "good day, sir!"}', 134 | 'content_type' => 'application/json', 135 | )); 136 | 137 | // Set the header using PHP's header() function. 138 | header(sprintf("%s: %s", $header->fieldName(), $header->fieldValue())); 139 | ``` 140 | 141 | ### Complete Server Example 142 | 143 | ```php 144 | build() 160 | 161 | // Get the authorization header for the request; it should be in the form 162 | // of 'Hawk id="...", mac="...", [...]' 163 | $authorization = $headers->get('Authorization'); 164 | 165 | try { 166 | $response = $server->authenticate( 167 | 'POST', 168 | 'example.com', 169 | 80, 170 | '/foo/bar?whatever', 171 | 'text/plain', 172 | 'hello world!' 173 | $authorization 174 | ); 175 | } catch(Dragooon\Hawk\Server\UnauthorizedException $e) { 176 | // If authorization is incorrect (invalid mac, etc.) we can catch an 177 | // unauthorized exception. 178 | throw $e; 179 | } 180 | 181 | // Huzzah! Do something at this point with the request as we now know that 182 | // it is an authenticated Hawk request. 183 | // 184 | // ... 185 | // 186 | // Ok we are done doing things! Assume based on what we did we ended up deciding 187 | // the following payload and content type should be used: 188 | 189 | $payload = '{"message": "good day, sir!"}'; 190 | $contentType = 'application/json'; 191 | 192 | // Create a Hawk header to sign our response 193 | $header = $server->createHeader($credentials, $artifacts, array( 194 | 'payload' => $payload, 195 | 'content_type' => $contentType, 196 | )); 197 | 198 | // Send some headers 199 | header(sprintf("%s: %s", 'Content-Type', 'application/json')); 200 | header(sprintf("%s: %s", $header->fieldName(), $header->fieldValue())); 201 | 202 | // Output our payload 203 | print $payload; 204 | ``` 205 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | ./tests/ 10 | 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Client/Client.php: -------------------------------------------------------------------------------- 1 | crypto = $crypto; 34 | $this->timeProvider = $timeProvider; 35 | $this->nonceProvider = $nonceProvider; 36 | $this->localtimeOffset = $localtimeOffset; 37 | } 38 | 39 | /** 40 | * {@inheritDoc} 41 | */ 42 | public function createRequest(CredentialsInterface $credentials, $uri, $method, array $options = []) 43 | { 44 | if (empty($method) || !is_string($method)) { 45 | throw new \InvalidArgumentException('Specified method is invalid'); 46 | } elseif (!$credentials->key() || !$credentials->id() || !$credentials->algorithm()) { 47 | throw new \InvalidArgumentException('Specified credentials is invalid'); 48 | } 49 | 50 | $timestamp = isset($options['timestamp']) ? $options['timestamp'] : $this->timeProvider->createTimestamp(); 51 | if ($this->localtimeOffset) { 52 | $timestamp += $this->localtimeOffset; 53 | } 54 | 55 | list ($host, $resource, $port) = $this->parseURI($uri); 56 | 57 | $nonce = isset($options['nonce']) ? $options['nonce'] : $this->nonceProvider->createNonce(); 58 | 59 | if (isset($options['payload'])) { 60 | $payload = $options['payload']; 61 | $contentType = !empty($options['content_type']) ? $options['content_type'] : ''; 62 | $hash = $this->crypto->calculatePayloadHash($payload, $credentials->algorithm(), $contentType); 63 | } else { 64 | $payload = null; 65 | $contentType = null; 66 | $hash = null; 67 | } 68 | 69 | $ext = isset($options['ext']) ? $options['ext'] : null; 70 | $app = isset($options['app']) ? $options['app'] : null; 71 | $dlg = isset($options['dlg']) ? $options['dlg'] : null; 72 | 73 | $artifacts = new Artifacts( 74 | $method, 75 | $host, 76 | $port, 77 | $resource, 78 | $timestamp, 79 | $nonce, 80 | $ext, 81 | $payload, 82 | $contentType, 83 | $hash, 84 | $app, 85 | $dlg 86 | ); 87 | 88 | $attributes = [ 89 | 'id' => $credentials->id(), 90 | 'ts' => $artifacts->timestamp(), 91 | 'nonce' => $artifacts->nonce(), 92 | ]; 93 | 94 | if (null !== $hash) { 95 | $attributes['hash'] = $hash; 96 | } 97 | 98 | if (null !== $ext) { 99 | $attributes['ext'] = $ext; 100 | } 101 | 102 | $attributes['mac'] = $this->crypto->calculateMac('header', $credentials, $artifacts); 103 | 104 | if (null !== $app) { 105 | $attributes['app'] = $app; 106 | } 107 | 108 | if (null !== $dlg) { 109 | $attributes['dlg'] = $dlg; 110 | } 111 | 112 | return new Request(HeaderFactory::create('Authorization', $attributes), $artifacts); 113 | } 114 | 115 | /** 116 | * {@inheritDoc} 117 | */ 118 | public function authenticate( 119 | CredentialsInterface $credentials, 120 | Request $request, 121 | $headerObjectOrString, 122 | array $options = [] 123 | ) 124 | { 125 | $header = HeaderFactory::createFromHeaderObjectOrString( 126 | 'Server-Authorization', 127 | $headerObjectOrString, 128 | function () { 129 | throw new \InvalidArgumentException( 130 | 'Header must either be a string or an instance of "Dragooon\Hawk\Header\Header"' 131 | ); 132 | } 133 | ); 134 | 135 | if (isset($options['payload'])) { 136 | $payload = $options['payload']; 137 | $contentType = !empty($options['content_type']) ? $options['content_type'] : ''; 138 | } else { 139 | $payload = null; 140 | $contentType = null; 141 | } 142 | 143 | if ($ts = $header->attribute('ts')) { 144 | // @todo do something with ts 145 | } 146 | 147 | $artifacts = new Artifacts( 148 | $request->artifacts()->method(), 149 | $request->artifacts()->host(), 150 | $request->artifacts()->port(), 151 | $request->artifacts()->resource(), 152 | $request->artifacts()->timestamp(), 153 | $request->artifacts()->nonce(), 154 | $header->attribute('ext'), 155 | $payload, 156 | $contentType, 157 | $header->attribute('hash'), 158 | $request->artifacts()->app(), 159 | $request->artifacts()->dlg() 160 | ); 161 | 162 | $mac = $this->crypto->calculateMac('response', $credentials, $artifacts); 163 | if (!$this->crypto->fixedTimeComparison($mac, $header->attribute('mac'))) { 164 | return false; 165 | } 166 | 167 | if (!$payload) { 168 | return true; 169 | } 170 | 171 | if (!$artifacts->hash()) { 172 | return false; 173 | } 174 | 175 | $hash = $this->crypto->calculatePayloadHash($payload, $credentials->algorithm(), $contentType); 176 | return $this->crypto->fixedTimeComparison($hash, $artifacts->hash()); 177 | } 178 | 179 | /** 180 | * {@inheritDoc} 181 | */ 182 | public function createBewit(CredentialsInterface $credentials, $uri, $ttlSec, array $options = []) 183 | { 184 | $timestamp = isset($options['timestamp']) ? $options['timestamp'] : $this->timeProvider->createTimestamp(); 185 | if ($this->localtimeOffset) { 186 | $timestamp += $this->localtimeOffset; 187 | } 188 | 189 | list ($host, $resource, $port) = $this->parseURI($uri); 190 | 191 | $ext = isset($options['ext']) ? $options['ext'] : null; 192 | 193 | $exp = $timestamp + $ttlSec; 194 | 195 | $artifacts = new Artifacts( 196 | 'GET', 197 | $host, 198 | $port, 199 | $resource, 200 | $exp, 201 | '', 202 | $ext 203 | ); 204 | 205 | $bewit = implode('\\', [ 206 | $credentials->id(), 207 | $exp, 208 | $this->crypto->calculateMac('bewit', $credentials, $artifacts), 209 | $ext, 210 | ]); 211 | 212 | return str_replace( 213 | ['+', '/', '=', "\n"], 214 | ['-', '_', '', ''], 215 | base64_encode($bewit) 216 | ); 217 | } 218 | 219 | /** 220 | * {@inheritDoc} 221 | */ 222 | public function createMessage(CredentialsInterface $credentials, $host, $port, $message, array $options = []) 223 | { 224 | if (empty($host) || empty($port) || !is_numeric($port)) { 225 | throw new \InvalidArgumentException('Invalid host or port specified'); 226 | } elseif (!$credentials->key() || !$credentials->id() || !$credentials->algorithm()) { 227 | throw new \InvalidArgumentException('Specified credentials is invalid'); 228 | } elseif (empty($message) || !is_string($message)) { 229 | throw new \InvalidArgumentException('Specified message is not valid'); 230 | } 231 | 232 | $timestamp = isset($options['timestamp']) ? $options['timestamp'] : $this->timeProvider->createTimestamp(); 233 | if ($this->localtimeOffset) { 234 | $timestamp += $this->localtimeOffset; 235 | } 236 | 237 | $artifacts = new Artifacts( 238 | '', 239 | $host, 240 | $port, 241 | '', 242 | $timestamp, 243 | !empty($options['nonce']) ? $options['nonce'] : $this->nonceProvider->createNonce(), 244 | '', 245 | '', 246 | '', 247 | $this->crypto->calculatePayloadHash($message, $credentials->algorithm(), '') 248 | ); 249 | 250 | $result = new Message( 251 | $credentials->id(), 252 | $timestamp, 253 | $artifacts->nonce(), 254 | $artifacts->hash(), 255 | $this->crypto->calculateMac('message', $credentials, $artifacts) 256 | ); 257 | 258 | return $result; 259 | } 260 | 261 | /** 262 | * @param string $uri 263 | * @return array(host, resource, port) 264 | * 265 | * @throws \InvalidArgumentException 266 | */ 267 | protected function parseURI($uri) 268 | { 269 | $parsed = parse_url($uri); 270 | 271 | if (!$parsed || empty($parsed['host'])) { 272 | throw new \InvalidARgumentException('Specified URI is invalid'); 273 | } 274 | 275 | $host = $parsed['host']; 276 | $resource = isset($parsed['path']) ? $parsed['path'] : ''; 277 | 278 | if (isset($parsed['query'])) { 279 | $resource .= '?' . $parsed['query']; 280 | } 281 | 282 | $port = isset($parsed['port']) ? $parsed['port'] : ($parsed['scheme'] === 'https' ? 443 : 80); 283 | 284 | return [$host, $resource, $port]; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/Client/ClientBuilder.php: -------------------------------------------------------------------------------- 1 | crypto = $crypto; 25 | 26 | return $this; 27 | } 28 | 29 | /** 30 | * @param TimeProviderInterface $timeProvider 31 | * @return $this 32 | */ 33 | public function setTimeProvider(TimeProviderInterface $timeProvider) 34 | { 35 | $this->timeProvider = $timeProvider; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @param NonceProviderInterface $nonceProvider 42 | * @return $this 43 | */ 44 | public function setNonceProvider(NonceProviderInterface $nonceProvider) 45 | { 46 | $this->nonceProvider = $nonceProvider; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * @param null $localtimeOffset 53 | * @return $this 54 | */ 55 | public function setLocaltimeOffset($localtimeOffset = null) 56 | { 57 | $this->localtimeOffset = $localtimeOffset; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @return Client 64 | */ 65 | public function build() 66 | { 67 | $crypto = $this->crypto ?: new Crypto; 68 | $timeProvider = $this->timeProvider ?: DefaultTimeProviderFactory::create(); 69 | $nonceProvider = $this->nonceProvider ?: DefaultNonceProviderFactory::create(); 70 | 71 | return new Client( 72 | $crypto, 73 | $timeProvider, 74 | $nonceProvider, 75 | $this->localtimeOffset 76 | ); 77 | } 78 | 79 | /** 80 | * @return static 81 | */ 82 | public static function create() 83 | { 84 | return new static; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Client/ClientInterface.php: -------------------------------------------------------------------------------- 1 | header = $header; 20 | $this->artifacts = $artifacts; 21 | } 22 | 23 | /** 24 | * @return Header 25 | */ 26 | public function header() 27 | { 28 | return $this->header; 29 | } 30 | 31 | /** 32 | * @return Artifacts 33 | */ 34 | public function artifacts() 35 | { 36 | return $this->artifacts; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Credentials/CallbackCredentialsProvider.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 15 | } 16 | 17 | /** 18 | * {@inheritDoc} 19 | */ 20 | public function loadCredentialsById($id) 21 | { 22 | $result = call_user_func($this->callback, $id); 23 | 24 | if (empty($result)) { 25 | throw new CredentialsNotFoundException($id); 26 | } 27 | 28 | return $result; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Credentials/Credentials.php: -------------------------------------------------------------------------------- 1 | key = trim($key); 24 | $this->algorithm = trim($algorithm); 25 | $this->id = trim($id); 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function id() 32 | { 33 | return $this->id; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function key() 40 | { 41 | return $this->key; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function algorithm() 48 | { 49 | return $this->algorithm; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Credentials/CredentialsInterface.php: -------------------------------------------------------------------------------- 1 | method = $method; 50 | $this->host = $host; 51 | $this->port = $port; 52 | $this->resource = $resource; 53 | $this->timestamp = $timestamp; 54 | $this->nonce = $nonce; 55 | $this->ext = $ext; 56 | $this->payload = trim($payload) != '' ? $payload : null; 57 | $this->contentType = trim($contentType) != '' ? $contentType : null; 58 | $this->hash = trim($hash) != '' ? $hash : null; 59 | $this->app = trim($app) != '' ? $app : null; 60 | $this->dlg = trim($dlg) != '' ? $dlg : null; 61 | } 62 | 63 | /** 64 | * @return int 65 | */ 66 | public function timestamp() 67 | { 68 | return $this->timestamp; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function nonce() 75 | { 76 | return $this->nonce; 77 | } 78 | 79 | /** 80 | * @return null|string 81 | */ 82 | public function ext() 83 | { 84 | return $this->ext; 85 | } 86 | 87 | /** 88 | * @return null|string 89 | */ 90 | public function payload() 91 | { 92 | return $this->payload; 93 | } 94 | 95 | /** 96 | * @return null|string 97 | */ 98 | public function contentType() 99 | { 100 | return $this->contentType; 101 | } 102 | 103 | /** 104 | * @return null|string 105 | */ 106 | public function hash() 107 | { 108 | return $this->hash; 109 | } 110 | 111 | /** 112 | * @return null|string 113 | */ 114 | public function app() 115 | { 116 | return $this->app; 117 | } 118 | 119 | /** 120 | * @return null|string 121 | */ 122 | public function dlg() 123 | { 124 | return $this->dlg; 125 | } 126 | 127 | /** 128 | * @return string 129 | */ 130 | public function resource() 131 | { 132 | return $this->resource; 133 | } 134 | 135 | /** 136 | * @return string 137 | */ 138 | public function host() 139 | { 140 | return $this->host; 141 | } 142 | 143 | /** 144 | * @return int 145 | */ 146 | public function port() 147 | { 148 | return $this->port; 149 | } 150 | 151 | /** 152 | * @return string 153 | */ 154 | public function method() 155 | { 156 | return $this->method; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Crypto/Crypto.php: -------------------------------------------------------------------------------- 1 | generateNormalizedString($type, $attributes); 38 | 39 | return base64_encode(hash_hmac($credentials->algorithm(), $normalized, $credentials->key(), true)); 40 | } 41 | 42 | /** 43 | * @param int $ts 44 | * @param CredentialsInterface $credentials 45 | * @return string 46 | */ 47 | public function calculateTsMac($ts, CredentialsInterface $credentials) 48 | { 49 | $normalized = 'hawk.' . self::HEADER_VERSION . '.ts' . "\n" . 50 | $ts . "\n"; 51 | 52 | return base64_encode(hash_hmac( 53 | $credentials->algorithm(), 54 | $normalized, 55 | $credentials->key(), 56 | true 57 | )); 58 | } 59 | 60 | /** 61 | * @param int $a 62 | * @param int $b 63 | * @return bool 64 | */ 65 | public function fixedTimeComparison($a, $b) 66 | { 67 | $mismatch = strlen($a) === strlen($b) ? 0 : 1; 68 | if ($mismatch) { 69 | $b = $a; 70 | } 71 | 72 | for ($i = 0; $i < strlen($a); $i++) { 73 | $ac = $a[$i]; 74 | $bc = $b[$i]; 75 | $mismatch += $ac === $bc ? 0 : 1; 76 | } 77 | 78 | return (0 === $mismatch); 79 | } 80 | 81 | /** 82 | * @param string $type 83 | * @param Artifacts $attributes 84 | * @return string 85 | */ 86 | private function generateNormalizedString($type, Artifacts $attributes) 87 | { 88 | $normalized = 'hawk.' . self::HEADER_VERSION . '.' . $type . "\n" . 89 | $attributes->timestamp() . "\n" . 90 | $attributes->nonce() . "\n" . 91 | strtoupper($attributes->method()) . "\n" . 92 | $attributes->resource() . "\n" . 93 | strtolower($attributes->host()) . "\n" . 94 | $attributes->port() . "\n" . 95 | $attributes->hash() . "\n"; 96 | 97 | if ($attributes->ext()) { 98 | // TODO: escape ext 99 | $normalized .= $attributes->ext(); 100 | } 101 | 102 | $normalized .= "\n"; 103 | 104 | if ($attributes->app()) { 105 | $normalized .= $attributes->app() . "\n" . 106 | $attributes->dlg() . "\n"; 107 | } 108 | 109 | return $normalized; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Header/FieldValueParserException.php: -------------------------------------------------------------------------------- 1 | fieldName = $fieldName; 19 | $this->fieldValue = $fieldValue; 20 | $this->attributes = $attributes; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function fieldName() 27 | { 28 | return $this->fieldName; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function fieldValue() 35 | { 36 | return $this->fieldValue; 37 | } 38 | 39 | /** 40 | * @param array $keys 41 | * @return array 42 | */ 43 | public function attributes(array $keys = null) 44 | { 45 | if (null === $keys) { 46 | return $this->attributes; 47 | } 48 | 49 | $attributes = []; 50 | foreach ($keys as $key) { 51 | if (isset($this->attributes[$key])) { 52 | $attributes[$key] = $this->attributes[$key]; 53 | } 54 | } 55 | 56 | return $attributes; 57 | } 58 | 59 | /** 60 | * @param string $key 61 | * @return mixed 62 | */ 63 | public function attribute($key) 64 | { 65 | if (isset($this->attributes[$key])) { 66 | return $this->attributes[$key]; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Header/HeaderFactory.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if ($index++ > 0) { 22 | $fieldValue .= ','; 23 | } 24 | 25 | $fieldValue .= ' ' . $key . '="' . $value . '"'; 26 | } 27 | } 28 | 29 | return new Header($fieldName, $fieldValue, $attributes); 30 | } 31 | 32 | /** 33 | * @param string $fieldName 34 | * @param mixed $fieldValue 35 | * @param array $requiredKeys 36 | * @param array $validKeys 37 | * @return Header 38 | * @throws FieldValueParserException 39 | * @throws NotHawkAuthorizationException 40 | */ 41 | public static function createFromString($fieldName, $fieldValue, array $requiredKeys = null, array $validKeys = []) 42 | { 43 | return static::create( 44 | $fieldName, 45 | HeaderParser::parseFieldValue($fieldValue, $requiredKeys, $validKeys) 46 | ); 47 | } 48 | 49 | /** 50 | * @param string $fieldName 51 | * @param Header|string $headerObjectOrString 52 | * @param callback $onError 53 | * @return Header 54 | * @throws FieldValueParserException 55 | * @throws NotHawkAuthorizationException 56 | */ 57 | public static function createFromHeaderObjectOrString($fieldName, $headerObjectOrString, $onError) 58 | { 59 | if (is_string($headerObjectOrString)) { 60 | return static::createFromString($fieldName, $headerObjectOrString); 61 | } elseif ($headerObjectOrString instanceof Header) { 62 | return $headerObjectOrString; 63 | } else { 64 | call_user_func($onError); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Header/HeaderParser.php: -------------------------------------------------------------------------------- 1 | id = trim($id); 23 | $this->timestamp = trim($timestamp); 24 | $this->nonce = trim($nonce); 25 | $this->hash = trim($hash); 26 | $this->mac = trim($mac); 27 | } 28 | 29 | /** 30 | * @return mixed 31 | */ 32 | public function id() 33 | { 34 | return $this->id; 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function timestamp() 41 | { 42 | return $this->timestamp; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function nonce() 49 | { 50 | return $this->nonce; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function hash() 57 | { 58 | return $this->hash; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function mac() 65 | { 66 | return $this->mac; 67 | } 68 | 69 | /** 70 | * Generates a serialized (json_encoded) version of the message for transport 71 | * 72 | * @return string 73 | */ 74 | public function serialized() 75 | { 76 | return json_encode( 77 | [ 78 | 'id' => $this->id, 79 | 'timestamp' => $this->timestamp, 80 | 'nonce' => $this->nonce, 81 | 'hash' => $this->hash, 82 | 'mac' => $this->mac, 83 | ] 84 | ); 85 | } 86 | 87 | /** 88 | * Takes a serialized (json_encoded) string and returns it's Message object 89 | * 90 | * @param string $serialized 91 | * @return Message 92 | * @throws \InvalidArgumentException 93 | */ 94 | public static function createFromSerialized($serialized) 95 | { 96 | $unserialized = json_decode($serialized, true); 97 | if (!$unserialized || empty($unserialized['id']) || empty($unserialized['timestamp']) || empty($unserialized['nonce']) 98 | || empty($unserialized['hash']) || empty($unserialized['mac']) 99 | ) { 100 | throw new \InvalidArgumentException('Invalid serialized string passed'); 101 | } 102 | 103 | return new Message( 104 | $unserialized['id'], 105 | $unserialized['timestamp'], 106 | $unserialized['nonce'], 107 | $unserialized['hash'], 108 | $unserialized['mac'] 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Nonce/CallbackNonceValidator.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 15 | } 16 | 17 | /** 18 | * {@inheritDoc} 19 | */ 20 | public function validateNonce($key, $nonce, $timestamp) 21 | { 22 | return call_user_func_array($this->callback, [$key, $nonce, $timestamp]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nonce/DefaultNonceProviderFactory.php: -------------------------------------------------------------------------------- 1 | getLowStrengthGenerator()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nonce/NonceProvider.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 17 | } 18 | 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | public function createNonce() 23 | { 24 | return $this->generator->generateString( 25 | 32, 26 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nonce/NonceProviderInterface.php: -------------------------------------------------------------------------------- 1 | credentials = $credentials; 20 | $this->artifacts = $artifacts; 21 | } 22 | 23 | /** 24 | * @return CredentialsInterface 25 | */ 26 | public function credentials() 27 | { 28 | return $this->credentials; 29 | } 30 | 31 | /** 32 | * @return Artifacts 33 | */ 34 | public function artifacts() 35 | { 36 | return $this->artifacts; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Server/Server.php: -------------------------------------------------------------------------------- 1 | crypto = $crypto; 44 | $this->credentialsProvider = $credentialsProvider; 45 | $this->timeProvider = $timeProvider; 46 | $this->nonceValidator = $nonceValidator; 47 | $this->timestampSkewSec = $timestampSkewSec; 48 | $this->localtimeOffsetSec = $localtimeOffsetSec; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function checkRequestForHawk($headerObjectOrString) 55 | { 56 | try { 57 | HeaderFactory::createFromHeaderObjectOrString( 58 | 'Authorization', 59 | $headerObjectOrString, 60 | function() { 61 | throw new UnauthorizedException('Not a hawk request'); 62 | } 63 | ); 64 | 65 | return true; 66 | } catch (UnauthorizedException $e) { 67 | return false; 68 | } catch (NotHawkAuthorizationException $e) { 69 | return false; 70 | } 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function authenticate( 77 | $method, 78 | $host, 79 | $port, 80 | $resource, 81 | $contentType = null, 82 | $payload = null, 83 | $headerObjectOrString = null 84 | ) 85 | { 86 | if (null === $headerObjectOrString) { 87 | throw new UnauthorizedException("Missing Authorization header"); 88 | } 89 | 90 | $header = HeaderFactory::createFromHeaderObjectOrString( 91 | 'Authorization', 92 | $headerObjectOrString, 93 | function () { 94 | throw new UnauthorizedException("Invalid Authorization header"); 95 | } 96 | ); 97 | 98 | // Measure now before any other processing 99 | $now = $this->timeProvider->createTimestamp() + $this->localtimeOffsetSec; 100 | 101 | $artifacts = new Artifacts( 102 | $method, 103 | $host, 104 | $port, 105 | $resource, 106 | $header->attribute('ts'), 107 | $header->attribute('nonce'), 108 | $header->attribute('ext'), 109 | $payload, 110 | $contentType, 111 | $header->attribute('hash'), 112 | $header->attribute('app'), 113 | $header->attribute('dlg') 114 | ); 115 | 116 | foreach (['id', 'ts', 'nonce', 'mac'] as $requiredAttribute) { 117 | if (strlen($header->attribute($requiredAttribute)) == 0) { 118 | throw new UnauthorizedException('Missing attributes'); 119 | } 120 | } 121 | 122 | $credentials = $this->loadCredentialsById($header->attribute('id')); 123 | 124 | $calculatedMac = $this->crypto->calculateMac('header', $credentials, $artifacts); 125 | 126 | if (!$this->crypto->fixedTimeComparison($calculatedMac, $header->attribute('mac'))) { 127 | throw new UnauthorizedException('Bad MAC'); 128 | } 129 | 130 | if (null !== $artifacts->payload()) { 131 | if (null === $artifacts->hash()) { 132 | // Should this ever happen? Difficult to get a this far if 133 | // hash is missing as the MAC will probably be wrong anyway. 134 | throw new UnauthorizedException('Missing required payload hash'); 135 | } 136 | 137 | $calculatedHash = $this->crypto->calculatePayloadHash( 138 | $artifacts->payload(), 139 | $credentials->algorithm(), 140 | $artifacts->contentType() 141 | ); 142 | 143 | if (!$this->crypto->fixedTimeComparison($calculatedHash, $artifacts->hash())) { 144 | throw new UnauthorizedException('Bad payload hash'); 145 | } 146 | } 147 | 148 | if (!$this->nonceValidator->validateNonce($credentials->key(), $artifacts->nonce(), $artifacts->timestamp())) { 149 | throw new UnauthorizedException('Invalid nonce'); 150 | } 151 | 152 | if (abs($header->attribute('ts') - $now) > $this->timestampSkewSec) { 153 | $ts = $this->timeProvider->createTimestamp() + $this->localtimeOffsetSec; 154 | $tsm = $this->crypto->calculateTsMac($ts, $credentials); 155 | 156 | throw new UnauthorizedException('Stale timestamp', ['ts' => $ts, 'tsm' => $tsm]); 157 | } 158 | 159 | return new Response($credentials, $artifacts); 160 | } 161 | 162 | /** 163 | * {@inheritDoc} 164 | */ 165 | public function createHeader(CredentialsInterface $credentials, Artifacts $artifacts, array $options = []) 166 | { 167 | if (!$credentials->key()) { 168 | throw new \InvalidARgumentException('Invalid credentials (missing key)'); 169 | } 170 | 171 | if (isset($options['payload'])) { 172 | $payload = $options['payload']; 173 | $contentType = !empty($options['content_type']) ? $options['content_type'] : ''; 174 | $hash = $this->crypto->calculatePayloadHash($payload, $credentials->algorithm(), $contentType); 175 | } else { 176 | $payload = null; 177 | $contentType = null; 178 | $hash = null; 179 | } 180 | 181 | $ext = isset($options['ext']) ? $options['ext'] : null; 182 | 183 | $responseArtifacts = new Artifacts( 184 | $artifacts->method(), 185 | $artifacts->host(), 186 | $artifacts->port(), 187 | $artifacts->resource(), 188 | $artifacts->timestamp(), 189 | $artifacts->nonce(), 190 | $ext, 191 | $payload, 192 | $contentType, 193 | $hash, 194 | $artifacts->app(), 195 | $artifacts->dlg() 196 | ); 197 | 198 | $attributes = [ 199 | 'mac' => $this->crypto->calculateMac('response', $credentials, $responseArtifacts), 200 | ]; 201 | 202 | if ($hash !== null) { 203 | $attributes['hash'] = $hash; 204 | } 205 | 206 | if ($ext) { 207 | $attributes['ext'] = $ext; 208 | } 209 | 210 | return HeaderFactory::create('Server-Authorization', $attributes); 211 | } 212 | 213 | /** 214 | * {@inheritDoc} 215 | */ 216 | public function authenticatePayload( 217 | CredentialsInterface $credentials, 218 | $payload, 219 | $contentType, 220 | $hash 221 | ) 222 | { 223 | $calculatedHash = $this->crypto->calculatePayloadHash($payload, $credentials->algorithm(), $contentType); 224 | 225 | return $this->crypto->fixedTimeComparison($calculatedHash, $hash); 226 | } 227 | 228 | /** 229 | * {@inheritDoc} 230 | */ 231 | public function authenticateBewit($host, $port, $resource) 232 | { 233 | // Measure now before any other processing 234 | $now = $this->timeProvider->createTimestamp() + $this->localtimeOffsetSec; 235 | 236 | if (!preg_match( 237 | '/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/', 238 | $resource, 239 | $resourceParts 240 | ) 241 | ) { 242 | // TODO: Should this do something else? 243 | throw new UnauthorizedException('Malformed resource or does not contan bewit'); 244 | } 245 | 246 | $bewit = base64_decode(str_replace( 247 | ['-', '_', '', ''], 248 | ['+', '/', '=', "\n"], 249 | $resourceParts[3] 250 | )); 251 | 252 | list ($id, $exp, $mac, $ext) = explode('\\', $bewit); 253 | 254 | if ($exp < $now) { 255 | throw new UnauthorizedException('Access expired'); 256 | } 257 | 258 | $resource = $resourceParts[1]; 259 | if (isset($resourceParts[4])) { 260 | $resource .= $resourceParts[2] . $resourceParts[4]; 261 | } 262 | 263 | $artifacts = new Artifacts( 264 | 'GET', 265 | $host, 266 | $port, 267 | $resource, 268 | $exp, 269 | '', 270 | $ext 271 | ); 272 | 273 | $credentials = $this->loadCredentialsById($id); 274 | 275 | $calculatedMac = $this->crypto->calculateMac( 276 | 'bewit', 277 | $credentials, 278 | $artifacts 279 | ); 280 | 281 | if (!$this->crypto->fixedTimeComparison($calculatedMac, $mac)) { 282 | throw new UnauthorizedException('Bad MAC'); 283 | } 284 | 285 | return new Response($credentials, $artifacts); 286 | } 287 | 288 | /** 289 | * {@inheritDoc} 290 | */ 291 | public function authenticateMessage($host, $port, $message, Message $authorization) 292 | { 293 | if (!$authorization->id() || !$authorization->timestamp() || !$authorization->nonce() 294 | || !$authorization->hash() || !$authorization->mac() 295 | ) { 296 | throw new UnauthorizedException('Bad authorization'); 297 | } 298 | 299 | $credentials = $this->loadCredentialsById($authorization->id()); 300 | 301 | $artifacts = new Artifacts( 302 | null, 303 | $host, 304 | $port, 305 | null, 306 | $authorization->timestamp(), 307 | $authorization->nonce(), 308 | null, 309 | null, 310 | null, 311 | $authorization->hash() 312 | ); 313 | 314 | if (!$this->nonceValidator->validateNonce($credentials->key(), $artifacts->nonce(), $artifacts->timestamp())) { 315 | throw new UnauthorizedException('Invalid nonce'); 316 | } 317 | 318 | $calculatedMac = $this->crypto->calculateMac('message', $credentials, $artifacts); 319 | if (!$this->crypto->fixedTimeComparison($calculatedMac, $authorization->mac())) { 320 | throw new UnauthorizedException('Bad MAC'); 321 | } 322 | 323 | $calculatedHash = $this->crypto->calculatePayloadHash($message, $credentials->algorithm(), ''); 324 | if (!$this->crypto->fixedTimeComparison($calculatedHash, $authorization->hash())) { 325 | throw new UnauthorizedException('Bad payload hash'); 326 | } 327 | 328 | $now = $this->timeProvider->createTimestamp() + $this->localtimeOffsetSec; 329 | 330 | if (abs($artifacts->timestamp() - $now) > $this->timestampSkewSec) { 331 | $ts = $this->timeProvider->createTimestamp() + $this->localtimeOffsetSec; 332 | $tsm = $this->crypto->calculateTsMac($ts, $credentials); 333 | 334 | throw new UnauthorizedException('Stale timestamp', ['ts' => $ts, 'tsm' => $tsm]); 335 | } 336 | 337 | return new Response($credentials, $artifacts); 338 | } 339 | 340 | /** 341 | * Loads a credential by ID 342 | * 343 | * @param int $id 344 | * @return Credentials 345 | * @throws UnauthorizedException 346 | */ 347 | protected function loadCredentialsById($id) 348 | { 349 | try { 350 | $credentials = $this->credentialsProvider->loadCredentialsById($id); 351 | 352 | if (!$credentials->key()) { 353 | throw new UnauthorizedException('Credentials invalid'); 354 | } 355 | 356 | return $credentials; 357 | } catch (CredentialsNotFoundException $e) { 358 | throw new UnauthorizedException('Credentials not found'); 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/Server/ServerBuilder.php: -------------------------------------------------------------------------------- 1 | credentialsProvider = $credentialsProvider; 27 | } 28 | 29 | /** 30 | * @param Crypto $crypto 31 | * @return $this 32 | */ 33 | public function setCrypto(Crypto $crypto) 34 | { 35 | $this->crypto = $crypto; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @param TimeProviderInterface $timeProvider 42 | * @return $this 43 | */ 44 | public function setTimeProvider(TimeProviderInterface $timeProvider) 45 | { 46 | $this->timeProvider = $timeProvider; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * @param NonceValidatorInterface $nonceValidator 53 | * @return $this 54 | */ 55 | public function setNonceValidator(NonceValidatorInterface $nonceValidator) 56 | { 57 | $this->nonceValidator = $nonceValidator; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @param int $timestampSkewSec 64 | * @return $this 65 | */ 66 | public function setTimestampSkewSec($timestampSkewSec) 67 | { 68 | $this->timestampSkewSec = $timestampSkewSec; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @param int $localtimeOffsetSec 75 | * @return $this 76 | */ 77 | public function setLocaltimeOffsetSec($localtimeOffsetSec) 78 | { 79 | $this->localtimeOffsetSec = $localtimeOffsetSec; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return Server 86 | */ 87 | public function build() 88 | { 89 | $crypto = $this->crypto ?: new Crypto; 90 | $timeProvider = $this->timeProvider ?: DefaultTimeProviderFactory::create(); 91 | $nonceValidator = $this->nonceValidator ?: new CallbackNonceValidator( 92 | function ($nonce, $timestamp) { 93 | return true; 94 | } 95 | ); 96 | $timestampSkewSec = $this->timestampSkewSec ?: 60; 97 | $localtimeOffsetSec = $this->localtimeOffsetSec ?: 0; 98 | 99 | return new Server( 100 | $crypto, 101 | $this->credentialsProvider, 102 | $timeProvider, 103 | $nonceValidator, 104 | $timestampSkewSec, 105 | $localtimeOffsetSec 106 | ); 107 | } 108 | 109 | /** 110 | * @param CredentialsProviderInterface $credentialsProvider 111 | * @return static 112 | */ 113 | public static function create(CredentialsProviderInterface $credentialsProvider) 114 | { 115 | return new static($credentialsProvider); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Server/ServerInterface.php: -------------------------------------------------------------------------------- 1 | header) { 27 | return $this->header; 28 | } 29 | 30 | $attributes = $this->attributes; 31 | if ($this->getMessage()) { 32 | $attributes['error'] = $this->getMessage(); 33 | } 34 | 35 | return $this->header = HeaderFactory::create('WWW-Authenticate', $attributes); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Time/ConstantTimeProvider.php: -------------------------------------------------------------------------------- 1 | time = $time; 15 | } 16 | 17 | /** 18 | * {@inheritDoc} 19 | */ 20 | public function createTimestamp() 21 | { 22 | return $this->time; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Time/DefaultTimeProviderFactory.php: -------------------------------------------------------------------------------- 1 | build(); 17 | 18 | $tentTestVectorsCredentials = new Credentials( 19 | 'HX9QcbD-r3ItFEnRcAuOSg', 20 | 'sha256', 21 | 'exqbZWtykFZIh2D7cXi9dA' 22 | ); 23 | 24 | $this->assertEquals( 25 | 'ZXhxYlpXdHlrRlpJaDJEN2NYaTlkQVwxMzY4OTk2ODAwXE8wbWhwcmdvWHFGNDhEbHc1RldBV3ZWUUlwZ0dZc3FzWDc2dHBvNkt5cUk9XA', 26 | $client->createBewit( 27 | $tentTestVectorsCredentials, 28 | 'https://example.com/posts', 29 | 0, 30 | [ 31 | 'timestamp' => 1368996800, 32 | ] 33 | ) 34 | ); 35 | } 36 | 37 | /** 38 | * @test 39 | * @dataProvider headerDataProvider 40 | * 41 | * @param Credentials $credentials 42 | * @param string $url 43 | * @param string $method 44 | * @param array $options 45 | * @param mixed $expectedHeader False if the header is expected to throw an exception 46 | * @param string $message 47 | * @return void 48 | */ 49 | public function shouldTestHeader(Credentials $credentials, $url, $method, array $options, $expectedHeader, $message) 50 | { 51 | $client = ClientBuilder::create()->build(); 52 | 53 | if ($expectedHeader === false) { 54 | $this->setExpectedException('InvalidArgumentException'); 55 | } 56 | 57 | $header = $client->createRequest($credentials, $url, $method, $options)->header(); 58 | 59 | $this->assertEquals($expectedHeader, $header->fieldValue(), $message); 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function headerDataProvider() 66 | { 67 | return [ 68 | [ 69 | new Credentials('2983d45yun89q', 'sha1', 123456), 70 | 'http://example.net/somewhere/over/the/rainbow', 71 | 'POST', 72 | ['ext' => 'Bazinga!', 'timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => 'something to write about'], 73 | 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="', 74 | 'Header with sha1 hash should be equal' 75 | ], 76 | [ 77 | new Credentials('2983d45yun89q', 'sha256', 123456), 78 | 'https://example.net/somewhere/over/the/rainbow', 79 | 'POST', 80 | ['ext' => 'Bazinga!', 'timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => 'something to write about', 'content_type' => 'text/plain'], 81 | 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ext="Bazinga!", mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="', 82 | 'Header with sha256 hash should be equal' 83 | ], 84 | [ 85 | new Credentials('2983d45yun89q', 'sha256', 123456), 86 | 'https://example.net/somewhere/over/the/rainbow', 87 | 'POST', 88 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => 'something to write about', 'content_type' => 'text/plain'], 89 | 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="', 90 | 'Header with sha256 hash should be equal (with no ext)' 91 | ], 92 | [ 93 | new Credentials('2983d45yun89q', 'sha256', 123456), 94 | 'https://example.net/somewhere/over/the/rainbow', 95 | 'POST', 96 | ['ext' => null, 'timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => 'something to write about', 'content_type' => 'text/plain'], 97 | 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="', 98 | 'Header with sha256 hash should be equal (ext specified as null)' 99 | ], 100 | [ 101 | new Credentials('2983d45yun89q', 'sha256', 123456), 102 | 'https://example.net/somewhere/over/the/rainbow', 103 | 'POST', 104 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => '', 'content_type' => 'text/plain'], 105 | 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="q/t+NNAkQZNlq/aAD6PlexImwQTxwgT2MahfTa9XRLA=", mac="U5k16YEzn3UnBHKeBzsDXn067Gu3R4YaY6xOt9PYRZM="', 106 | 'Header with sha256 hash should be equal (empty payload)' 107 | ], 108 | [ 109 | new Credentials('2983d45yun89q', 'sha256', 123456), 110 | '', 111 | 'POST', 112 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => '', 'content_type' => 'text/plain'], 113 | false, 114 | 'Header should return an error (missing URI)' 115 | ], 116 | [ 117 | new Credentials('2983d45yun89q', 'sha256', 123456), 118 | 4, 119 | 'POST', 120 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => '', 'content_type' => 'text/plain'], 121 | false, 122 | 'Header should return an error (invalid URI)' 123 | ], 124 | [ 125 | new Credentials('2983d45yun89q', 'sha256', 123456), 126 | 'https://example.net/somewhere/over/the/rainbow', 127 | '', 128 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => '', 'content_type' => 'text/plain'], 129 | false, 130 | 'Header should return an error (missing method)' 131 | ], 132 | [ 133 | new Credentials('2983d45yun89q', 'sha256', 123456), 134 | 'https://example.net/somewhere/over/the/rainbow', 135 | 4, 136 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => '', 'content_type' => 'text/plain'], 137 | false, 138 | 'Header should return an error (invalid method)' 139 | ], 140 | [ 141 | new Credentials('2983d45yun89q', 'sha256'), 142 | 'https://example.net/somewhere/over/the/rainbow', 143 | 'POST', 144 | ['timestamp' => 1353809207, 'nonce' => 'Ygvqdz', 'payload' => '', 'content_type' => 'text/plain'], 145 | false, 146 | 'Header should return an error (invalid credentials)' 147 | ], 148 | ]; 149 | } 150 | 151 | /** 152 | * @test 153 | * @dataProvider messageDataProvider 154 | * 155 | * @param Credentials $credentials 156 | * @param string $host 157 | * @param int $port 158 | * @param string $message 159 | * @param array $options 160 | * @param mixed $expected 161 | * @param string $testMessage 162 | */ 163 | public function shouldTestMessage(Credentials $credentials, $host, $port, $message, array $options, $expected, $testMessage) 164 | { 165 | $client = ClientBuilder::create()->build(); 166 | 167 | if ($expected === false) { 168 | $this->setExpectedException('InvalidArgumentException'); 169 | } 170 | 171 | $message = $client->createMessage($credentials, $host, $port, $message, $options); 172 | 173 | if (!empty($expected)) { 174 | $this->assertEquals($options['timestamp'], $message->timestamp(), $testMessage); 175 | $this->assertEquals($options['nonce'], $message->nonce(), $testMessage); 176 | $this->assertEquals($expected, $message->mac(), $testMessage); 177 | } 178 | } 179 | 180 | /** 181 | * @return array 182 | */ 183 | public function messageDataProvider() 184 | { 185 | return [ 186 | [ 187 | new Credentials('2983d45yun89q', 'sha256', 123456), 188 | 'example.net', 189 | 80, 190 | 'I am the boodyman', 191 | ['timestamp' => 1353809207, 'nonce' => 'abc123'], 192 | 'fWpeQac+YUDgpFkOXiJCfHXV19FHU6uKJh2pXyKa8BQ=', 193 | 'Message authorization should be generated' 194 | ], 195 | [ 196 | new Credentials('2983d45yun89q', 'sha256'), 197 | 'example.net', 198 | 80, 199 | 'I am the boodyman', 200 | ['timestamp' => 1353809207, 'nonce' => 'abc123'], 201 | false, 202 | 'Message authorization should fail on invalid credentials' 203 | ], 204 | [ 205 | new Credentials('2983d45yun89q', 'sha256', 123456), 206 | '', 207 | 80, 208 | 'I am the boodyman', 209 | ['timestamp' => 1353809207, 'nonce' => 'abc123'], 210 | false, 211 | 'Message authorization should fail on invalid host' 212 | ], 213 | ]; 214 | } 215 | 216 | /** 217 | * @test 218 | */ 219 | public function shouldTestAuthentication() 220 | { 221 | $client = ClientBuilder::create()->build(); 222 | 223 | $credentials = new Credentials('werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 'sha256', 123456); 224 | 225 | $blankHeader = new Header('', ''); 226 | 227 | // Test for successful authentication (without payload) 228 | $artifacts = new Artifacts( 229 | 'POST', 230 | 'example.com', 231 | 8080, 232 | '/resource/4?filter=a', 233 | 1362336900, 234 | 'eb5S_L', 235 | 'some-app-data' 236 | ); 237 | $test = $client->authenticate( 238 | $credentials, 239 | new Request($blankHeader, $artifacts), 240 | 'Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"', 241 | ['content_type' => 'text/plain'] 242 | ); 243 | $this->assertTrue($test, 'Should successfully authenticate'); 244 | 245 | // Test for successful authentication (with payload) 246 | $artifacts = new Artifacts( 247 | 'POST', 248 | 'example.com', 249 | 8080, 250 | '/resource/4?filter=a', 251 | 1362336900, 252 | 'eb5S_L', 253 | 'some-app-data' 254 | ); 255 | $test = $client->authenticate( 256 | $credentials, 257 | new Request($blankHeader, $artifacts), 258 | 'Hawk mac="9lE3eaJZQof5GnxjM0eQZmtJ3M/GrqKVaX1dUI3zuO8=", hash="vcngjQGyNJQ/Q3y/voD1FNW1h1xK1D/EGCvIH86cfu0=", ext="response-specific', 259 | ['content_type' => 'text/plain', 'payload' => 'This is amazing'] 260 | ); 261 | $this->assertTrue($test, 'Should successfully authenticate'); 262 | 263 | // Test for unsuccessful authentication (with payload) 264 | $artifacts = new Artifacts( 265 | 'GET', 266 | 'example.com', 267 | 8080, 268 | '/resource/4?filter=a', 269 | 1362336900, 270 | 'eb5S_L', 271 | 'some-app-data' 272 | ); 273 | $test = $client->authenticate( 274 | $credentials, 275 | new Request($blankHeader, $artifacts), 276 | 'Hawk mac="9lE3eaJZQof5GnxjM0eQZmtJ3M/GrqKVaX1dUI3zuO8=", hash="vcngjQGyNJQ/Q3y/voD1FNW1h1xK1D/EGCvIH86cfu0=", ext="response-specific', 277 | ['content_type' => 'text/plain', 'payload' => 'This is amazing'] 278 | ); 279 | $this->assertFalse($test, 'Should not authenticate'); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /tests/Credentials/CredentialsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('HX9QcbD-r3ItFEnRcAuOSg', $credentials->key()); 19 | $this->assertEquals('sha256', $credentials->algorithm()); 20 | $this->assertEquals('exqbZWtykFZIh2D7cXi9dA', $credentials->id()); 21 | 22 | try { 23 | new Credentials( 24 | 'HX9QcbD-r3ItFEnRcAuOSg', 25 | 'example', 26 | 'exqbZWtykFZIh2D7cXi9dA' 27 | ); 28 | } catch (\InvalidArgumentException $e) { 29 | return true; 30 | } 31 | 32 | $this->fail('Credentials should throw an exception for having invalid algorithm'); 33 | } 34 | } -------------------------------------------------------------------------------- /tests/Crypto/ArtifactsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('testmethod', $artifacts->method()); 26 | $this->assertEquals('testhost', $artifacts->host()); 27 | $this->assertEquals('testport', $artifacts->port()); 28 | $this->assertEquals('testresource', $artifacts->resource()); 29 | $this->assertEquals('testtimestamp', $artifacts->timestamp()); 30 | $this->assertEquals('testnonce', $artifacts->nonce()); 31 | $this->assertEquals('testext', $artifacts->ext()); 32 | $this->assertEquals('testpayload', $artifacts->payload()); 33 | $this->assertEquals('testcontenttype', $artifacts->contentType()); 34 | $this->assertEquals('testhash', $artifacts->hash()); 35 | $this->assertEquals('testapp', $artifacts->app()); 36 | $this->assertEquals('testdlg', $artifacts->dlg()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Crypto/CryptoTest.php: -------------------------------------------------------------------------------- 1 | calculatePayloadHash( 24 | $payload, 25 | $algorithm, 26 | $contentType 27 | ); 28 | 29 | $this->assertEquals($expectedHash, $calculatedHash); 30 | } 31 | 32 | public function payloadDataProvider() 33 | { 34 | return [ 35 | [ 36 | 'neQFHgYKl/jFqDINrC21uLS0gkFglTz789rzcSr7HYU=', 37 | '{"type":"https://tent.io/types/status/v0#"}', 38 | 'sha256', 39 | 'application/vnd.tent.post.v0+json' 40 | ] 41 | ]; 42 | } 43 | 44 | /** 45 | * @test 46 | * @dataProvider macDataProvider 47 | */ 48 | public function shouldCalculateMac( 49 | $expectedMac, 50 | $type, 51 | CredentialsInterface $credentials, 52 | Artifacts $artifacts 53 | ) 54 | { 55 | $crypto = new Crypto; 56 | 57 | $calculatedMac = $crypto->calculateMac($type, $credentials, $artifacts); 58 | 59 | $this->assertEquals($expectedMac, $calculatedMac); 60 | } 61 | 62 | public function macDataProvider() 63 | { 64 | $tentTestVectorsCredentials = new Credentials( 65 | 'HX9QcbD-r3ItFEnRcAuOSg', 66 | 'sha256', 67 | 'exqbZWtykFZIh2D7cXi9dA' 68 | ); 69 | 70 | $tentTestVectorsAttributes = [ 71 | 'method' => 'POST', 72 | 'host' => 'example.com', 73 | 'port' => 443, 74 | 'resource' => '/posts', 75 | 'timestamp' => 1368996800, 76 | 'nonce' => '3yuYCD4Z', 77 | 'payload' => '{"type":"https://tent.io/types/status/v0#"}', 78 | 'content_type' => 'application/vnd.tent.post.v0+json', 79 | 'hash' => 'neQFHgYKl/jFqDINrC21uLS0gkFglTz789rzcSr7HYU=', 80 | ]; 81 | 82 | return [ 83 | [ 84 | 85 | // 86 | // App request w/hash 87 | // 88 | 89 | '2sttHCQJG9ejj1x7eCi35FP23Miu9VtlaUgwk68DTpM=', 90 | 91 | 'header', 92 | $tentTestVectorsCredentials, 93 | new Artifacts( 94 | $tentTestVectorsAttributes['method'], 95 | $tentTestVectorsAttributes['host'], 96 | $tentTestVectorsAttributes['port'], 97 | $tentTestVectorsAttributes['resource'], 98 | $tentTestVectorsAttributes['timestamp'], 99 | $tentTestVectorsAttributes['nonce'], 100 | null, 101 | $tentTestVectorsAttributes['payload'], 102 | $tentTestVectorsAttributes['content_type'], 103 | $tentTestVectorsAttributes['hash'], 104 | 'wn6yzHGe5TLaT-fvOPbAyQ' 105 | ), 106 | ], 107 | [ 108 | 109 | // 110 | // Server Response (App request w/hash) 111 | // 112 | 113 | 'lTG3kTBr33Y97Q4KQSSamu9WY/mOUKnZzq/ho9x+yxw=', 114 | 115 | 'response', 116 | $tentTestVectorsCredentials, 117 | new Artifacts( 118 | $tentTestVectorsAttributes['method'], 119 | $tentTestVectorsAttributes['host'], 120 | $tentTestVectorsAttributes['port'], 121 | $tentTestVectorsAttributes['resource'], 122 | $tentTestVectorsAttributes['timestamp'], 123 | $tentTestVectorsAttributes['nonce'], 124 | null, 125 | null, 126 | null, 127 | null, 128 | 'wn6yzHGe5TLaT-fvOPbAyQ' 129 | ), 130 | ], 131 | [ 132 | 133 | // 134 | // Relationship Request 135 | // 136 | 137 | 'OO2ldBDSw8KmNHlEdTC4BciIl8+uiuCRvCnJ9KkcR3Y=', 138 | 139 | 'header', 140 | $tentTestVectorsCredentials, 141 | new Artifacts( 142 | $tentTestVectorsAttributes['method'], 143 | $tentTestVectorsAttributes['host'], 144 | $tentTestVectorsAttributes['port'], 145 | $tentTestVectorsAttributes['resource'], 146 | $tentTestVectorsAttributes['timestamp'], 147 | $tentTestVectorsAttributes['nonce'] 148 | ), 149 | ], 150 | [ 151 | 152 | // 153 | // Server Response w/ hash (Relationship Request) 154 | // 155 | 156 | 'LvxASIZ2gop5cwE2mNervvz6WXkPmVslwm11MDgEZ5E=', 157 | 158 | 'response', 159 | $tentTestVectorsCredentials, 160 | new Artifacts( 161 | $tentTestVectorsAttributes['method'], 162 | $tentTestVectorsAttributes['host'], 163 | $tentTestVectorsAttributes['port'], 164 | $tentTestVectorsAttributes['resource'], 165 | $tentTestVectorsAttributes['timestamp'], 166 | $tentTestVectorsAttributes['nonce'], 167 | null, 168 | $tentTestVectorsAttributes['payload'], 169 | $tentTestVectorsAttributes['content_type'], 170 | $tentTestVectorsAttributes['hash'] 171 | ), 172 | ], 173 | [ 174 | 175 | // 176 | // Bewit (GET /posts) 177 | // 178 | 179 | 'O0mhprgoXqF48Dlw5FWAWvVQIpgGYsqsX76tpo6KyqI=', 180 | 181 | 'bewit', 182 | $tentTestVectorsCredentials, 183 | new Artifacts( 184 | 'GET', 185 | $tentTestVectorsAttributes['host'], 186 | $tentTestVectorsAttributes['port'], 187 | $tentTestVectorsAttributes['resource'], 188 | $tentTestVectorsAttributes['timestamp'], 189 | '' 190 | ), 191 | ], 192 | ]; 193 | } 194 | 195 | /** 196 | * @test 197 | * @dataProvider tsMacDataProvider 198 | */ 199 | public function shouldCalculateTsMac( 200 | $expectedTsMac, 201 | $ts, 202 | CredentialsInterface $credentials 203 | ) 204 | { 205 | $crypto = new Crypto; 206 | 207 | $calculatedTsMac = $crypto->calculateTsMac($ts, $credentials); 208 | 209 | $this->assertEquals($expectedTsMac, $calculatedTsMac); 210 | } 211 | 212 | public function tsMacDataProvider() 213 | { 214 | $tentTestVectorsCredentials = new Credentials( 215 | 'HX9QcbD-r3ItFEnRcAuOSg', 216 | 'sha256', 217 | 'exqbZWtykFZIh2D7cXi9dA' 218 | ); 219 | 220 | $tentTestVectorsAttributes = [ 221 | 'method' => 'POST', 222 | 'host' => 'example.com', 223 | 'port' => 443, 224 | 'resource' => '/posts', 225 | 'timestamp' => 1368996800, 226 | 'nonce' => '3yuYCD4Z', 227 | 'payload' => '{"type":"https://tent.io/types/status/v0#"}', 228 | 'content_type' => 'application/vnd.tent.post.v0+json', 229 | 'hash' => 'neQFHgYKl/jFqDINrC21uLS0gkFglTz789rzcSr7HYU=', 230 | ]; 231 | 232 | return [ 233 | [ 234 | 'HPDcD5S3Kw7LM/oyoXKcgv2Z30RnOLAI5ebXpYDGfo4=', 235 | 236 | $tentTestVectorsAttributes['timestamp'], 237 | $tentTestVectorsCredentials, 238 | ], 239 | ]; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /tests/Header/HeaderFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($header instanceof Header); 22 | $this->assertEquals(123456, $header->attribute('id')); 23 | $this->assertEquals('Ygvqdz', $header->attribute('nonce')); 24 | 25 | try { 26 | $header = 'InvalidHawkHeader'; 27 | HeaderFactory::createFromHeaderObjectOrString( 28 | 'Test', 29 | $header, 30 | function () { 31 | } 32 | ); 33 | $this->fail('Should throw an exception for invalid header'); 34 | } catch (NotHawkAuthorizationException $e) { 35 | } 36 | 37 | $header = HeaderFactory::createFromHeaderObjectOrString( 38 | 'Test', 39 | new Header('Test', 'Hawk id="123456"', ['id' => 123456]), 40 | function () { 41 | } 42 | ); 43 | $this->assertTrue($header instanceof Header); 44 | $this->assertEquals(123456, $header->attribute('id')); 45 | } 46 | 47 | 48 | /** 49 | * @test 50 | */ 51 | public function shouldTestString() 52 | { 53 | $header = 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'; 54 | $header = HeaderFactory::createFromString('Test', $header); 55 | 56 | $this->assertTrue($header instanceof Header); 57 | $this->assertEquals(123456, $header->attribute('id')); 58 | $this->assertEquals('Ygvqdz', $header->attribute('nonce')); 59 | } 60 | } -------------------------------------------------------------------------------- /tests/Header/HeaderParserTest.php: -------------------------------------------------------------------------------- 1 | fail('Should throw an exception for invalid header'); 15 | } catch (NotHawkAuthorizationException $e) { 16 | } 17 | 18 | return true; 19 | } 20 | 21 | /** 22 | * @test 23 | */ 24 | public function shouldTestFieldValueParserException() 25 | { 26 | try { 27 | $header = 'Hawk id="123456", ts="1353809207", hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'; 28 | HeaderParser::parseFieldValue($header, ['id', 'nonce', 'hash']); 29 | $this->fail('Should throw an exception for missing required values'); 30 | } catch (FieldValueParserException $e) { 31 | } 32 | 33 | return true; 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function shouldTestParseFieldValue() 40 | { 41 | $header = 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'; 42 | $header = HeaderParser::parseFieldValue($header, ['id', 'ts']); 43 | 44 | $this->assertTrue(is_array($header)); 45 | $this->assertEquals(123456, $header['id']); 46 | $this->assertEquals(1353809207, $header['ts']); 47 | $this->assertEquals('Ygvqdz', $header['nonce']); 48 | $this->assertEquals('bsvY3IfUllw6V5rvk4tStEvpBhE=', $header['hash']); 49 | $this->assertEquals('Bazinga!', $header['ext']); 50 | $this->assertEquals('qbf1ZPG/r/e06F4ht+T77LXi5vw=', $header['mac']); 51 | 52 | try { 53 | $header = 'Hawk wrong="test"'; 54 | HeaderParser::parseFieldValue($header); 55 | $this->fail('Should throw an exception for invalid key'); 56 | } catch (FieldValueParserException $e) { 57 | $this->assertEquals('Invalid key: wrong', $e->getMessage()); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /tests/Message/MessageTest.php: -------------------------------------------------------------------------------- 1 | serialized(); 14 | 15 | $this->assertEquals('{"id":"123456","timestamp":"1433581607","nonce":"abc123","hash":"hash1233","mac":"mac1234"}', $serialized, 'Should generate JSON encoded object'); 16 | 17 | $unserialized = Message::createFromSerialized($serialized); 18 | $this->assertEquals('abc123', $unserialized->nonce()); 19 | $this->assertEquals(123456, $unserialized->id()); 20 | } 21 | } -------------------------------------------------------------------------------- /tests/Server/ServerTest.php: -------------------------------------------------------------------------------- 1 | build(); 32 | $validHeader = 'Hawk id="123"'; 33 | $invalidHeader = ""; 34 | 35 | $this->assertTrue($server->checkRequestForHawk($validHeader)); 36 | $this->assertFalse($server->checkRequestForHawk($invalidHeader)); 37 | } 38 | 39 | /** 40 | * @test 41 | * @dataProvider bewitDataProvider 42 | * 43 | * @param string $host 44 | * @param int $port 45 | * @param string $resource 46 | * @param int $localTimeOffsetSec 47 | * @param string $expected 48 | * @param string $message 49 | */ 50 | public function shouldAuthenticateBewit($host, $port, $resource, $localTimeOffsetSec, $expected, $message) 51 | { 52 | $key = 'HX9QcbD-r3ItFEnRcAuOSg'; 53 | $credentialsProvider = new CallbackCredentialsProvider( 54 | function ($id) use ($key) { 55 | return new Credentials( 56 | $key, 57 | 'sha256', 58 | 'exqbZWtykFZIh2D7cXi9dA' 59 | ); 60 | } 61 | ); 62 | 63 | $server = ServerBuilder::create($credentialsProvider) 64 | ->setLocalTimeOffsetSec($localTimeOffsetSec) 65 | ->build(); 66 | 67 | try { 68 | $result = $server->authenticateBewit($host, $port, $resource); 69 | 70 | if ($expected === true) { 71 | $this->assertTrue($result instanceof Response, $message); 72 | $this->assertEquals($key, $result->credentials()->key(), $message); 73 | } else { 74 | $this->fail($message); 75 | } 76 | } catch (\Exception $e) { 77 | if (is_string($expected)) { 78 | $this->assertEquals($expected, $e->getMessage(), $message); 79 | } else { 80 | $this->fail($message); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function bewitDataProvider() 89 | { 90 | $now = DefaultTimeProviderFactory::create()->createTimestamp(); 91 | 92 | return [ 93 | [ 94 | 'example.com', 95 | 443, 96 | '/posts?bewit=ZXhxYlpXdHlrRlpJaDJEN2NYaTlkQVwxMzY4OTk2ODAwXE8wbWhwcmdvWHFGNDhEbHc1RldBV3ZWUUlwZ0dZc3FzWDc2dHBvNkt5cUk9XA', 97 | 1368996800 - $now, 98 | true, 99 | 'Should accept valid bewit request', 100 | ], 101 | [ 102 | 'example.com', 103 | 443, 104 | '/posts', 105 | 1368996800 - $now, 106 | 'Malformed resource or does not contan bewit', 107 | 'Should reject (missing bewit)', 108 | ], 109 | [ 110 | 'example.com', 111 | 80, 112 | '/posts?bewit=ZXhxYlpXdHlrRlpJaDJEN2NYaTlkQVwxMzY4OTk2ODAwXE8wbWhwcmdvWHFGNDhEbHc1RldBV3ZWUUlwZ0dZc3FzWDc2dHBvNkt5cUk9XA', 113 | 1368996800 - $now, 114 | 'Bad MAC', 115 | 'Should reject on invalid Bewit', 116 | ], 117 | ]; 118 | } 119 | 120 | /** 121 | * @test 122 | * @dataProvider headerDataProvider 123 | * 124 | * @param Credentials $credentials 125 | * @param Artifacts $artifacts 126 | * @param array $options 127 | * @param mixed $expectedHeader False if the header is expected to throw an exception 128 | * @param string $message 129 | * @return void 130 | */ 131 | public function shouldTestHeader(Credentials $credentials, Artifacts $artifacts, array $options, $expectedHeader, $message) 132 | { 133 | $server = ServerBuilder::create( 134 | new CallbackCredentialsProvider( 135 | function ($id) { 136 | // We don't need this for testing header 137 | return false; 138 | } 139 | ) 140 | )->build(); 141 | 142 | if ($expectedHeader === false) { 143 | $this->setExpectedException('InvalidArgumentException'); 144 | } 145 | 146 | $header = $server->createHeader($credentials, $artifacts, $options); 147 | 148 | $this->assertEquals($expectedHeader, $header->fieldValue(), $message); 149 | } 150 | 151 | /** 152 | * @return array 153 | */ 154 | public function headerDataProvider() 155 | { 156 | return [ 157 | [ 158 | new Credentials('werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 'sha256', 123456), 159 | new Artifacts('POST', 'example.com', 8080, '/resource/4?filter=a', 1398546787, 'xUwusx', 'some-app-data'), 160 | ['payload' => 'some reply', 'content_type' => 'text/plain', 'ext' => 'response-specific'], 161 | 'Hawk mac="n14wVJK4cOxAytPUMc5bPezQzuJGl5n7MYXhFQgEKsE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"', 162 | 'Should generate a valid header (with payload)', 163 | ], 164 | [ 165 | new Credentials('werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 'sha256', 123456), 166 | new Artifacts('POST', 'example.com', 8080, '/resource/4?filter=a', 1398546787, 'xUwusx', 'some-app-data'), 167 | ['payload' => '', 'content_type' => 'text/plain', 'ext' => 'response-specific'], 168 | 'Hawk mac="i8/kUBDx0QF+PpCtW860kkV/fa9dbwEoe/FpGUXowf0=", hash="q/t+NNAkQZNlq/aAD6PlexImwQTxwgT2MahfTa9XRLA=", ext="response-specific"', 169 | 'Should generate a valid header (without payload)', 170 | ], 171 | [ 172 | new Credentials('werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 'sha256', 123456), 173 | new Artifacts('POST', 'example.com', 8080, '/resource/4?filter=a', 1398546787, 'xUwusx', 'some-app-data'), 174 | ['payload' => 'some reply', 'content_type' => 'text/plain', 'ext' => null], 175 | 'Hawk mac="6PrybJTJs20jsgBw5eilXpcytD8kUbaIKNYXL+6g0ns=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM="', 176 | 'Should generate a valid header (without ext)', 177 | ], 178 | [ 179 | new Credentials('werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', 'sha256'), 180 | new Artifacts('POST', 'example.com', 8080, '/resource/4?filter=a', 1398546787, 'xUwusx', 'some-app-data'), 181 | ['payload' => 'some reply', 'content_type' => 'text/plain', 'ext' => null], 182 | 'Hawk mac="6PrybJTJs20jsgBw5eilXpcytD8kUbaIKNYXL+6g0ns=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM="', 183 | 'Should generate generate a header (missing credentials ID)', 184 | ], 185 | [ 186 | new Credentials(null, 'sha256', 123456), 187 | new Artifacts('POST', 'example.com', 8080, '/resource/4?filter=a', 1398546787, 'xUwusx', 'some-app-data'), 188 | ['payload' => 'some reply', 'content_type' => 'text/plain', 'ext' => null], 189 | false, 190 | 'Should throw an exception (missing credentials key)', 191 | ], 192 | ]; 193 | } 194 | 195 | /** 196 | * @test 197 | * @dataProvider authenticateDataProvider 198 | * 199 | * @param string $method 200 | * @param string $host 201 | * @param int $port 202 | * @param string $resource 203 | * @param string $contentType 204 | * @param mixed $payload 205 | * @param string $header 206 | * @param int $localTimeOffsetSec 207 | * @param mixed $expected true if a successful result is expected otherwise exception's message 208 | * @param string $message 209 | */ 210 | public function shouldTestAuthentication($method, $host, $port, $resource, $contentType, $payload, $header, 211 | $localTimeOffsetSec, $expected, $message) 212 | { 213 | $key = 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn'; 214 | 215 | $serverBuilder = ServerBuilder::create( 216 | new CallbackCredentialsProvider( 217 | function ($id) use ($key) { 218 | return new Credentials( 219 | $key, 220 | $id == 1 ? 'sha1' : 'sha256', 221 | $id 222 | ); 223 | } 224 | ) 225 | ); 226 | 227 | if (!empty($localTimeOffsetSec)) { 228 | $serverBuilder->setLocaltimeOffsetSec($localTimeOffsetSec); 229 | } 230 | 231 | $server = $serverBuilder->build(); 232 | 233 | try { 234 | $result = $server->authenticate($method, $host, $port, $resource, $contentType, $payload, $header); 235 | 236 | if ($expected === true) { 237 | $this->assertTrue($result instanceof Response, $message); 238 | $this->assertEquals($key, $result->credentials()->key(), $message); 239 | } else { 240 | $this->fail($message); 241 | } 242 | } catch (\Exception $e) { 243 | if (is_string($expected)) { 244 | $this->assertEquals($expected, $e->getMessage(), $message); 245 | } else { 246 | $this->fail($message); 247 | } 248 | } 249 | } 250 | 251 | /** 252 | * @return array 253 | */ 254 | public function authenticateDataProvider() 255 | { 256 | $timeProvider = DefaultTimeProviderFactory::create(); 257 | $now = $timeProvider->createTimestamp(); 258 | 259 | return [ 260 | [ 261 | 'GET', 262 | 'example.com', 263 | 8080, 264 | '/resource/4?filter=a', 265 | null, 266 | null, 267 | 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"', 268 | 1353788437 - $now, 269 | true, 270 | 'Should accept valid authentication request (GET with sha1)', 271 | ], 272 | [ 273 | 'GET', 274 | 'example.com', 275 | 8000, 276 | '/resource/1?b=1&a=2', 277 | null, 278 | null, 279 | 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", mac="m8r1rHbXN6NgO+KIIhjO7sFRyd78RNGVUwehe8Cp2dU=", ext="some-app-data"', 280 | 1353832234 - $now, 281 | true, 282 | 'Should accept valid authentication request (GET with sha256)', 283 | ], 284 | [ 285 | 'POST', 286 | 'example.com', 287 | 8080, 288 | '/resource/4?filter=a', 289 | null, 290 | null, 291 | 'Hawk id="123456", ts="1357926341", nonce="1AwuJD", hash="qAiXIVv+yjDATneWxZP2YCTa9aHRgQdnH9b3Wc+o3dg=", ext="some-app-data", mac="UeYcj5UoTVaAWXNvJfLVia7kU3VabxCqrccXP8sUGC4="', 292 | 1357926341 - $now, 293 | true, 294 | 'Should accept valid authentication request (POST with sha256)', 295 | ], 296 | [ 297 | 'POST', 298 | 'example.com', 299 | 8080, 300 | '/resource/4?filter=a', 301 | null, 302 | 'some message', 303 | 'Hawk id="123456", ts="1357926341", nonce="1AwuJD", hash="FF897AJ2LPnv/0ilMuEgXBWGImE+/9TuSfw1oi4Rsqk=", ext="some-app-data", mac="aMy5l6U7ePOyX0Kk2Aq3wEJhmKyWtiQqYBEgfymhKns="', 304 | 1357926341 - $now, 305 | true, 306 | 'Should accept valid authentication request (POST with payload)', 307 | ], 308 | [ 309 | 'POST', 310 | 'example.com', 311 | 8080, 312 | '/resource/4?filter=a', 313 | null, 314 | 'some message', 315 | 'Hawk id="123456", ts="1357926341", nonce="1AwuJD", hash="FF897AJ2LPnv/0ilMuEgXBWGImE+/9TuSfw1oi4Rsqk=", ext="some-app-data", mac="aMy5l6U7ePOyX0KtiQqYBEgfymhKns="', 316 | 1357926341 - $now, 317 | 'Bad MAC', 318 | 'Should reject for invalid Mac', 319 | ], 320 | [ 321 | 'POST', 322 | 'example.com', 323 | 8080, 324 | '/resource/4?filter=a', 325 | null, 326 | 'some message', 327 | 'Hawk id="123456", ts="1357926341", nonce="1AwuJD", hash="qAiXIVv+yjDATneWxZP2YCTa9aHRgQdnH9b3Wc+o3dg=", ext="some-app-data", mac="UeYcj5UoTVaAWXNvJfLVia7kU3VabxCqrccXP8sUGC4="', 328 | 1357926341 - $now, 329 | 'Bad payload hash', 330 | 'Should reject for invalid hash', 331 | ], 332 | [ 333 | 'POST', 334 | 'example.com', 335 | 8080, 336 | '/resource/4?filter=a', 337 | null, 338 | 'some message', 339 | 'Hawk id="123456", ts="1357926341", nonce="1AwuJD", ext="some-app-data", mac="XCqOLBuIUZQoNZzTtikW0v06zJhhDNGiKWNfuErWLJ4="', 340 | 1357926341 - $now, 341 | 'Missing required payload hash', 342 | 'Should reject for missing payload hash', 343 | ], 344 | [ 345 | 'POST', 346 | 'example.com', 347 | 8080, 348 | '/resource/4?filter=a', 349 | null, 350 | 'some message', 351 | 'HawkUseless header', 352 | 1357926341 - $now, 353 | 'Missing attributes', 354 | 'Should reject for invalid header', 355 | ], 356 | [ 357 | 'POST', 358 | 'example.com', 359 | 8080, 360 | '/resource/4?filter=a', 361 | null, 362 | 'some message', 363 | 'Hawk ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"', 364 | 1357926341 - $now, 365 | 'Missing attributes', 366 | 'Should reject for invalid header (missing ID)', 367 | ], 368 | [ 369 | 'POST', 370 | 'example.com', 371 | 8080, 372 | '/resource/4?filter=a', 373 | null, 374 | 'some message', 375 | 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"', 376 | 1357926341 - $now, 377 | 'Missing attributes', 378 | 'Should reject for invalid header (missing nonce)', 379 | ], 380 | [ 381 | 'POST', 382 | 'example.com', 383 | 8080, 384 | '/resource/4?filter=a', 385 | null, 386 | 'some message', 387 | 'Hawk id=" ", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"', 388 | 1357926341 - $now, 389 | 'Missing attributes', 390 | 'Should reject for invalid header (invalid ID)', 391 | ], 392 | ]; 393 | } 394 | 395 | /** 396 | * @test 397 | * 398 | * Tests for replay nonce attack 399 | */ 400 | public function shouldTestReplay() 401 | { 402 | $key = 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn'; 403 | 404 | $serverBuilder = ServerBuilder::create( 405 | new CallbackCredentialsProvider( 406 | function ($id) use ($key) { 407 | return new Credentials( 408 | $key, 409 | $id == 1 ? 'sha1' : 'sha256', 410 | $id 411 | ); 412 | } 413 | ) 414 | ); 415 | 416 | $serverBuilder->setNonceValidator( 417 | new CallbackNonceValidator( 418 | function ($key, $nonce, $timestamp) { 419 | static $memory = []; 420 | if (isset($memory[$nonce])) { 421 | throw new UnauthorizedException('Invalid nonce'); 422 | } 423 | $memory[$nonce] = $timestamp; 424 | return true; 425 | } 426 | ) 427 | ); 428 | $serverBuilder->setLocaltimeOffsetSec(1353788437 - time()); 429 | 430 | $server = $serverBuilder->build(); 431 | 432 | try { 433 | for ($i = 0; $i < 2; $i++) { 434 | $server->authenticate( 435 | 'GET', 436 | 'example.com', 437 | 8080, 438 | '/resource/4?filter=a', 439 | null, 440 | null, 441 | 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"' 442 | ); 443 | } 444 | 445 | $this->fail('Should reject duplicate nonce'); 446 | } catch (UnauthorizedException $e) { 447 | $this->assertEquals('Invalid nonce', $e->getMessage()); 448 | } 449 | } 450 | 451 | /** 452 | * @test 453 | * @dataProvider messageDataProvider 454 | * 455 | * @param string $host 456 | * @param int $port 457 | * @param string $message 458 | * @param Message $authorization 459 | * @param int $localTimeOffsetSec 460 | * @param mixed $expected 461 | * @param string $testMessage 462 | */ 463 | public function shouldTestMessage($host, $port, $message, Message $authorization, $localTimeOffsetSec, $expected, $testMessage) 464 | { 465 | $key = '2983d45yun89q'; 466 | 467 | $serverBuilder = ServerBuilder::create( 468 | new CallbackCredentialsProvider( 469 | function ($id) use ($key) { 470 | return new Credentials( 471 | $key, 472 | $id == 1 ? 'sha1' : 'sha256', 473 | $id 474 | ); 475 | } 476 | ) 477 | ); 478 | 479 | if (!empty($localTimeOffsetSec)) { 480 | $serverBuilder->setLocaltimeOffsetSec($localTimeOffsetSec); 481 | } 482 | 483 | $server = $serverBuilder->build(); 484 | 485 | try { 486 | $result = $server->authenticateMessage($host, $port, $message, $authorization); 487 | 488 | if ($expected === true) { 489 | $this->assertTrue($result instanceof Response, $testMessage); 490 | $this->assertEquals($key, $result->credentials()->key(), $testMessage); 491 | } else { 492 | $this->fail($testMessage); 493 | } 494 | } catch (\Exception $e) { 495 | if (is_string($expected)) { 496 | $this->assertEquals($expected, $e->getMessage(), $testMessage); 497 | } else { 498 | $this->fail($testMessage); 499 | } 500 | } 501 | } 502 | 503 | /** 504 | * @return array 505 | */ 506 | public function messageDataProvider() 507 | { 508 | $timeProvider = DefaultTimeProviderFactory::create(); 509 | $now = $timeProvider->createTimestamp(); 510 | 511 | return [ 512 | [ 513 | 'example.net', 514 | 80, 515 | 'I am the boodyman', 516 | new Message(123456, 1353809207, 'abc123', '8bu1yuaHAgWqdTzyqwocrHNxVvGk9qXMVL7XC5FlsMo=', 'fWpeQac+YUDgpFkOXiJCfHXV19FHU6uKJh2pXyKa8BQ='), 517 | 1353809207 - $now, 518 | true, 519 | 'Should authenticate message', 520 | ], 521 | [ 522 | 'example.net', 523 | 80, 524 | 'I am the boodyman', 525 | new Message(123456, 1353809207, 'abc123', '8bu1yuaHAgWqdTzyqwocrHNxVvGk9qXMVL7XC5FlsMo=', 'fQac+YUDgpFkOXiJCfHXV19FHU6uKJh2pXyKa8BQ='), 526 | 1353809207 - $now, 527 | 'Bad MAC', 528 | 'Should fail for invalid MAC', 529 | ], 530 | [ 531 | 'example.net', 532 | 80, 533 | 'I am the boodyman', 534 | new Message(123456, 1353809207, 'abc123', 'aaaabbb', '8pIsaWSf/s/0E0SNZnSzJ3bOKI9j5r0ehKPrdVZXJQs='), 535 | 1353809207 - $now, 536 | 'Bad payload hash', 537 | 'Should fail for invalid payload hash', 538 | ], 539 | ]; 540 | } 541 | } 542 | --------------------------------------------------------------------------------