pax_global_header00006660000000000000000000000064142342523540014516gustar00rootroot0000000000000052 comment=44957bb34aae9ab9f7b6780876f951ea1983a3b2 hawk-9.0.1/000077500000000000000000000000001423425235400124575ustar00rootroot00000000000000hawk-9.0.1/.github/000077500000000000000000000000001423425235400140175ustar00rootroot00000000000000hawk-9.0.1/.github/workflows/000077500000000000000000000000001423425235400160545ustar00rootroot00000000000000hawk-9.0.1/.github/workflows/codeql-analysis.yml000066400000000000000000000042301423425235400216660ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above branches: [main] schedule: - cron: '0 21 * * 4' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['javascript'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 hawk-9.0.1/.gitignore000077500000000000000000000001661423425235400144550ustar00rootroot00000000000000**/node_modules **/package-lock.json coverage.* **/.DS_Store **/._* **/*.pem **/.vs **/.vscode **/.idea yarn.lock hawk-9.0.1/.taskcluster.yml000066400000000000000000000016631423425235400156320ustar00rootroot00000000000000version: 1 policy: pullRequests: public tasks: $let: head_rev: $if: 'tasks_for == "github-pull-request"' then: ${event.pull_request.head.sha} else: ${event.after} repository: $if: 'tasks_for == "github-pull-request"' then: ${event.pull_request.head.repo.html_url} else: ${event.repository.html_url} in: - provisionerId: 'proj-taskcluster' workerType: 'ci' created: {$fromNow: ''} deadline: {$fromNow: '1 hour'} payload: maxRunTime: 3600 image: node:12 command: - /bin/bash - '--login' - '-c' - >- git clone ${repository} repo && cd repo && git config advice.detachedHead false && git checkout ${head_rev} && yarn && yarn test metadata: name: test description: Tests for hawk owner: nobody@mozilla.com source: https://github.com/mozilla/hawk hawk-9.0.1/API.md000066400000000000000000000706151423425235400134230ustar00rootroot00000000000000# Introduction **Hawk** is an HTTP authentication scheme providing mechanisms for making authenticated HTTP requests with partial cryptographic verification of the request and response, covering the HTTP method, request URI, host, and optionally the request payload. Similar to the HTTP [Digest access authentication schemes](http://www.ietf.org/rfc/rfc2617.txt), **Hawk** uses a set of client credentials which include an identifier (e.g. username) and key (e.g. password). Likewise, just as with the Digest scheme, the key is never included in authenticated requests. Instead, it is used to calculate a request MAC value which is included in its place. However, **Hawk** has several differences from Digest. In particular, while both use a nonce to limit the possibility of replay attacks, in **Hawk** the client generates the nonce and uses it in combination with a timestamp, leading to less "chattiness" (interaction with the server). Also unlike Digest, this scheme is not intended to protect the key itself (the password in Digest) because the client and server must both have access to the key material in the clear. The primary design goals of this scheme are to: * simplify and improve HTTP authentication for services that are unwilling or unable to deploy TLS for all resources, * secure credentials against leakage (e.g., when the client uses some form of dynamic configuration to determine where to send an authenticated request), and * avoid the exposure of credentials sent to a malicious server over an unauthenticated secure channel due to client failure to validate the server's identity as part of its TLS handshake. In addition, **Hawk** supports a method for granting third-parties temporary access to individual resources using a query parameter called _bewit_ (in falconry, a leather strap used to attach a tracking device to the leg of a hawk). The **Hawk** scheme requires the establishment of a shared symmetric key between the client and the server, which is beyond the scope of this module. Typically, the shared credentials are established via an initial TLS-protected phase or derived from some other shared confidential information available to both the client and the server. ## Replay Protection Without replay protection, an attacker can use a compromised (but otherwise valid and authenticated) request more than once, gaining access to a protected resource. To mitigate this, clients include both a nonce and a timestamp when making requests. This gives the server enough information to prevent replay attacks. The nonce is generated by the client, and is a string unique across all requests with the same timestamp and key identifier combination. The timestamp enables the server to restrict the validity period of the credentials where requests occurring afterwards are rejected. It also removes the need for the server to retain an unbounded number of nonce values for future checks. By default, **Hawk** uses a time window of 1 minute to allow for time skew between the client and server (which in practice translates to a maximum of 2 minutes as the skew can be positive or negative). Using a timestamp requires the client's clock to be in sync with the server's clock. **Hawk** requires both the client clock and the server clock to use NTP to ensure synchronization. However, given the limitations of some client types (e.g. browsers) to deploy NTP, the server provides the client with its current time (in seconds precision) in response to a bad timestamp. There is no expectation that the client will adjust its system clock to match the server (in fact, this would be a potential attack vector). Instead, the client only uses the server's time to calculate an offset used only for communications with that particular server. The protocol rewards clients with synchronized clocks by reducing the number of round trips required to authenticate the first request. ## Usage Example Server code: ```js const Http = require('http'); const Hawk = require('hawk'); // Credentials lookup function const credentialsFunc = function (id) { const credentials = { key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'Steve' }; return credentials; }; // Create HTTP server const handler = async function (req, res) { let payload, status; // Authenticate incoming request try { const { credentials, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); payload = `Hello ${credentials.user} ${artifacts.ext}`; status = 200; } catch (error) { payload = 'Shoosh!'; status = 401; } // Prepare response const headers = { 'Content-Type': 'text/plain' }; // Generate Server-Authorization response header const header = Hawk.server.header(credentials, artifacts, { payload, contentType: headers['Content-Type'] }); headers['Server-Authorization'] = header; // Send the response back res.writeHead(status, headers); res.end(payload); }; // Start server Http.createServer(handler).listen(8000, 'example.com'); ``` Client code: ```js const Request = require('request'); const Hawk = require('hawk'); // Client credentials const credentials = { id: 'dh37fgj492je', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256' } // Request options const requestOptions = { uri: 'http://example.com:8000/resource/1?b=1&a=2', method: 'GET', headers: {} }; // Generate Authorization request header const { header } = Hawk.client.header('http://example.com:8000/resource/1?b=1&a=2', 'GET', { credentials: credentials, ext: 'some-app-data' }); requestOptions.headers.Authorization = header; // Send authenticated request Request(requestOptions, function (error, response, body) { // Authenticate the server's response const isValid = Hawk.client.authenticate(response, credentials, header.artifacts, { payload: body }); // Output results console.log(`${response.statusCode}: ${body}` + (isValid ? ' (valid)' : ' (invalid)')); }); ``` ## Time Sync **Hawk** can utilize the [`@hapi/sntp`](https://github.com/outmoded/sntp) module for time sync management. By default, the local machine time is used. To automatically retrieve and synchronize the clock within the application, add `@hapi/sntp` to your application and initialize it at the top level: ```js const Sntp = require('@hapi/sntp'); const Hawk = require('hawk'); Sntp.start(); Hawk.utils.setTimeFunction(Sntp.now) ``` ## Protocol Example The client attempts to access a protected resource without authentication, sending the following HTTP request to the resource server: ``` GET /resource/1?b=1&a=2 HTTP/1.1 Host: example.com:8000 ``` The resource server returns an authentication challenge. ``` HTTP/1.1 401 Unauthorized WWW-Authenticate: Hawk ``` The client has previously obtained a set of **Hawk** credentials for accessing resources on the "`http://example.com/`" server. The **Hawk** credentials issued to the client include the following attributes: * Key identifier: `dh37fgj492je` * Key: `werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn` * Algorithm: `hmac sha256` * Hash: `6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=` The client generates the authentication header by calculating a timestamp (e.g. the number of seconds since January 1, 1970 00:00:00 GMT), generating a nonce, and constructing the normalized request string (each value followed by a newline character): ``` hawk.1.header 1353832234 j4h3g2 GET /resource/1?b=1&a=2 example.com 8000 some-app-ext-data ``` The request MAC is calculated using HMAC with the specified hash algorithm "`sha256`" and the key over the normalized request string. The result is base64-encoded to produce the request MAC: ``` 6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE= ``` The client includes the **Hawk** key identifier, timestamp, nonce, application specific data, and request MAC with the request using the HTTP `Authorization` request header field: ``` GET /resource/1?b=1&a=2 HTTP/1.1 Host: example.com:8000 Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=" ``` The server validates the request by calculating the request MAC again based on the request received and verifies the validity and scope of the **Hawk** credentials. If valid, the server responds with the requested resource. ### Payload Validation **Hawk** provides optional payload validation. When generating the authentication header, the client calculates a payload hash using the specified hash algorithm. The hash is calculated over the concatenated value of (each followed by a newline character): * `hawk.1.payload` * the content-type in lowercase, without any parameters (e.g. `application/json`) * the request payload UTF-8 encoded, prior to any HTTP content encoding (the exact representation requirements should be specified by the server for payloads other than simple single-part ascii to ensure interoperability) For example: * Payload: `Thank you for flying Hawk` * Content Type: `text/plain` * Algorithm: `sha256` * Hash: `Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=` Results in the following input to the payload hash function (newline terminated values): ``` hawk.1.payload text/plain Thank you for flying Hawk ``` Which produces the following hash value: ``` Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY= ``` The client constructs the normalized request string (newline terminated values): ``` hawk.1.header 1353832234 j4h3g2 POST /resource/1?b=1&a=2 example.com 8000 Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY= some-app-ext-data ``` Then calculates the request MAC and includes the **Hawk** key identifier, timestamp, nonce, payload hash, application specific data, and request MAC, with the request using the HTTP `Authorization` request header field: ``` POST /resource/1?b=1&a=2 HTTP/1.1 Host: example.com:8000 Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", hash="Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=", ext="some-app-ext-data", mac="aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw=" ``` It is up to the server if and when it validates the payload for any given request, based solely on its security policy and the nature of the data included. If the payload is available at the time of authentication, the server uses the hash value provided by the client to construct the normalized string and validates the MAC. If the MAC is valid, the server calculates the payload hash and compares the value with the provided payload hash in the header. In many cases, checking the MAC first is faster than calculating the payload hash. However, if the payload is not available at authentication time (e.g. too large to fit in memory, streamed elsewhere, or processed at a different stage in the application), the server may choose to defer payload validation for later by retaining the hash value provided by the client after validating the MAC. It is important to note that MAC validation does not mean the hash value provided by the client is valid, only that the value included in the header was not modified. Without calculating the payload hash on the server and comparing it to the value provided by the client, the payload may be modified by an attacker. ## Response Payload Validation **Hawk** provides partial response payload validation. The server includes the `Server-Authorization` response header which enables the client to authenticate the response and ensure it is talking to the right server. **Hawk** defines the HTTP `Server-Authorization` header as a response header using the exact same syntax as the `Authorization` request header field. The header is constructed using the same process as the client's request header. The server uses the same credentials and other artifacts provided by the client to constructs the normalized request string. The `ext` and `hash` values are replaced with new values based on the server response. The rest as identical to those used by the client. The result MAC digest is included with the optional `hash` and `ext` values: ``` Server-Authorization: Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific" ``` ## Browser Support and Considerations This library can be included in browser-based applications using Webpack or similar bundling technologies. **Hawk** relies on the _Server-Authorization_ and _WWW-Authenticate_ headers in its response to communicate with the client. Therefore, in case of CORS requests, it is important to consider sending _Access-Control-Expose-Headers_ with the value _"WWW-Authenticate, Server-Authorization"_ on each response from your server. As explained in the [specifications](http://www.w3.org/TR/cors/#access-control-expose-headers-response-header), it will indicate that these headers can safely be accessed by the client (using getResponseHeader() on the XmlHttpRequest object). Otherwise you will be met with a ["simple response header"](http://www.w3.org/TR/cors/#simple-response-header) which excludes these fields and would prevent the Hawk client from authenticating the requests.You can read more about the why and how in this [article](http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server) # Single URI Authorization There are cases in which limited and short-term access to a protected resource is granted to a third party which does not have access to the shared credentials. For example, displaying a protected image on a web page accessed by anyone. **Hawk** provides limited support for such URIs in the form of a _bewit_ - a URI query parameter appended to the request URI which contains the necessary credentials to authenticate the request. Because of the significant security risks involved in issuing such access, bewit usage is purposely limited only to GET requests and for a finite period of time. Both the client and server can issue bewit credentials, however, the server should not use the same credentials as the client to maintain clear traceability as to who issued which credentials. In order to simplify implementation, bewit credentials do not support single-use policy and can be replayed multiple times within the granted access timeframe. ## Bewit Usage Example Server code: ```js const Http = require('http'); const Hawk = require('hawk'); // Credentials lookup function const credentialsFunc = function (id) { const credentials = { key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256' }; return credentials; }; // Create HTTP server const handler = function (req, res) { try { const { credentials, attributes } = await Hawk.uri.authenticate(req, credentialsFunc); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Access granted'); } catch (err) { res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Shoosh!'); } }; Http.createServer(handler).listen(8000, 'example.com'); ``` Bewit code generation: ```js const Hawk = require('hawk'); // Client credentials const credentials = { id: 'dh37fgj492je', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256' } // Generate bewit const duration = 60 * 5; // 5 Minutes const bewit = Hawk.uri.getBewit('http://example.com:8000/resource/1?b=1&a=2', { credentials: credentials, ttlSec: duration, ext: 'some-app-data' }); const uri = 'http://example.com:8000/resource/1?b=1&a=2' + '&bewit=' + bewit; // Output URI console.log('URI: ' + uri); ``` # Security Considerations The greatest sources of security risks are usually found not in **Hawk** but in the policies and procedures surrounding its use. Implementers are strongly encouraged to assess how this module addresses their security requirements. This section includes an incomplete list of security considerations that must be reviewed and understood before deploying **Hawk** on the server. Many of the protections provided in **Hawk** depends on whether and how they are used. ### MAC Keys Transmission **Hawk** does not provide any mechanism for obtaining or transmitting the set of shared credentials required. Any mechanism used to obtain **Hawk** credentials must ensure that these transmissions are protected using transport-layer mechanisms such as TLS. ### Confidentiality of Requests While **Hawk** provides a mechanism for verifying the integrity of HTTP requests, it provides no guarantee of request confidentiality. Unless other precautions are taken, eavesdroppers will have full access to the request content. Servers should carefully consider the types of data likely to be sent as part of such requests, and employ transport-layer security mechanisms to protect sensitive resources. ### Spoofing by Counterfeit Servers **Hawk** provides limited verification of the server authenticity. When receiving a response back from the server, the server may choose to include a response `Server-Authorization` header which the client can use to verify the response. However, it is up to the server to determine when such measure is included, to up to the client to enforce that policy. A hostile party could take advantage of this by intercepting the client's requests and returning misleading or otherwise incorrect responses. Service providers should consider such attacks when developing services using this protocol, and should require transport-layer security for any requests where the authenticity of the resource server or of server responses is an issue. ### Plaintext Storage of Credentials The **Hawk** key functions the same way passwords do in traditional authentication systems. In order to compute the request MAC, the server must have access to the key in plaintext form. This is in contrast, for example, to modern operating systems, which store only a one-way hash of user credentials. If an attacker were to gain access to these keys - or worse, to the server's database of all such keys - he or she would be able to perform any action on behalf of any resource owner. Accordingly, it is critical that servers protect these keys from unauthorized access. ### Entropy of Keys Unless a transport-layer security protocol is used, eavesdroppers will have full access to authenticated requests and request MAC values, and will thus be able to mount offline brute-force attacks to recover the key used. Servers should be careful to assign keys which are long enough, and random enough, to resist such attacks for at least the length of time that the **Hawk** credentials are valid. For example, if the credentials are valid for two weeks, servers should ensure that it is not possible to mount a brute force attack that recovers the key in less than two weeks. Of course, servers are urged to err on the side of caution, and use the longest key reasonable. It is equally important that the pseudo-random number generator (PRNG) used to generate these keys be of sufficiently high quality. Many PRNG implementations generate number sequences that may appear to be random, but which nevertheless exhibit patterns or other weaknesses which make cryptanalysis or brute force attacks easier. Implementers should be careful to use cryptographically secure PRNGs to avoid these problems. ### Coverage Limitations The request MAC only covers the HTTP `Host` header and optionally the `Content-Type` header. It does not cover any other headers which can often affect how the request body is interpreted by the server. If the server behavior is influenced by the presence or value of such headers, an attacker can manipulate the request headers without being detected. Implementers should use the `ext` feature to pass application-specific information via the `Authorization` header which is protected by the request MAC. The response authentication, when performed, only covers the response payload, content-type, and the request information provided by the client in its request (method, resource, timestamp, nonce, etc.). It does not cover the HTTP status code or any other response header field (e.g. `Location`) which can affect the client's behaviour. ### Future Time Manipulation The protocol relies on a clock sync between the client and server. To accomplish this, the server informs the client of its current time when an invalid timestamp is received. If an attacker is able to manipulate this information and cause the client to use an incorrect time, it would be able to cause the client to generate authenticated requests using time in the future. Such requests will fail when sent by the client, and will not likely leave a trace on the server (given the common implementation of nonce, if at all enforced). The attacker will then be able to replay the request at the correct time without detection. The client must only use the time information provided by the server if: * it was delivered over a TLS connection and the server identity has been verified, or * the `tsm` MAC digest calculated using the same client credentials over the timestamp has been verified. ### Client Clock Poisoning When receiving a request with a bad timestamp, the server provides the client with its current time. The client must never use the time received from the server to adjust its own clock, and must only use it to calculate an offset for communicating with that particular server. ### Bewit Limitations Special care must be taken when issuing bewit credentials to third parties. Bewit credentials are valid until expiration and cannot be revoked or limited without using other means. Whatever resource they grant access to will be completely exposed to anyone with access to the bewit credentials which act as bearer credentials for that particular resource. While bewit usage is limited to GET requests only and therefore cannot be used to perform transactions or change server state, it can still be used to expose private and sensitive information. ### Host Header Forgery Hawk validates the incoming request MAC against the incoming HTTP Host header. However, unless the optional `host` and `port` options are used with `server.authenticate()`, a malicious client can mint new host names pointing to the server's IP address and use that to craft an attack by sending a valid request that's meant for another hostname than the one used by the server. Server implementors must manually verify that the host header received matches their expectation (or use the options mentioned above). # Frequently Asked Questions ### Where is the protocol specification? If you are looking for some prose explaining how all this works, **this is it**. **Hawk** is being developed as an open source project instead of a standard. In other words, the [code](https://github.com/hueniverse/hawk/tree/master/lib) is the specification. Not sure about something? Open an issue! ### Is it done? Yes. The protocol is finished. This module might change with enhancements and bug fixes but the protocol itself is locked. ### Where can I find **Hawk** implementations in other languages? **Hawk**'s only reference implementation is provided in JavaScript as a node.js module. However, it has been ported to other languages. The full list is maintained [here](https://github.com/hueniverse/hawk/issues?labels=port&state=closed). Please add an issue if you are working on another port. A cross-platform test-suite is in the works. ### Why isn't the algorithm part of the challenge or dynamically negotiated? The algorithm used is closely related to the key issued as different algorithms require different key sizes (and other requirements). While some keys can be used for multiple algorithm, the protocol is designed to closely bind the key and algorithm together as part of the issued credentials. ### Why is Host and Content-Type the only headers covered by the request MAC? It is really hard to include other headers. Headers can be changed by proxies and other intermediaries and there is no well-established way to normalize them. Many platforms change the case of header field names and values. The only straight-forward solution is to include the headers in some blob (say, base64 encoded JSON) and include that with the request, an approach taken by JWT and other such formats. However, that design violates the HTTP header boundaries, repeats information, and introduces other security issues because firewalls will not be aware of these "hidden" headers. In addition, any information repeated must be compared to the duplicated information in the header and therefore only moves the problem elsewhere. ### Why not just use HTTP Digest? Digest requires pre-negotiation to establish a nonce. This means you can't just make a request - you must first send a protocol handshake to the server. This pattern has become unacceptable for most web services, especially mobile where extra round-trip are costly. ### Why bother with all this nonce and timestamp business? **Hawk** is an attempt to find a reasonable, practical compromise between security and usability. OAuth 1.0 got timestamp and nonces halfway right but failed when it came to scalability and consistent developer experience. **Hawk** addresses it by requiring the client to sync its clock, but provides it with tools to accomplish it. In general, replay protection is a matter of application-specific threat model. It is less of an issue on a TLS-protected system where the clients are implemented using best practices and are under the control of the server. Instead of dropping replay protection, **Hawk** offers a required time window and an optional nonce verification. Together, it provides developers with the ability to decide how to enforce their security policy without impacting the client's implementation. ### What are `app` and `dlg` in the authorization header and normalized mac string? The original motivation for **Hawk** was to replace the OAuth 1.0 use cases. This included both a simple client-server mode which this module is specifically designed for, and a delegated access mode which is being developed separately in [Oz](https://github.com/hueniverse/oz). In addition to the **Hawk** use cases, Oz requires another attribute: the application id `app`. This provides binding between the credentials and the application in a way that prevents an attacker from tricking an application to use credentials issued to someone else. It also has an optional 'delegated-by' attribute `dlg` which is the application id of the application the credentials were directly issued to. The goal of these two additions is to allow Oz to utilize **Hawk** directly, but with the additional security of delegated credentials. ### What is the purpose of the static strings used in each normalized MAC input? When calculating a hash or MAC, a static prefix (tag) is added. The prefix is used to prevent MAC values from being used or reused for a purpose other than what they were created for (i.e. prevents switching MAC values between a request, response, and a bewit use cases). It also protects against exploits created after a potential change in how the protocol creates the normalized string. For example, if a future version would switch the order of nonce and timestamp, it can create an exploit opportunity for cases where the nonce is similar in format to a timestamp. ### Does **Hawk** have anything to do with OAuth? Short answer: no. **Hawk** was originally proposed as the OAuth MAC Token specification. However, the OAuth working group in its consistent incompetence failed to produce a final, usable solution to address one of the most popular use cases of OAuth 1.0 - using it to authenticate simple client-server transactions (i.e. two-legged). As you can guess, the OAuth working group is still hard at work to produce more garbage. **Hawk** provides a simple HTTP authentication scheme for making client-server requests. It does not address the OAuth use case of delegating access to a third party. If you are looking for an OAuth alternative, check out [Oz](https://github.com/hueniverse/oz). # Implementations - [Logibit Hawk in F#/.Net](https://github.com/logibit/logibit.hawk/) - [Tent Hawk in Ruby](https://github.com/tent/hawk-ruby) - [Wealdtech in Java](https://github.com/wealdtech/hawk) - [Kumar's Mohawk in Python](https://github.com/kumar303/mohawk/) - [Hiyosi in Go](https://github.com/hiyosi/hawk) # Acknowledgements **Hawk** is a derivative work of the [HTTP MAC Authentication Scheme](http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05) proposal co-authored by Ben Adida, Adam Barth, and Eran Hammer, which in turn was based on the OAuth 1.0 community specification. Special thanks to Ben Laurie for his always insightful feedback and advice. The **Hawk** logo was created by [Chris Carrasco](http://chriscarrasco.com). hawk-9.0.1/CODE_OF_CONDUCT.md000066400000000000000000000012631423425235400152600ustar00rootroot00000000000000# Community Participation Guidelines This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. hawk-9.0.1/CONTRIBUTING.md000066400000000000000000000004731423425235400147140ustar00rootroot00000000000000# How to Contribute We welcome pull requests from everyone. We do expect everyone to adhere to the [Mozilla Community Participation Guidelines][participation]. Note that this library is in "maintenance mode" -- we are not adding new features, although we will accept low-impact bug fixes and dependency upgrades. hawk-9.0.1/LICENSE.md000077500000000000000000000026751423425235400141000ustar00rootroot00000000000000Copyright (c) 2012-2020, Sideway Inc, and project contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. hawk-9.0.1/README.md000077500000000000000000000015301423425235400137400ustar00rootroot00000000000000# hawk #### HTTP Holder-Of-Key Authentication Scheme. Documentation of the protocol, and the JS API, is in https://github.com/mozilla/hawk/blob/main/API.md. ## Ownership Changes This was once `hueniverse/hawk` and relased as `hawk`. Then, after the 7.0.10 release, it was moved to the `hapijs/hawk` repository and released as `@hapi/hawk`. Hapi later de-supported the library, after releasing version 8.0.0. It has since been moved to `mozilla/hawk` and is again released as `hawk`. All of the intermediate versions are also relased as `hawk`. Changes are represented in GitHub releases on this repository. Mozilla maintains several Hawk implementations in different langauages, so it is likely to stay at Mozilla for some time. This library is in "maintenance mode" -- no features will be added, and only security-related bugfixes will be applied. hawk-9.0.1/SECURITY.md000066400000000000000000000003221423425235400142450ustar00rootroot00000000000000# Security Policy Mozilla has a [well-defined process for handling security vulnerabilities](https://www.mozilla.org/en-US/about/governance/policies/security-group/bugs/) based around responsible disclosure. hawk-9.0.1/lib/000077500000000000000000000000001423425235400132255ustar00rootroot00000000000000hawk-9.0.1/lib/client.js000077500000000000000000000253661423425235400150600ustar00rootroot00000000000000'use strict'; const Url = require('url'); const B64 = require('@hapi/b64'); const Boom = require('@hapi/boom'); const Cryptiles = require('@hapi/cryptiles'); const Hoek = require('@hapi/hoek'); const Crypto = require('./crypto'); const Utils = require('./utils'); const internals = {}; // Generate an Authorization header for a given request /* uri: 'http://example.com/resource?a=b' or object from Url.parse() method: HTTP verb (e.g. 'GET', 'POST') options: { // Required credentials: { id: 'dh37fgj492je', key: 'aoijedoaijsdlaksjdl', algorithm: 'sha256' // 'sha1', 'sha256' }, // Optional ext: 'application-specific', // Application specific data sent via the ext attribute timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds nonce: '2334f34f', // A pre-generated nonce localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) contentType: 'application/json', // Payload content-type (ignored if hash provided) hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash app: '24s23423f34dx', // Oz application id dlg: '234sz34tww3sd' // Oz delegated-by application id } */ exports.header = function (uri, method, options) { // Validate inputs if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') || !method || typeof method !== 'string' || !options || typeof options !== 'object') { throw new Boom.Boom('Invalid argument type'); } // Application time const timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); // Validate credentials const credentials = options.credentials; if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials'); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm'); } // Parse URI if (typeof uri === 'string') { uri = Url.parse(uri); } // Calculate signature const artifacts = { ts: timestamp, nonce: options.nonce || Cryptiles.randomString(6), method, resource: uri.pathname + (uri.search || ''), // Maintain trailing '?' host: uri.hostname, port: uri.port || (uri.protocol === 'http:' ? 80 : 443), hash: options.hash, ext: options.ext, app: options.app, dlg: options.dlg }; // Calculate payload hash if (!artifacts.hash && (options.payload || options.payload === '')) { artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); } const mac = Crypto.calculateMac('header', credentials, artifacts); // Construct header const hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed let header = 'Hawk id="' + credentials.id + '", ts="' + artifacts.ts + '", nonce="' + artifacts.nonce + (artifacts.hash ? '", hash="' + artifacts.hash : '') + (hasExt ? '", ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) : '') + '", mac="' + mac + '"'; if (artifacts.app) { header = header + ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'; } return { header, artifacts }; }; // Validate server response /* res: node's response object artifacts: object received from header().artifacts options: { payload: optional payload received required: specifies if a Server-Authorization header is required. Defaults to 'false' } */ exports.authenticate = function (res, credentials, artifacts, options) { options = options || {}; artifacts = Hoek.clone(artifacts); const result = { headers: {} }; if (res.headers['www-authenticate']) { // Parse HTTP WWW-Authenticate header try { var wwwAttributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']); } catch (err) { throw new Boom.Boom('Invalid WWW-Authenticate header'); } result.headers['www-authenticate'] = wwwAttributes; // Validate server timestamp (not used to update clock since it is done via the SNPT client) if (wwwAttributes.ts) { const tsm = Crypto.calculateTsMac(wwwAttributes.ts, credentials); if (tsm !== wwwAttributes.tsm) { throw new Boom.Boom('Invalid server timestamp hash', { decorate: result }); } } } // Parse HTTP Server-Authorization header if (!res.headers['server-authorization']) { if (options.required) { throw new Boom.Boom('Missing Server-Authorization header', { decorate: result }); } return result; } try { var serverAuthAttributes = Utils.parseAuthorizationHeader(res.headers['server-authorization'], ['mac', 'ext', 'hash']); } catch (err) { throw new Boom.Boom('Invalid Server-Authorization header', { decorate: result }); } result.headers['server-authorization'] = serverAuthAttributes; artifacts.ext = serverAuthAttributes.ext; artifacts.hash = serverAuthAttributes.hash; const mac = Crypto.calculateMac('response', credentials, artifacts); if (mac !== serverAuthAttributes.mac) { throw new Boom.Boom('Bad response mac', { decorate: result }); } if (options.payload === null || options.payload === undefined) { return result; } if (!serverAuthAttributes.hash) { throw new Boom.Boom('Missing response hash attribute', { decorate: result }); } const calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']); if (calculatedHash !== serverAuthAttributes.hash) { throw new Boom.Boom('Bad response payload mac', { decorate: result }); } return result; }; // Generate a bewit value for a given URI /* uri: 'http://example.com/resource?a=b' or object from Url.parse() options: { // Required credentials: { id: 'dh37fgj492je', key: 'aoijedoaijsdlaksjdl', algorithm: 'sha256' // 'sha1', 'sha256' }, ttlSec: 60 * 60, // TTL in seconds // Optional ext: 'application-specific', // Application specific data sent via the ext attribute localtimeOffsetMsec: 400 // Time offset to sync with server time }; */ exports.getBewit = function (uri, options) { // Validate inputs if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') || !options || typeof options !== 'object' || !options.ttlSec) { throw new Boom.Boom('Invalid inputs'); } const ext = (options.ext === null || options.ext === undefined ? '' : options.ext); // Zero is valid value // Application time const now = Utils.now(options.localtimeOffsetMsec); // Validate credentials const credentials = options.credentials; if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials'); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm'); } // Parse URI if (typeof uri === 'string') { uri = Url.parse(uri); } // Calculate signature const exp = Math.floor(now / 1000) + options.ttlSec; const mac = Crypto.calculateMac('bewit', credentials, { ts: exp, nonce: '', method: 'GET', resource: uri.pathname + (uri.search || ''), // Maintain trailing '?' host: uri.hostname, port: uri.port || (uri.protocol === 'http:' ? 80 : 443), ext }); // Construct bewit: id\exp\mac\ext const bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + ext; return B64.base64urlEncode(bewit); }; // Generate an authorization string for a message /* host: 'example.com', port: 8000, message: '{"some":"payload"}', // UTF-8 encoded string for body hash generation options: { // Required credentials: { id: 'dh37fgj492je', key: 'aoijedoaijsdlaksjdl', algorithm: 'sha256' // 'sha1', 'sha256' }, // Optional timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds nonce: '2334f34f', // A pre-generated nonce localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) } */ exports.message = function (host, port, message, options) { options = options || {}; // Validate inputs if (!host || typeof host !== 'string' || !port || typeof port !== 'number' || message === null || message === undefined || typeof message !== 'string' || typeof options !== 'object') { throw new Boom.Boom('Invalid inputs'); } // Application time const timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); // Validate credentials const credentials = options.credentials; if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials'); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm'); } // Calculate signature const artifacts = { ts: timestamp, nonce: options.nonce || Cryptiles.randomString(6), host, port, hash: Crypto.calculatePayloadHash(message, credentials.algorithm) }; // Construct authorization const result = { id: credentials.id, ts: artifacts.ts, nonce: artifacts.nonce, hash: artifacts.hash, mac: Crypto.calculateMac('message', credentials, artifacts) }; return result; }; hawk-9.0.1/lib/crypto.js000077500000000000000000000066641423425235400151220ustar00rootroot00000000000000'use strict'; const Crypto = require('crypto'); const Url = require('url'); const Utils = require('./utils'); const internals = {}; // MAC normalization format version exports.headerVersion = '1'; // Prevent comparison of mac values generated with different normalized string formats // Supported HMAC algorithms exports.algorithms = ['sha1', 'sha256']; // Calculate the request MAC /* type: 'header', // 'header', 'bewit', 'response' credentials: { key: 'aoijedoaijsdlaksjdl', algorithm: 'sha256' // 'sha1', 'sha256' }, options: { method: 'GET', resource: '/resource?a=1&b=2', host: 'example.com', port: 8080, ts: 1357718381034, nonce: 'd3d345f', hash: 'U4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=', ext: 'app-specific-data', app: 'hf48hd83qwkj', // Application id (Oz) dlg: 'd8djwekds9cj' // Delegated by application id (Oz), requires options.app } */ exports.calculateMac = function (type, credentials, options) { const normalized = exports.generateNormalizedString(type, options); const hmac = Crypto.createHmac(credentials.algorithm, credentials.key).update(normalized); const digest = hmac.digest('base64'); return digest; }; exports.generateNormalizedString = function (type, options) { let resource = options.resource || ''; if (resource && resource[0] !== '/') { const url = Url.parse(resource, false); resource = url.path; // Includes query } let normalized = 'hawk.' + exports.headerVersion + '.' + type + '\n' + options.ts + '\n' + options.nonce + '\n' + (options.method || '').toUpperCase() + '\n' + resource + '\n' + options.host.toLowerCase() + '\n' + options.port + '\n' + (options.hash || '') + '\n'; if (options.ext) { normalized = normalized + options.ext.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); } normalized = normalized + '\n'; if (options.app) { normalized = normalized + options.app + '\n' + (options.dlg || '') + '\n'; } return normalized; }; exports.calculatePayloadHash = function (payload, algorithm, contentType) { const hash = exports.initializePayloadHash(algorithm, contentType); hash.update(payload || ''); return exports.finalizePayloadHash(hash); }; exports.initializePayloadHash = function (algorithm, contentType) { const hash = Crypto.createHash(algorithm); hash.update('hawk.' + exports.headerVersion + '.payload\n'); hash.update(Utils.parseContentType(contentType) + '\n'); return hash; }; exports.finalizePayloadHash = function (hash) { hash.update('\n'); return hash.digest('base64'); }; exports.calculateTsMac = function (ts, credentials) { const hmac = Crypto.createHmac(credentials.algorithm, credentials.key); hmac.update('hawk.' + exports.headerVersion + '.ts\n' + ts + '\n'); return hmac.digest('base64'); }; exports.timestampMessage = function (credentials, localtimeOffsetMsec) { const now = Utils.nowSecs(localtimeOffsetMsec); const tsm = exports.calculateTsMac(now, credentials); return { ts: now, tsm }; }; hawk-9.0.1/lib/index.js000077500000000000000000000004271423425235400147000ustar00rootroot00000000000000'use strict'; exports.server = require('./server'); exports.client = require('./client'); exports.crypto = require('./crypto'); exports.utils = require('./utils'); exports.uri = { authenticate: exports.server.authenticateBewit, getBewit: exports.client.getBewit }; hawk-9.0.1/lib/server.js000077500000000000000000000411121423425235400150730ustar00rootroot00000000000000'use strict'; const B64 = require('@hapi/b64'); const Boom = require('@hapi/boom'); const Cryptiles = require('@hapi/cryptiles'); const Hoek = require('@hapi/hoek'); const Crypto = require('./crypto'); const Utils = require('./utils'); const internals = {}; // Hawk authentication /* req: node's HTTP request object or an object as follows: const request = { method: 'GET', url: '/resource/4?a=1&b=2', host: 'example.com', port: 8080, authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="' }; credentialsFunc: required function to lookup the set of Hawk credentials based on the provided credentials id. The credentials include the MAC key, MAC algorithm, and other attributes (such as username) needed by the application. This function is the equivalent of verifying the username and password in Basic authentication. const credentialsFunc = async function (id) { // Lookup credentials in database const item = await db.lookup(id); // Can throw errors if (!item) { return null; } const credentials = { // Required key: item.key, algorithm: item.algorithm, // Application specific user: item.user }; return credentials; }; options: { hostHeaderName: optional header field name, used to override the default 'Host' header when used behind a cache of a proxy. Apache2 changes the value of the 'Host' header while preserving the original (which is what the module must verify) in the 'x-forwarded-host' header field. Only used when passed a node Http.ServerRequest object. nonceFunc: optional nonce validation function. The function signature is `async function(key, nonce, ts)` and it must return no value for success or throw an error for invalid state. timestampSkewSec: optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds. Provides a +/- skew which means actual allowed window is double the number of seconds. localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative). Defaults to 0. payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash' header attribute. The server always ensures the value provided has been included in the request MAC. When this option is provided, it validates the hash value itself. Validation is done by calculating a hash value over the entire payload (assuming it has already be normalized to the same format and encoding used by the client to calculate the hash on request). If the payload is not available at the time of authentication, the authenticatePayload() method can be used by passing it the credentials and attributes.hash returned from authenticate(). host: optional host name override. Only used when passed a node request object. port: optional port override. Only used when passed a node request object. } Return value: { credentials, artifacts } or throws an error. */ exports.authenticate = async function (req, credentialsFunc, options) { options = options || {}; // Default options options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds // Application time const now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing // Convert node Http request object to a request configuration object const request = Utils.parseRequest(req, options); // Parse HTTP Authorization header const attributes = Utils.parseAuthorizationHeader(request.authorization); // Construct artifacts container const artifacts = { method: request.method, host: request.host, port: request.port, resource: request.url, ts: attributes.ts, nonce: attributes.nonce, hash: attributes.hash, ext: attributes.ext, app: attributes.app, dlg: attributes.dlg, mac: attributes.mac, id: attributes.id }; // Verify required header attributes if (!attributes.id || !attributes.ts || !attributes.nonce || !attributes.mac) { throw Boom.badRequest('Missing attributes', { decorate: { artifacts } }); } // Fetch Hawk credentials try { var credentials = await credentialsFunc(attributes.id); } catch (err) { throw Boom.boomify(err, { decorate: { artifacts } }); } if (!credentials) { throw Object.assign(Utils.unauthorized('Unknown credentials'), { artifacts }); } const result = { credentials, artifacts }; if (!credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials', { decorate: result }); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm', { decorate: result }); } // Calculate MAC const mac = Crypto.calculateMac('header', credentials, artifacts); if (!Cryptiles.fixedTimeComparison(mac, attributes.mac)) { throw Object.assign(Utils.unauthorized('Bad mac'), result); } // Check payload hash if (options.payload || options.payload === '') { if (!attributes.hash) { throw Object.assign(Utils.unauthorized('Missing required payload hash'), result); } const hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType); if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) { throw Object.assign(Utils.unauthorized('Bad payload hash'), result); } } // Check nonce if (options.nonceFunc) { try { await options.nonceFunc(credentials.key, attributes.nonce, attributes.ts); } catch (err) { throw Object.assign(Utils.unauthorized('Invalid nonce'), result); } } // Check timestamp staleness if (Math.abs((attributes.ts * 1000) - now) > (options.timestampSkewSec * 1000)) { const tsm = Crypto.timestampMessage(credentials, options.localtimeOffsetMsec); throw Object.assign(Utils.unauthorized('Stale timestamp', tsm), result); } // Successful authentication return result; }; // Authenticate payload hash - used when payload cannot be provided during authenticate() /* payload: raw request payload credentials: from authenticate() artifacts: from authenticate() contentType: req.headers['content-type'] Return value: void or throws an error. */ exports.authenticatePayload = function (payload, credentials, artifacts, contentType) { const calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType); if (!Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash)) { throw Object.assign(Utils.unauthorized('Bad payload hash'), { credentials, artifacts }); } }; // Authenticate payload hash - used when payload cannot be provided during authenticate() /* calculatedHash: the payload hash calculated using Crypto.calculatePayloadHash() artifacts: from authenticate() Return value: void or throws an error. */ exports.authenticatePayloadHash = function (calculatedHash, artifacts) { if (!Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash)) { throw Object.assign(Utils.unauthorized('Bad payload hash'), { artifacts }); } }; // Generate a Server-Authorization header for a given response /* credentials: {}, // Object received from authenticate() artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored options: { ext: 'application-specific', // Application specific data sent via the ext attribute payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) contentType: 'application/json', // Payload content-type (ignored if hash provided) hash: 'U4MKKSmiVxk37JCCrAVIjV=' // Pre-calculated payload hash } */ exports.header = function (credentials, artifacts, options) { options = options || {}; // Prepare inputs if (!artifacts || typeof artifacts !== 'object' || typeof options !== 'object') { throw new Boom.Boom('Invalid inputs'); } artifacts = Hoek.clone(artifacts); delete artifacts.mac; artifacts.hash = options.hash; artifacts.ext = options.ext; // Validate credentials if (!credentials || !credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials'); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm'); } // Calculate payload hash if (!artifacts.hash && (options.payload || options.payload === '')) { artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); } const mac = Crypto.calculateMac('response', credentials, artifacts); // Construct header let header = 'Hawk mac="' + mac + '"' + (artifacts.hash ? ', hash="' + artifacts.hash + '"' : ''); if (artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== '') { // Other falsey values allowed header = header + ', ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) + '"'; } return header; }; /* * Arguments and options are the same as authenticate() with the exception that the only supported options are: * 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port' */ // 1 2 3 4 internals.bewitRegex = /^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/; exports.authenticateBewit = async function (req, credentialsFunc, options) { options = options || {}; // Application time const now = Utils.now(options.localtimeOffsetMsec); // Convert node Http request object to a request configuration object const request = Utils.parseRequest(req, options); // Extract bewit if (request.url.length > Utils.limits.maxMatchLength) { throw Boom.badRequest('Resource path exceeds max length'); } const resource = request.url.match(internals.bewitRegex); if (!resource) { throw Utils.unauthorized(); } // Bewit not empty if (!resource[3]) { throw Utils.unauthorized('Empty bewit'); } // Verify method is GET if (request.method !== 'GET' && request.method !== 'HEAD') { throw Utils.unauthorized('Invalid method'); } // No other authentication if (request.authorization) { throw Boom.badRequest('Multiple authentications'); } // Parse bewit try { var bewitString = B64.base64urlDecode(resource[3]); } catch (err) { throw Boom.badRequest('Invalid bewit encoding'); } // Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character) const bewitParts = bewitString.split('\\'); if (bewitParts.length !== 4) { throw Boom.badRequest('Invalid bewit structure'); } const bewit = { id: bewitParts[0], exp: parseInt(bewitParts[1], 10), mac: bewitParts[2], ext: bewitParts[3] || '' }; if (!bewit.id || !bewit.exp || !bewit.mac) { throw Boom.badRequest('Missing bewit attributes'); } // Construct URL without bewit let url = resource[1]; if (resource[4]) { url = url + resource[2] + resource[4]; } // Check expiration if (bewit.exp * 1000 <= now) { throw Object.assign(Utils.unauthorized('Access expired'), { bewit }); } // Fetch Hawk credentials try { var credentials = await credentialsFunc(bewit.id); } catch (err) { throw new Boom.Boom(err, { decorate: { bewit } }); } if (!credentials) { throw Object.assign(Utils.unauthorized('Unknown credentials'), { bewit }); } const result = { credentials, attributes: bewit }; if (!credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials', { decorate: result }); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm', { decorate: result }); } // Calculate MAC const mac = Crypto.calculateMac('bewit', credentials, { ts: bewit.exp, nonce: '', method: 'GET', resource: url, host: request.host, port: request.port, ext: bewit.ext }); if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) { throw Object.assign(Utils.unauthorized('Bad mac'), result); } // Successful authentication return result; }; /* * options are the same as authenticate() with the exception that the only supported options are: * 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec' */ exports.authenticateMessage = async function (host, port, message, authorization, credentialsFunc, options) { options = options || {}; // Default options options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds // Application time const now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing // Validate authorization if (!authorization.id || !authorization.ts || !authorization.nonce || !authorization.hash || !authorization.mac) { throw Boom.badRequest('Invalid authorization'); } // Fetch Hawk credentials const credentials = await credentialsFunc(authorization.id); if (!credentials) { throw Utils.unauthorized('Unknown credentials'); } const result = { credentials }; if (!credentials.key || !credentials.algorithm) { throw new Boom.Boom('Invalid credentials', { decorate: result }); } if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { throw new Boom.Boom('Unknown algorithm', { decorate: result }); } // Construct artifacts container const artifacts = { ts: authorization.ts, nonce: authorization.nonce, host, port, hash: authorization.hash }; // Calculate MAC const mac = Crypto.calculateMac('message', credentials, artifacts); if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) { throw Object.assign(Utils.unauthorized('Bad mac'), result); } // Check payload hash const hash = Crypto.calculatePayloadHash(message, credentials.algorithm); if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) { throw Object.assign(Utils.unauthorized('Bad message hash'), result); } // Check nonce if (options.nonceFunc) { try { await options.nonceFunc(credentials.key, authorization.nonce, authorization.ts); } catch (err) { throw Object.assign(Utils.unauthorized('Invalid nonce'), result); } } // Check timestamp staleness if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) { throw Object.assign(Utils.unauthorized('Stale timestamp'), result); } // Successful authentication return result; }; hawk-9.0.1/lib/utils.js000077500000000000000000000105741423425235400147350ustar00rootroot00000000000000'use strict'; const Boom = require('@hapi/boom'); const internals = {}; exports.version = function () { return require('../package.json').version; }; exports.limits = { maxMatchLength: 4096 // Limit the length of uris and headers to avoid a DoS attack on string matching }; // Extract host and port from request // $1 $2 internals.hostHeaderRegex = /^(?:(?:\r\n)?\s)*((?:[^:]+)|(?:\[[^\]]+\]))(?::(\d+))?(?:(?:\r\n)?\s)*$/; // (IPv4, hostname)|(IPv6) exports.parseHost = function (req, hostHeaderName) { hostHeaderName = (hostHeaderName ? hostHeaderName.toLowerCase() : 'host'); const hostHeader = req.headers[hostHeaderName]; if (!hostHeader) { return null; } if (hostHeader.length > exports.limits.maxMatchLength) { return null; } const hostParts = hostHeader.match(internals.hostHeaderRegex); if (!hostParts) { return null; } return { name: hostParts[1], port: (hostParts[2] ? hostParts[2] : (req.connection && req.connection.encrypted ? 443 : 80)) }; }; // Parse Content-Type header content exports.parseContentType = function (header) { if (!header) { return ''; } return header.split(';')[0].trim().toLowerCase(); }; // Convert node's to request configuration object exports.parseRequest = function (req, options) { if (!req.headers) { return req; } // Obtain host and port information let host; if (!options.host || !options.port) { host = exports.parseHost(req, options.hostHeaderName); if (!host) { throw Boom.badRequest('Invalid Host header'); } } const request = { method: req.method, url: req.url, host: options.host || host.name, port: options.port || host.port, authorization: req.headers.authorization, contentType: req.headers['content-type'] || '' }; return request; }; let _now = Date.now; // override the `now` function, e.g., to use sntp exports.setTimeFunction = function (fn) { _now = fn; }; exports.now = function (localtimeOffsetMsec) { return _now() + (localtimeOffsetMsec || 0); }; exports.nowSecs = function (localtimeOffsetMsec) { return Math.floor(exports.now(localtimeOffsetMsec) / 1000); }; internals.authHeaderRegex = /^(\w+)(?:\s+(.*))?$/; // Header: scheme[ something] internals.attributeRegex = /^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/; // !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 // Parse Hawk HTTP Authorization header exports.parseAuthorizationHeader = function (header, keys) { keys = keys || ['id', 'ts', 'nonce', 'hash', 'ext', 'mac', 'app', 'dlg']; if (!header) { throw Boom.unauthorized(null, 'Hawk'); } if (header.length > exports.limits.maxMatchLength) { throw Boom.badRequest('Header length too long'); } const headerParts = header.match(internals.authHeaderRegex); if (!headerParts) { throw Boom.badRequest('Invalid header syntax'); } const scheme = headerParts[1]; if (scheme.toLowerCase() !== 'hawk') { throw Boom.unauthorized(null, 'Hawk'); } const attributesString = headerParts[2]; if (!attributesString) { throw Boom.badRequest('Invalid header syntax'); } const attributes = {}; let errorMessage = ''; const verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, ($0, $1, $2) => { // Check valid attribute names if (keys.indexOf($1) === -1) { errorMessage = 'Unknown attribute: ' + $1; return; } // Allowed attribute value characters if ($2.match(internals.attributeRegex) === null) { errorMessage = 'Bad attribute value: ' + $1; return; } // Check for duplicates if (attributes.hasOwnProperty($1)) { errorMessage = 'Duplicate attribute: ' + $1; return; } attributes[$1] = $2; return ''; }); if (verify !== '') { throw Boom.badRequest(errorMessage || 'Bad header format'); } return attributes; }; exports.unauthorized = function (message, attributes) { return Boom.unauthorized(message || null, 'Hawk', attributes); }; hawk-9.0.1/package.json000077500000000000000000000012151423425235400147470ustar00rootroot00000000000000{ "name": "hawk", "description": "HTTP Hawk Authentication Scheme", "version": "9.0.1", "repository": "git://github.com/mozilla/hawk", "main": "lib/index.js", "files": [ "lib" ], "keywords": [ "http", "authentication", "scheme", "hawk" ], "dependencies": { "@hapi/b64": "5.x.x", "@hapi/boom": "9.x.x", "@hapi/cryptiles": "5.x.x", "@hapi/hoek": "9.x.x" }, "devDependencies": { "@hapi/code": "8.x.x", "@hapi/lab": "22.x.x" }, "scripts": { "test": "lab -a @hapi/code -t 100 -L", "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" }, "license": "BSD-3-Clause" } hawk-9.0.1/test/000077500000000000000000000000001423425235400134365ustar00rootroot00000000000000hawk-9.0.1/test/client.js000077500000000000000000000477301423425235400152700ustar00rootroot00000000000000'use strict'; const Code = require('@hapi/code'); const Hawk = require('..'); const Lab = require('@hapi/lab'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Client', () => { describe('header()', () => { it('returns a valid authorization header (sha1)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; const { header } = Hawk.client.header('http://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about' }); expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'); }); it('returns a valid authorization header (sha256)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const { header } = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain' }); expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ext="Bazinga!", mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'); }); it('returns a valid authorization header (no ext)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const { header } = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain' }); expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'); }); it('returns a valid authorization header (null ext)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const { header } = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain', ext: null }); expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'); }); it('returns a valid authorization header (empty payload)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const { header } = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: '', contentType: 'text/plain' }); expect(header).to.equal('Hawk id=\"123456\", ts=\"1353809207\", nonce=\"Ygvqdz\", hash=\"q/t+NNAkQZNlq/aAD6PlexImwQTxwgT2MahfTa9XRLA=\", mac=\"U5k16YEzn3UnBHKeBzsDXn067Gu3R4YaY6xOt9PYRZM=\"'); }); it('returns a valid authorization header (pre hashed payload)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const options = { credentials, timestamp: 1353809207, nonce: 'Ygvqdz', payload: 'something to write about', contentType: 'text/plain' }; options.hash = Hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); const { header } = Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', options); expect(header).to.equal('Hawk id="123456", ts="1353809207", nonce="Ygvqdz", hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'); }); it('errors on missing uri', () => { expect(() => Hawk.client.header('', 'POST')).to.throw('Invalid argument type'); }); it('errors on invalid uri', () => { expect(() => Hawk.client.header(4, 'POST')).to.throw('Invalid argument type'); }); it('errors on missing method', () => { expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', '')).to.throw('Invalid argument type'); }); it('errors on invalid method', () => { expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 5)).to.throw('Invalid argument type'); }); it('errors on missing options', () => { expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST')).to.throw('Invalid argument type'); }); it('errors on invalid options', () => { expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', 'abc')).to.throw('Invalid argument type'); }); it('errors on invalid credentials (id)', () => { const credentials = { key: '2983d45yun89q', algorithm: 'sha256' }; expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207 })).to.throw('Invalid credentials'); }); it('errors on missing credentials', () => { expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { ext: 'Bazinga!', timestamp: 1353809207 })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (key)', () => { const credentials = { id: '123456', algorithm: 'sha256' }; expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207 })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (algorithm)', () => { const credentials = { id: '123456', key: 'asdasdasd' }; expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, ext: 'Bazinga!', timestamp: 1353809207 })).to.throw('Invalid credentials'); }); it('errors on invalid algorithm', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'hmac-sha-0' }; expect(() => Hawk.client.header('https://example.net/somewhere/over/the/rainbow', 'POST', { credentials, payload: 'something, anything!', ext: 'Bazinga!', timestamp: 1353809207 })).to.throw('Unknown algorithm'); }); }); describe('authenticate()', () => { it('rejects on invalid header', () => { const res = { headers: { 'server-authorization': 'Hawk mac="abc", bad="xyz"' } }; expect(() => Hawk.client.authenticate(res)).to.throw('Invalid Server-Authorization header'); }); it('rejects on invalid mac', () => { const res = { headers: { 'content-type': 'text/plain', 'server-authorization': 'Hawk mac="_IJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' } }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1362336900', nonce: 'eb5S_L', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', app: undefined, dlg: undefined, mac: 'BlmSe8K+pbKIb6YsZCnt4E1GrYvY1AaYayNR82dGpIk=', id: '123456' }; const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; expect(() => Hawk.client.authenticate(res, credentials, artifacts)).to.throw('Bad response mac'); }); it('returns headers on ignoring hash', () => { const res = { headers: { 'content-type': 'text/plain', 'server-authorization': 'Hawk mac="XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' } }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1362336900', nonce: 'eb5S_L', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', app: undefined, dlg: undefined, mac: 'BlmSe8K+pbKIb6YsZCnt4E1GrYvY1AaYayNR82dGpIk=', id: '123456' }; const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const { headers } = Hawk.client.authenticate(res, credentials, artifacts, { payload: null }); expect(headers).to.equal({ 'server-authorization': { mac: 'XIJRsMl/4oL+nn+vKoeVZPdCHXB4yJkNnBbTbHFZUYE=', hash: 'f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=', ext: 'response-specific' } }); }); it('validates response payload', () => { const payload = 'some reply'; const res = { headers: { 'content-type': 'text/plain', 'server-authorization': 'Hawk mac="odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' } }; const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1453070933', nonce: '3hOHpR', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', app: undefined, dlg: undefined, mac: '/DitzeD66F2f7O535SERbX9p+oh9ZnNLqSNHG+c7/vs=', id: '123456' }; const { headers } = Hawk.client.authenticate(res, credentials, artifacts, { payload }); expect(headers).to.equal({ 'server-authorization': { mac: 'odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=', hash: 'f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=', ext: 'response-specific' } }); }); it('errors on invalid response payload', () => { const payload = 'wrong reply'; const res = { headers: { 'content-type': 'text/plain', 'server-authorization': 'Hawk mac="odsVGUq0rCoITaiNagW22REIpqkwP9zt5FyqqOW9Zj8=", hash="f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=", ext="response-specific"' } }; const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1453070933', nonce: '3hOHpR', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', app: undefined, dlg: undefined, mac: '/DitzeD66F2f7O535SERbX9p+oh9ZnNLqSNHG+c7/vs=', id: '123456' }; expect(() => Hawk.client.authenticate(res, credentials, artifacts, { payload })).to.throw('Bad response payload mac'); }); it('fails on invalid WWW-Authenticate header format', () => { const header = 'Hawk ts="1362346425875", tsm="PhwayS28vtnn3qbv0mqRBYSXebN/zggEtucfeZ620Zo=", x="Stale timestamp"'; expect(() => Hawk.client.authenticate({ headers: { 'www-authenticate': header } }, {})).to.throw('Invalid WWW-Authenticate header'); }); it('fails on invalid WWW-Authenticate header format', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const header = 'Hawk ts="1362346425875", tsm="hwayS28vtnn3qbv0mqRBYSXebN/zggEtucfeZ620Zo=", error="Stale timestamp"'; expect(() => Hawk.client.authenticate({ headers: { 'www-authenticate': header } }, credentials)).to.throw('Invalid server timestamp hash'); }); it('skips tsm validation when missing ts', () => { const header = 'Hawk error="Stale timestamp"'; const { headers } = Hawk.client.authenticate({ headers: { 'www-authenticate': header } }); expect(headers).to.equal({ 'www-authenticate': { error: 'Stale timestamp' } }); }); it('errors on missing server-authorization header', () => { const res = { headers: { 'content-type': 'text/plain' } }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1362336900', nonce: 'eb5S_L', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', app: undefined, dlg: undefined, mac: 'BlmSe8K+pbKIb6YsZCnt4E1GrYvY1AaYayNR82dGpIk=', id: '123456' }; const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; expect(() => Hawk.client.authenticate(res, credentials, artifacts, { required: true })).to.throw('Missing Server-Authorization header'); }); }); describe('message()', () => { it('generates authorization', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; const auth = Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' }); expect(auth).to.exist(); expect(auth.ts).to.equal(1353809207); expect(auth.nonce).to.equal('abc123'); }); it('errors on invalid host', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message(5, 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on invalid port', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', '80', 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on missing host', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message(undefined, 0, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on missing port', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', undefined, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on null message', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', 80, null, { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on missing message', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', 80, undefined, { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on invalid message', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', 80, 5, { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid inputs'); }); it('errors on invalid credentials', () => { expect(() => Hawk.client.message('example.com', 80, 'I am the boodyman')).to.throw('Invalid credentials'); }); it('errors on invalid options', () => { expect(() => Hawk.client.message('example.com', 80, 'I am the boodyman', '123')).to.throw('Invalid inputs'); }); it('errors on invalid credentials (id)', () => { const credentials = { key: '2983d45yun89q', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (algorithm)', () => { const credentials = { key: '2983d45yun89q', id: '123' }; expect(() => Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (key)', () => { const credentials = { id: '123456', algorithm: 'sha1' }; expect(() => Hawk.client.message('example.com', 80, 'I am the boodyman', { credentials, timestamp: 1353809207, nonce: 'abc123' })).to.throw('Invalid credentials'); }); }); }); hawk-9.0.1/test/crypto.js000077500000000000000000000037001423425235400153170ustar00rootroot00000000000000'use strict'; const Code = require('@hapi/code'); const Hawk = require('..'); const Lab = require('@hapi/lab'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Crypto', () => { describe('generateNormalizedString()', () => { it('should return a valid normalized string', () => { expect(Hawk.crypto.generateNormalizedString('header', { ts: 1357747017, nonce: 'k3k4j5', method: 'GET', resource: '/resource/something', host: 'example.com', port: 8080 })).to.equal('hawk.1.header\n1357747017\nk3k4j5\nGET\n/resource/something\nexample.com\n8080\n\n\n'); }); it('should return a valid normalized string (ext)', () => { expect(Hawk.crypto.generateNormalizedString('header', { ts: 1357747017, nonce: 'k3k4j5', method: 'GET', resource: '/resource/something', host: 'example.com', port: 8080, ext: 'this is some app data' })).to.equal('hawk.1.header\n1357747017\nk3k4j5\nGET\n/resource/something\nexample.com\n8080\n\nthis is some app data\n'); }); it('should return a valid normalized string (payload + ext)', () => { expect(Hawk.crypto.generateNormalizedString('header', { ts: 1357747017, nonce: 'k3k4j5', method: 'GET', resource: '/resource/something', host: 'example.com', port: 8080, hash: 'U4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=', ext: 'this is some app data' })).to.equal('hawk.1.header\n1357747017\nk3k4j5\nGET\n/resource/something\nexample.com\n8080\nU4MKKSmiVxk37JCCrAVIjV/OhB3y+NdwoCr6RShbVkE=\nthis is some app data\n'); }); }); }); hawk-9.0.1/test/index.js000077500000000000000000000303661423425235400151160ustar00rootroot00000000000000'use strict'; const Url = require('url'); const Code = require('@hapi/code'); const Hawk = require('..'); const Lab = require('@hapi/lab'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Hawk', () => { const credentialsFunc = function (id) { return { id, key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: (id === '1' ? 'sha1' : 'sha256'), user: 'steve' }; }; it('generates a header then successfully parse it (configuration)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials1 = credentialsFunc('123456'); req.authorization = Hawk.client.header(Url.parse('http://example.com:8080/resource/4?filter=a'), req.method, { credentials: credentials1, ext: 'some-app-data' }).header; expect(req.authorization).to.exist(); const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); }); it('generates a header then successfully parse it (node request)', async () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: 'example.com:8080', 'content-type': 'text/plain;x=y' } }; const payload = 'some not so random text'; const credentials1 = credentialsFunc('123456'); const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); req.headers.authorization = reqHeader.header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(() => Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.not.throw(); const res = { headers: { 'content-type': 'text/plain' } }; res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' }); expect(res.headers['server-authorization']).to.exist(); expect(() => Hawk.client.authenticate(res, credentials2, artifacts, { payload: 'some reply' })).to.not.throw(); }); it('generates a header then successfully parse it (absolute request uri)', async () => { const req = { method: 'POST', url: 'http://example.com:8080/resource/4?filter=a', headers: { host: 'example.com:8080', 'content-type': 'text/plain;x=y' } }; const payload = 'some not so random text'; const credentials1 = credentialsFunc('123456'); const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); req.headers.authorization = reqHeader.header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(() => Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.not.throw(); const res = { headers: { 'content-type': 'text/plain' } }; res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' }); expect(res.headers['server-authorization']).to.exist(); expect(() => Hawk.client.authenticate(res, credentials2, artifacts, { payload: 'some reply' })).to.not.throw(); }); it('generates a header then successfully parse it (no server header options)', async () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: 'example.com:8080', 'content-type': 'text/plain;x=y' } }; const payload = 'some not so random text'; const credentials1 = credentialsFunc('123456'); const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); req.headers.authorization = reqHeader.header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(() => Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.not.throw(); const res = { headers: { 'content-type': 'text/plain' } }; res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts); expect(res.headers['server-authorization']).to.exist(); expect(() => Hawk.client.authenticate(res, credentials2, artifacts)).to.not.throw(); }); it('generates a header then fails to parse it (missing server header hash)', async () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: 'example.com:8080', 'content-type': 'text/plain;x=y' } }; const payload = 'some not so random text'; const credentials1 = credentialsFunc('123456'); const reqHeader = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', payload, contentType: req.headers['content-type'] }); req.headers.authorization = reqHeader.header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(() => Hawk.server.authenticatePayload(payload, credentials2, artifacts, req.headers['content-type'])).to.not.throw(); const res = { headers: { 'content-type': 'text/plain' } }; res.headers['server-authorization'] = Hawk.server.header(credentials2, artifacts); expect(res.headers['server-authorization']).to.exist(); expect(() => Hawk.client.authenticate(res, credentials2, artifacts, { payload: 'some reply' })).to.throw('Missing response hash attribute'); }); it('generates a header then successfully parse it (with hash)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials1 = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); }); it('generates a header then successfully parse it then validate payload', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials1 = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(() => Hawk.server.authenticatePayload('hola!', credentials2, artifacts)).to.not.throw(); expect(() => Hawk.server.authenticatePayload('hello!', credentials2, artifacts)).to.throw('Bad payload hash'); }); it('generates a header then successfully parses and validates payload', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials1 = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, payload: 'hola!', ext: 'some-app-data' }).header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc, { payload: 'hola!' }); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); }); it('generates a header then successfully parse it (app)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials1 = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', app: 'asd23ased' }).header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(artifacts.app).to.equal('asd23ased'); }); it('generates a header then successfully parse it (app, dlg)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials1 = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials: credentials1, ext: 'some-app-data', app: 'asd23ased', dlg: '23434szr3q4d' }).header; const { credentials: credentials2, artifacts } = await Hawk.server.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(artifacts.ext).to.equal('some-app-data'); expect(artifacts.app).to.equal('asd23ased'); expect(artifacts.dlg).to.equal('23434szr3q4d'); }); it('generates a header then fail authentication due to bad hash', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials, payload: 'hola!', ext: 'some-app-data' }).header; await expect(Hawk.server.authenticate(req, credentialsFunc, { payload: 'byebye!' })).to.reject('Bad payload hash'); }); it('generates a header for one resource then fail to authenticate another', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const credentials = credentialsFunc('123456'); req.authorization = Hawk.client.header('http://example.com:8080/resource/4?filter=a', req.method, { credentials, ext: 'some-app-data' }).header; req.url = '/something/else'; const err = await expect(Hawk.server.authenticate(req, credentialsFunc)).to.reject(); expect(err.credentials).to.exist(); }); }); hawk-9.0.1/test/readme.js000077500000000000000000000057601423425235400152440ustar00rootroot00000000000000'use strict'; const Code = require('@hapi/code'); const Hawk = require('..'); const Hoek = require('@hapi/hoek'); const Lab = require('@hapi/lab'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('README', () => { describe('core', () => { const credentials = { id: 'dh37fgj492je', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256' }; const options = { credentials, timestamp: 1353832234, nonce: 'j4h3g2', ext: 'some-app-ext-data' }; it('should generate a header protocol example', () => { const { header } = Hawk.client.header('http://example.com:8000/resource/1?b=1&a=2', 'GET', options); expect(header).to.equal('Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="'); }); it('should generate a normalized string protocol example', () => { const normalized = Hawk.crypto.generateNormalizedString('header', { credentials, ts: options.timestamp, nonce: options.nonce, method: 'GET', resource: '/resource/1?b=1&a=2', host: 'example.com', port: 8000, ext: options.ext }); expect(normalized).to.equal('hawk.1.header\n1353832234\nj4h3g2\nGET\n/resource/1?b=1&a=2\nexample.com\n8000\n\nsome-app-ext-data\n'); }); const payloadOptions = Hoek.clone(options); payloadOptions.payload = 'Thank you for flying Hawk'; payloadOptions.contentType = 'text/plain'; it('should generate a header protocol example (with payload)', () => { const { header } = Hawk.client.header('http://example.com:8000/resource/1?b=1&a=2', 'POST', payloadOptions); expect(header).to.equal('Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", hash="Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=", ext="some-app-ext-data", mac="aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw="'); }); it('should generate a normalized string protocol example (with payload)', () => { const normalized = Hawk.crypto.generateNormalizedString('header', { credentials, ts: options.timestamp, nonce: options.nonce, method: 'POST', resource: '/resource/1?b=1&a=2', host: 'example.com', port: 8000, hash: Hawk.crypto.calculatePayloadHash(payloadOptions.payload, credentials.algorithm, payloadOptions.contentType), ext: options.ext }); expect(normalized).to.equal('hawk.1.header\n1353832234\nj4h3g2\nPOST\n/resource/1?b=1&a=2\nexample.com\n8000\nYi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=\nsome-app-ext-data\n'); }); }); }); hawk-9.0.1/test/server.js000077500000000000000000001301171423425235400153100ustar00rootroot00000000000000'use strict'; const Code = require('@hapi/code'); const Hawk = require('..'); const Hoek = require('@hapi/hoek'); const Lab = require('@hapi/lab'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Server', () => { const credentialsFunc = function (id) { return { id, key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: (id === '1' ? 'sha1' : 'sha256'), user: 'steve' }; }; describe('authenticate()', () => { it('parses a valid authentication header (sha1)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"' }; const { credentials } = await Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() }); expect(credentials.user).to.equal('steve'); }); it('parses a valid authentication header (sha256)', async () => { const req = { method: 'GET', url: '/resource/1?b=1&a=2', host: 'example.com', port: 8000, authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", mac="m8r1rHbXN6NgO+KIIhjO7sFRyd78RNGVUwehe8Cp2dU=", ext="some-app-data"' }; const { credentials } = await Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353832234000 - Hawk.utils.now() }); expect(credentials.user).to.equal('steve'); }); it('parses a valid authentication header (host override)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', headers: { host: 'example1.com:8080', authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"' } }; const { credentials } = await Hawk.server.authenticate(req, credentialsFunc, { host: 'example.com', localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() }); expect(credentials.user).to.equal('steve'); }); it('parses a valid authentication header (host port override)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', headers: { host: 'example1.com:80', authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"' } }; const { credentials } = await Hawk.server.authenticate(req, credentialsFunc, { host: 'example.com', port: 8080, localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() }); expect(credentials.user).to.equal('steve'); }); it('parses a valid authentication header (POST with payload)', async () => { const req = { method: 'POST', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123456", ts="1357926341", nonce="1AwuJD", hash="qAiXIVv+yjDATneWxZP2YCTa9aHRgQdnH9b3Wc+o3dg=", ext="some-app-data", mac="UeYcj5UoTVaAWXNvJfLVia7kU3VabxCqrccXP8sUGC4="' }; const { credentials } = await Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1357926341000 - Hawk.utils.now() }); expect(credentials.user).to.equal('steve'); }); it('errors on missing hash', async () => { const req = { method: 'GET', url: '/resource/1?b=1&a=2', host: 'example.com', port: 8000, authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", mac="m8r1rHbXN6NgO+KIIhjO7sFRyd78RNGVUwehe8Cp2dU=", ext="some-app-data"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { payload: 'body', localtimeOffsetMsec: 1353832234000 - Hawk.utils.now() })).to.reject('Missing required payload hash'); }); it('errors on missing hash (empty payload)', async () => { const req = { method: 'GET', url: '/resource/1?b=1&a=2', host: 'example.com', port: 8000, authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", mac="m8r1rHbXN6NgO+KIIhjO7sFRyd78RNGVUwehe8Cp2dU=", ext="some-app-data"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { payload: '', localtimeOffsetMsec: 1353832234000 - Hawk.utils.now() })).to.reject('Missing required payload hash'); }); it('errors on a stale timestamp', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123456", ts="1362337299", nonce="UzmxSs", ext="some-app-data", mac="wnNUxchvvryMH2RxckTdZ/gY3ijzvccx4keVvELC61w="' }; const err = await expect(Hawk.server.authenticate(req, credentialsFunc)).to.reject('Stale timestamp'); const header = err.output.headers['WWW-Authenticate']; const ts = header.match(/^Hawk ts\=\"(\d+)\"\, tsm\=\"([^\"]+)\"\, error=\"Stale timestamp\"$/); const now = Hawk.utils.now(); expect(parseInt(ts[1], 10) * 1000).to.be.within(now - 1000, now + 1000); const res = { headers: { 'www-authenticate': header } }; expect(() => Hawk.client.authenticate(res, err.credentials, err.artifacts)).to.not.throw(); }); it('errors on a replay', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="bXx7a7p1h9QYQNZ8x7QhvDQym8ACgab4m3lVSFn4DBw=", ext="hello"' }; const memoryCache = {}; const options = { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now(), nonceFunc: function (key, nonce, ts) { if (memoryCache[key + nonce]) { throw new Error(); } memoryCache[key + nonce] = true; } }; const { credentials } = await Hawk.server.authenticate(req, credentialsFunc, options); expect(credentials.user).to.equal('steve'); await expect(Hawk.server.authenticate(req, credentialsFunc, options)).to.reject('Invalid nonce'); }); it('does not error on nonce collision if keys differ', async () => { const reqSteve = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="bXx7a7p1h9QYQNZ8x7QhvDQym8ACgab4m3lVSFn4DBw=", ext="hello"' }; const reqBob = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="456", ts="1353788437", nonce="k3j4h2", mac="LXfmTnRzrLd9TD7yfH+4se46Bx6AHyhpM94hLCiNia4=", ext="hello"' }; const credentialsFuncion = function (id) { const credentials = { '123': { id, key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: (id === '1' ? 'sha1' : 'sha256'), user: 'steve' }, '456': { id, key: 'xrunpaw3489ruxnpa98w4rxnwerxhqb98rpaxn39848', algorithm: (id === '1' ? 'sha1' : 'sha256'), user: 'bob' } }; return credentials[id]; }; const memoryCache = {}; const options = { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now(), nonceFunc: function (key, nonce, ts) { if (memoryCache[key + nonce]) { throw new Error(); } memoryCache[key + nonce] = true; } }; const { credentials: credentials1 } = await Hawk.server.authenticate(reqSteve, credentialsFuncion, options); expect(credentials1.user).to.equal('steve'); const { credentials: credentials2 } = await Hawk.server.authenticate(reqBob, credentialsFuncion, options); expect(credentials2.user).to.equal('bob'); }); it('errors on an invalid authentication header: wrong scheme', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Basic asdasdasdasd' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Unauthorized'); }); it('errors on an invalid authentication header: no scheme', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: '!@#' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid header syntax'); }); it('errors on an missing authorization header', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const err = await expect(Hawk.server.authenticate(req, credentialsFunc)).to.reject(); expect(err.isMissing).to.equal(true); }); it('errors on an missing host header', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', headers: { authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' } }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid Host header'); }); it('errors on an missing authorization attribute (id)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Missing attributes'); }); it('errors on an missing authorization attribute (ts)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Missing attributes'); }); it('errors on an missing authorization attribute (nonce)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Missing attributes'); }); it('errors on an missing authorization attribute (mac)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Missing attributes'); }); it('errors on an unknown authorization attribute', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", x="3", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Unknown attribute: x'); }); it('errors on an bad authorization header format', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123\\", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Bad header format'); }); it('errors on an bad authorization attribute value', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="\t", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Bad attribute value: id'); }); it('errors on an empty authorization attribute value', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Bad attribute value: id'); }); it('errors on duplicated authorization attribute key', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", id="456", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Duplicate attribute: id'); }); it('errors on an invalid authorization header format', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk' }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid header syntax'); }); it('errors on an bad host header (missing host)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', headers: { host: ':8080', authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' } }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid Host header'); }); it('errors on an bad host header (pad port)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', headers: { host: 'example.com:something', authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' } }; await expect(Hawk.server.authenticate(req, credentialsFunc, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid Host header'); }); it('errors on credentialsFunc error', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { throw new Error('Unknown user'); }; await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Unknown user'); }); it('errors on credentialsFunc error (with credentials)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { const error = new Error('Unknown user'); error.credentials = { some: 'value' }; throw error; }; const err = await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Unknown user'); expect(err.credentials.some).to.equal('value'); }); it('errors on missing credentials', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { return null; }; await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Unknown credentials'); }); it('errors on invalid credentials (id)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { return { key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', user: 'steve' }; }; const err = await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid credentials'); expect(err.output.payload.message).to.equal('An internal server error occurred'); }); it('errors on invalid credentials (key)', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { return { id: '23434d3q4d5345d', user: 'steve' }; }; const err = await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Invalid credentials'); expect(err.output.payload.message).to.equal('An internal server error occurred'); }); it('errors on unknown credentials algorithm', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcUyW6EEgUH4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { return { key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'hmac-sha-0', user: 'steve' }; }; const err = await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Unknown algorithm'); expect(err.output.payload.message).to.equal('An internal server error occurred'); }); it('errors on unknown bad mac', async () => { const req = { method: 'GET', url: '/resource/4?filter=a', host: 'example.com', port: 8080, authorization: 'Hawk id="123", ts="1353788437", nonce="k3j4h2", mac="/qwS4UjfVWMcU4jlr7T/wuKe3dKijvTvSos=", ext="hello"' }; const credentialsFuncion = function (id) { return { key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; }; await expect(Hawk.server.authenticate(req, credentialsFuncion, { localtimeOffsetMsec: 1353788437000 - Hawk.utils.now() })).to.reject('Bad mac'); }); }); describe('header()', () => { it('generates header', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; const header = Hawk.server.header(credentials, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' }); expect(header).to.equal('Hawk mac=\"n14wVJK4cOxAytPUMc5bPezQzuJGl5n7MYXhFQgEKsE=\", hash=\"f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=\", ext=\"response-specific\"'); }); it('generates header (empty payload)', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; const header = Hawk.server.header(credentials, artifacts, { payload: '', contentType: 'text/plain', ext: 'response-specific' }); expect(header).to.equal('Hawk mac=\"i8/kUBDx0QF+PpCtW860kkV/fa9dbwEoe/FpGUXowf0=\", hash=\"q/t+NNAkQZNlq/aAD6PlexImwQTxwgT2MahfTa9XRLA=\", ext=\"response-specific\"'); }); it('generates header (empty ext)', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: '', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; const header = Hawk.server.header(credentials, artifacts, { payload: '', contentType: 'text/plain', ext: '' }); expect(header).to.equal('Hawk mac=\"q+fdjQv3kF56JGKLYeLzAS9dYGcvDqAXRG7MTVHAFKE=\", hash=\"q/t+NNAkQZNlq/aAD6PlexImwQTxwgT2MahfTa9XRLA=\"'); }); it('generates header (pre calculated hash)', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; const options = { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' }; options.hash = Hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); const header = Hawk.server.header(credentials, artifacts, options); expect(header).to.equal('Hawk mac=\"n14wVJK4cOxAytPUMc5bPezQzuJGl5n7MYXhFQgEKsE=\", hash=\"f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=\", ext=\"response-specific\"'); }); it('generates header (null ext)', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; const header = Hawk.server.header(credentials, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: null }); expect(header).to.equal('Hawk mac=\"6PrybJTJs20jsgBw5eilXpcytD8kUbaIKNYXL+6g0ns=\", hash=\"f9cDF/TDm7TkYRLnGwRMfeDzT6LixQVLvrIKhh0vgmM=\"'); }); it('errors on missing artifacts', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; expect(() => Hawk.server.header(credentials, null, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' })).to.throw('Invalid inputs'); }); it('errors on invalid artifacts', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'sha256', user: 'steve' }; expect(() => Hawk.server.header(credentials, 5, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' })).to.throw('Invalid inputs'); }); it('errors on missing credentials', () => { const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; expect(() => Hawk.server.header(null, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (key)', () => { const credentials = { id: '123456', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; expect(() => Hawk.server.header(credentials, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (algorithm)', () => { const credentials = { id: '123456', key: 'asdasd', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; expect(() => Hawk.server.header(credentials, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' })).to.throw('Invalid credentials'); }); it('errors on invalid algorithm', () => { const credentials = { id: '123456', key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: 'x', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; expect(() => Hawk.server.header(credentials, artifacts, { payload: 'some reply', contentType: 'text/plain', ext: 'response-specific' })).to.throw('Unknown algorithm'); }); it('errors on invalid options', () => { const credentials = { id: '123456', algorithm: 'sha256', user: 'steve' }; const artifacts = { method: 'POST', host: 'example.com', port: '8080', resource: '/resource/4?filter=a', ts: '1398546787', nonce: 'xUwusx', hash: 'nJjkVtBE5Y/Bk38Aiokwn0jiJxt/0S2WRSUwWLCf5xk=', ext: 'some-app-data', mac: 'dvIvMThwi28J61Jc3P0ryAhuKpanU63GXdx6hkmQkJA=', id: '123456' }; expect(() => Hawk.server.header(credentials, artifacts, 'abc')).to.throw('Invalid inputs'); }); }); describe('authenticateBewit()', () => { it('errors on uri too long', async () => { let long = '/'; for (let i = 0; i < 5000; ++i) { long += 'x'; } const req = { method: 'GET', url: long, host: 'example.com', port: 8080, authorization: 'Hawk id="1", ts="1353788437", nonce="k3j4h2", mac="zy79QQ5/EYFmQqutVnYb73gAc/U=", ext="hello"' }; const err = await expect(Hawk.server.authenticateBewit(req, credentialsFunc, {})).to.reject('Resource path exceeds max length'); expect(err.output.statusCode).to.equal(400); }); }); describe('authenticateMessage()', () => { it('errors on invalid authorization (ts)', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); delete auth.ts; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc)).to.reject('Invalid authorization'); }); it('errors on invalid authorization (mac)', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); delete auth.mac; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc)).to.reject('Invalid authorization'); }); it('errors on invalid authorization (nonce)', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); delete auth.nonce; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc)).to.reject('Invalid authorization'); }); it('errors on invalid authorization (hash)', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); delete auth.hash; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc)).to.reject('Invalid authorization'); }); it('errors with credentials', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); const err = await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, (id) => { const error = new Error('something'); error.credentials = { some: 'value' }; throw error; })).to.reject('something'); expect(err.credentials.some).to.equal('value'); }); it('errors on nonce collision', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); const nonceFunc = function (key, nonce, ts) { throw new Error(); }; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, { nonceFunc })).to.reject('Invalid nonce'); }); it('should generate an authorization then successfully parse it', async () => { const credentials1 = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials: credentials1 }); expect(auth).to.exist(); const { credentials: credentials2 } = await Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc); expect(credentials2.user).to.equal('steve'); }); it('should fail authorization on mismatching host', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); await expect(Hawk.server.authenticateMessage('example1.com', 8080, 'some message', auth, credentialsFunc)).to.reject('Bad mac'); }); it('should fail authorization on stale timestamp', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, { localtimeOffsetMsec: 100000 })).to.reject('Stale timestamp'); }); it('overrides timestampSkewSec', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials, localtimeOffsetMsec: 100000 }); expect(auth).to.exist(); await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, { timestampSkewSec: 500 })).to.not.reject(); }); it('should fail authorization on invalid authorization', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); delete auth.id; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc)).to.reject('Invalid authorization'); }); it('should fail authorization on bad hash', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message1', auth, credentialsFunc)).to.reject('Bad message hash'); }); it('should fail authorization on nonce error', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, credentialsFunc, { nonceFunc: function (key, nonce, ts) { throw new Error('kaboom'); } })).to.reject('Invalid nonce'); }); it('should fail authorization on credentials error', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); const errFunc = function (id) { throw new Error('kablooey'); }; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, errFunc)).to.reject('kablooey'); }); it('should fail authorization on missing credentials', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); const errFunc = function (id) { }; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, errFunc)).to.reject('Unknown credentials'); }); it('should fail authorization on invalid credentials', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); const errFunc = function (id) { return {}; }; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, errFunc)).to.reject('Invalid credentials'); }); it('should fail authorization on invalid credentials (algorithm)', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); const errFunc = function (id) { return { key: 'asdasd' }; }; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, errFunc)).to.reject('Invalid credentials'); }); it('should fail authorization on invalid credentials algorithm', async () => { const credentials = credentialsFunc('123456'); const auth = Hawk.client.message('example.com', 8080, 'some message', { credentials }); expect(auth).to.exist(); const errFunc = function (id) { return { key: '123', algorithm: '456' }; }; await expect(Hawk.server.authenticateMessage('example.com', 8080, 'some message', auth, errFunc)).to.reject('Unknown algorithm'); }); it('should fail on missing host', () => { const credentials = credentialsFunc('123456'); expect(() => Hawk.client.message(null, 8080, 'some message', { credentials })).to.throw('Invalid inputs'); }); it('should fail on missing credentials', () => { expect(() => Hawk.client.message('example.com', 8080, 'some message')).to.throw('Invalid credentials'); }); it('should fail on invalid algorithm', () => { const credentials = credentialsFunc('123456'); const creds = Hoek.clone(credentials); creds.algorithm = 'blah'; expect(() => Hawk.client.message('example.com', 8080, 'some message', { credentials: creds })).to.throw('Unknown algorithm'); }); }); describe('authenticatePayloadHash()', () => { it('checks payload hash', () => { expect(() => Hawk.server.authenticatePayloadHash('abcdefg', { hash: 'abcdefg' })).to.not.throw(); expect(() => Hawk.server.authenticatePayloadHash('1234567', { hash: 'abcdefg' })).to.throw('Bad payload hash'); }); }); }); hawk-9.0.1/test/uri.js000077500000000000000000000435521423425235400146070ustar00rootroot00000000000000'use strict'; const Url = require('url'); const B64 = require('@hapi/b64'); const Boom = require('@hapi/boom'); const Code = require('@hapi/code'); const Hawk = require('..'); const Lab = require('@hapi/lab'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('Uri', () => { const credentialsFunc = function (id) { return { id, key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn', algorithm: (id === '1' ? 'sha1' : 'sha256'), user: 'steve' }; }; it('should generate a bewit then successfully authenticate it', async () => { const req = { method: 'GET', url: '/resource/4?a=1&b=2', host: 'example.com', port: 80 }; const credentials1 = credentialsFunc('123456'); const bewit = Hawk.uri.getBewit('http://example.com/resource/4?a=1&b=2', { credentials: credentials1, ttlSec: 60 * 60 * 24 * 365 * 100, ext: 'some-app-data' }); req.url += '&bewit=' + bewit; const { credentials: credentials2, attributes } = await Hawk.uri.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(attributes.ext).to.equal('some-app-data'); }); it('should generate a bewit then successfully authenticate it (HEAD)', async () => { const req = { method: 'HEAD', url: '/resource/4?a=1&b=2', host: 'example.com', port: 80 }; const credentials1 = credentialsFunc('123456'); const bewit = Hawk.uri.getBewit('http://example.com/resource/4?a=1&b=2', { credentials: credentials1, ttlSec: 60 * 60 * 24 * 365 * 100, ext: 'some-app-data' }); req.url += '&bewit=' + bewit; const { credentials: credentials2, attributes } = await Hawk.uri.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); expect(attributes.ext).to.equal('some-app-data'); }); it('should generate a bewit then successfully authenticate it (no ext)', async () => { const req = { method: 'GET', url: '/resource/4?a=1&b=2', host: 'example.com', port: 80 }; const credentials1 = credentialsFunc('123456'); const bewit = Hawk.uri.getBewit('http://example.com/resource/4?a=1&b=2', { credentials: credentials1, ttlSec: 60 * 60 * 24 * 365 * 100 }); req.url += '&bewit=' + bewit; const { credentials: credentials2 } = await Hawk.uri.authenticate(req, credentialsFunc); expect(credentials2.user).to.equal('steve'); }); it('should successfully authenticate a request (last param)', async () => { const req = { method: 'GET', url: '/resource/4?a=1&b=2&bewit=MTIzNDU2XDQ1MTE0ODQ2MjFcMzFjMmNkbUJFd1NJRVZDOVkva1NFb2c3d3YrdEVNWjZ3RXNmOGNHU2FXQT1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; const { credentials, attributes } = await Hawk.uri.authenticate(req, credentialsFunc); expect(credentials.user).to.equal('steve'); expect(attributes.ext).to.equal('some-app-data'); }); it('should successfully authenticate a request (first param)', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MTE0ODQ2MjFcMzFjMmNkbUJFd1NJRVZDOVkva1NFb2c3d3YrdEVNWjZ3RXNmOGNHU2FXQT1cc29tZS1hcHAtZGF0YQ&a=1&b=2', host: 'example.com', port: 8080 }; const { credentials, attributes } = await Hawk.uri.authenticate(req, credentialsFunc); expect(credentials.user).to.equal('steve'); expect(attributes.ext).to.equal('some-app-data'); }); it('should successfully authenticate a request (only param)', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MTE0ODQ2NDFcZm1CdkNWT3MvcElOTUUxSTIwbWhrejQ3UnBwTmo4Y1VrSHpQd3Q5OXJ1cz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; const { credentials, attributes } = await Hawk.uri.authenticate(req, credentialsFunc); expect(credentials.user).to.equal('steve'); expect(attributes.ext).to.equal('some-app-data'); }); it('fails on multiple authentication', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MTE0ODQ2NDFcZm1CdkNWT3MvcElOTUUxSTIwbWhrejQ3UnBwTmo4Y1VrSHpQd3Q5OXJ1cz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080, authorization: 'Basic asdasdasdasd' }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Multiple authentications'); }); it('fails on method other than GET', async () => { const credentials = credentialsFunc('123456'); const req = { method: 'POST', url: '/resource/4?filter=a', host: 'example.com', port: 8080 }; const exp = Math.floor(Hawk.utils.now() / 1000) + 60; const ext = 'some-app-data'; const mac = Hawk.crypto.calculateMac('bewit', credentials, { ts: exp, nonce: '', method: req.method, resource: req.url, host: req.host, port: req.port, ext }); const bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + ext; req.url += '&bewit=' + B64.base64urlEncode(bewit); await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Invalid method'); }); it('fails on invalid host header', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', headers: { host: 'example.com:something' } }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Invalid Host header'); }); it('fails on empty bewit', async () => { const req = { method: 'GET', url: '/resource/4?bewit=', host: 'example.com', port: 8080 }; const err = await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Empty bewit'); expect(err.isMissing).to.not.exist(); }); it('fails on invalid bewit', async () => { const req = { method: 'GET', url: '/resource/4?bewit=*', host: 'example.com', port: 8080 }; const err = await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Invalid bewit encoding'); expect(err.isMissing).to.not.exist(); }); it('fails on missing bewit', async () => { const req = { method: 'GET', url: '/resource/4', host: 'example.com', port: 8080 }; const err = await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Unauthorized'); expect(err.isMissing).to.equal(true); }); it('fails on invalid bewit structure', async () => { const req = { method: 'GET', url: '/resource/4?bewit=abc', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Invalid bewit structure'); }); it('fails on empty bewit attribute', async () => { const req = { method: 'GET', url: '/resource/4?bewit=YVxcY1xk', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Missing bewit attributes'); }); it('fails on missing bewit id attribute', async () => { const req = { method: 'GET', url: '/resource/4?bewit=XDQ1NTIxNDc2MjJcK0JFbFhQMXhuWjcvd1Nrbm1ldGhlZm5vUTNHVjZNSlFVRHk4NWpTZVJ4VT1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Missing bewit attributes'); }); it('fails on missing bewit mac attribute', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ3MDkzMTY5NjNcXHNvbWUtYXBwLWRhdGE', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Missing bewit attributes'); }); it('fails on expired access', async () => { const req = { method: 'GET', url: '/resource/4?a=1&b=2&bewit=MTIzNDU2XDEzNTY0MTg1ODNcWk1wZlMwWU5KNHV0WHpOMmRucTRydEk3NXNXTjFjeWVITTcrL0tNZFdVQT1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, credentialsFunc)).to.reject('Access expired'); }); it('fails on credentials function error', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, (id) => { throw Boom.badRequest('Boom'); })).to.reject('Boom'); }); it('fails on credentials function error with credentials', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; const err = await expect(Hawk.uri.authenticate(req, (id, callback) => { const error = Boom.badRequest('Boom'); error.credentials = { some: 'value' }; throw error; })).to.reject('Boom'); expect(err.credentials.some).to.equal('value'); }); it('fails on null credentials function response', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, (id) => null)).to.reject('Unknown credentials'); }); it('fails on invalid credentials function response', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, (id) => ({}))).to.reject('Invalid credentials'); }); it('fails on invalid credentials function response (algorithm)', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, (id) => ({ key: '123123' }))).to.reject('Invalid credentials'); }); it('fails on invalid credentials function response (unknown algorithm)', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, (id) => ({ key: 'xxx', algorithm: 'xxx' }))).to.reject('Unknown algorithm'); }); it('fails on invalid credentials function response (bad mac)', async () => { const req = { method: 'GET', url: '/resource/4?bewit=MTIzNDU2XDQ1MDk5OTE3MTlcTUE2eWkwRWRwR0pEcWRwb0JkYVdvVDJrL0hDSzA1T0Y3MkhuZlVmVy96Zz1cc29tZS1hcHAtZGF0YQ', host: 'example.com', port: 8080 }; await expect(Hawk.uri.authenticate(req, (id) => ({ key: 'xxx', algorithm: 'sha256' }))).to.reject('Bad mac'); }); describe('getBewit()', () => { it('returns a valid bewit value', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdca3NjeHdOUjJ0SnBQMVQxekRMTlBiQjVVaUtJVTl0T1NKWFRVZEc3WDloOD1ceGFuZHlhbmR6'); }); it('returns a valid bewit value (explicit port)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const bewit = Hawk.uri.getBewit('https://example.com:8080/somewhere/over/the/rainbow', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdcaFpiSjNQMmNLRW80a3kwQzhqa1pBa1J5Q1p1ZWc0V1NOYnhWN3ZxM3hIVT1ceGFuZHlhbmR6'); }); it('returns a valid bewit value (null ext)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const bewit = Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: null }); expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdcSUdZbUxnSXFMckNlOEN4dktQczRKbFdJQStValdKSm91d2dBUmlWaENBZz1c'); }); it('returns a valid bewit value (parsed uri)', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; const bewit = Hawk.uri.getBewit(Url.parse('https://example.com/somewhere/over/the/rainbow'), { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' }); expect(bewit).to.equal('MTIzNDU2XDEzNTY0MjA3MDdca3NjeHdOUjJ0SnBQMVQxekRMTlBiQjVVaUtJVTl0T1NKWFRVZEc3WDloOD1ceGFuZHlhbmR6'); }); it('errors on invalid options', () => { expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', 4)).to.throw('Invalid inputs'); }); it('errors on missing options.ttlSec', () => { expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', {})).to.throw('Invalid inputs'); }); it('errors on missing uri', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; expect(() => Hawk.uri.getBewit('', { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' })).to.throw('Invalid inputs'); }); it('errors on invalid uri', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'sha256' }; expect(() => Hawk.uri.getBewit(5, { credentials, ttlSec: 300, localtimeOffsetMsec: 1356420407232 - Hawk.utils.now(), ext: 'xandyandz' })).to.throw('Invalid inputs'); }); it('errors on invalid credentials (id)', () => { const credentials = { key: '2983d45yun89q', algorithm: 'sha256' }; expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 3000, ext: 'xandyandz' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (id)', () => { const credentials = { key: '2983d45yun89q', algorithm: 'sha256' }; expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 3000, ext: 'xandyandz' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (algorithm)', () => { const credentials = { key: '2983d45yun89q', id: '123' }; expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 3000, ext: 'xandyandz' })).to.throw('Invalid credentials'); }); it('errors on missing credentials', () => { expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { ttlSec: 3000, ext: 'xandyandz' })).to.throw('Invalid credentials'); }); it('errors on invalid credentials (key)', () => { const credentials = { id: '123456', algorithm: 'sha256' }; expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 3000, ext: 'xandyandz' })).to.throw('Invalid credentials'); }); it('errors on invalid algorithm', () => { const credentials = { id: '123456', key: '2983d45yun89q', algorithm: 'hmac-sha-0' }; expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow', { credentials, ttlSec: 300, ext: 'xandyandz' })).to.throw('Unknown algorithm'); }); it('errors on missing options', () => { expect(() => Hawk.uri.getBewit('https://example.com/somewhere/over/the/rainbow')).to.throw('Invalid inputs'); }); }); }); hawk-9.0.1/test/utils.js000077500000000000000000000075211423425235400151440ustar00rootroot00000000000000'use strict'; const Code = require('@hapi/code'); const Hawk = require('..'); const Lab = require('@hapi/lab'); const Package = require('../package.json'); const internals = {}; const { describe, it, after } = exports.lab = Lab.script(); const expect = Code.expect; describe('Utils', () => { describe('parseHost()', () => { it('returns port 80 for non tls node request', () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: 'example.com', 'content-type': 'text/plain;x=y' } }; expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(80); }); it('returns port 443 for non tls node request', () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: 'example.com', 'content-type': 'text/plain;x=y' }, connection: { encrypted: true } }; expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(443); }); it('returns port 443 for non tls node request (IPv6)', () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: '[123:123:123]', 'content-type': 'text/plain;x=y' }, connection: { encrypted: true } }; expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(443); }); it('parses IPv6 headers', () => { const req = { method: 'POST', url: '/resource/4?filter=a', headers: { host: '[123:123:123]:8000', 'content-type': 'text/plain;x=y' }, connection: { encrypted: true } }; const host = Hawk.utils.parseHost(req, 'Host'); expect(host.port).to.equal('8000'); expect(host.name).to.equal('[123:123:123]'); }); it('errors on header too long', () => { let long = ''; for (let i = 0; i < 5000; ++i) { long += 'x'; } expect(Hawk.utils.parseHost({ headers: { host: long } })).to.be.null(); }); }); describe('parseAuthorizationHeader()', () => { it('errors on header too long', () => { let long = 'Scheme a="'; for (let i = 0; i < 5000; ++i) { long += 'x'; } long += '"'; expect(() => Hawk.utils.parseAuthorizationHeader(long, ['a'])).to.throw('Header length too long'); }); }); describe('version()', () => { it('returns the correct package version number', () => { expect(Hawk.utils.version()).to.equal(Package.version); }); }); describe('setTimeFunction()', () => { after(() => { Hawk.utils.setTimeFunction(Date.now); }); it('creates the value that now() will return', () => { Hawk.utils.setTimeFunction(() => 269323200000); expect(Hawk.utils.now()).to.equal(269323200000); }); }); describe('unauthorized()', () => { it('returns a hawk 401', () => { expect(Hawk.utils.unauthorized('kaboom').output.headers['WWW-Authenticate']).to.equal('Hawk error="kaboom"'); }); it('supports attributes', () => { expect(Hawk.utils.unauthorized('kaboom', { a: 'b' }).output.headers['WWW-Authenticate']).to.equal('Hawk a="b", error="kaboom"'); }); }); });