Giter Site home page Giter Site logo

jessety / simple-hmac-auth Goto Github PK

View Code? Open in Web Editor NEW
13.0 3.0 3.0 286 KB

Protocol specification and Node library designed to make building APIs that use HMAC signatures simple

Home Page: https://npmjs.com/package/simple-hmac-auth

License: MIT License

JavaScript 23.74% Handlebars 0.65% TypeScript 75.61%
nodejs api-security hmac-authentication request-signing request-signatures simple-hmac-auth

simple-hmac-auth's Introduction

simple-hmac-auth

HTTP authentication specification and Node library designed to make building APIs that use HMAC signatures simple.

ci coverage npm license

Specification

For all incoming requests, the HTTP method, path, query string, headers and body should be signed with a secret and sent as the request's "signature." The headers should include the user's API key as well as a timestamp of when the request was made. On the server, the request signature is re-generated and confirmed against the signature from the client. If the signatures do not match the request is rejected. If the server receives a request with a timestamp older than five minutes it is also rejected.

This enables three things:

  • Verify the authenticity of the client
  • Prevent MITM attack
  • Protect against replay attacks

The client's authenticity is confirmed by their continued ability to produce signatures based on their secret. This approach also prevents man-in-the-middle attacks because any tampering would result in the signature mismatching the request's contents. Finally, replay attacks are prevented because signed requests with old timestamps will be rejected.

Request signatures are designed to be used in conjunction with HTTPS.

Headers

Each request requires three headers: authorization, signature and either date or timestamp. If the HTTP request contains a body, the content-length and content-type headers are also required.

The date header is a standard RFC-822 (updated in RFC-1123) date, as per RFC-7231. Because it cannot be programmatically set inside of a browser, the timestamp header may be substituted instead.

The authorization header is a standard as per RFC-2617 that, confusingly, is designed for authentication and not authorization. It should contain a string representation of the client's API key.

The signature header contains the signature of the entire request, as well as a reference to the version of the protocol, and the algorithm used to generate the signature.

(Note: As per RFC-6648, X- prefixed headers should not be adopted for new protocols, and thus the prefix is omitted.)

A correctly signed HTTP request may look like this:

  POST https://localhost:443/api/items/
  content-type: application/json
  content-length: 90
  date: Tue, 20 Apr 2016 18:48:24 GMT
  authorization: api-key SAMPLE_API_KEY
  signature: simple-hmac-auth sha256 64b0a4bd0cbb45c5b2fe8b1e4a15419b6018a9a90eb19046247af6a9e8896bd3

Signature

To calculate the signature, the client first needs to create a string representation of the request. When the server receives an authenticated request it computes the the signature and compares it with the signature provided by the client. Therefore, the client must create a string representation of the request in the exact same way as the server. This is called "canonicalization."

The format of a canonical representation of a request is:

  HTTP Verb + \n
  URI + \n
  Canonical query string + \n
  Canonically formatted signed headers + \n
  Hashed body payload

The canonical representations of these elements are as follows

Component Format Example
HTTP Verb upperCase(verb) POST, GET or DELETE
URI encode(uri) /items/test%20item
Query String encode(paramA) + '=' + encode(valueA) + '&' + encode(paramB) + '=' + encode(valueB) paramA=valueA&paramB=value%20B
Headers lowerCase(keyA) + ':' + trim(valueA) + '\n' + lowerCase(keyB) + ':' + trim(valueB) keyA:valueA
keyB:value%20B
Hashed payload hex(hash('sha256', bodyData)) ...

The HTTP verb must be upper case. The URI should be url-encoded. The query string elements should be alphabetically sorted. The header keys must all be lower case (as per RFC-2616) and alphabetically sorted. The only headers included in the signature should be: authorization and date- however content-length and content-type should be included if the HTTP body is not empty. The last line of the request string should be a hex representation of a SHA256 hash of the request body. If there is no request body, it should be the hash of an empty string.

Programmatically:

  upperCase(method) + \n
  path + \n
  encode(paramA) + '=' + escape(valueA) + '&' + escape(paramB) + '=' + escape(valueB) + \n
  lowerCase(headerKeyA) + ':' + trim(headerValueA) + \n + lowerCase(headerKeyB) + ':' + trim(headerKeyB) + \n
  hex(hash('sha256', bodyData)) + \n

For Example

  POST
  /items/test
  paramA=valueA&paraB=value%20B
  authorization: api-key SAMPLE_API_KEY
  content-length:15
  content-type: application/json
  date:Tue, 20 Apr 2016 18:48:24 GMT
  8eb2e35250a66c65d981393c74cead26a66c33c54c4d4a327c31d3e5f08b9e1b

Then the HMAC signature of the entire request is generated by signing it with the secret, as a hex representation:

const signature = hex(hmacSha256(secret, requestString))

That value is then sent as the contents of the signature header along with the algorithm used to generate it, as well as the version of the protocol the signature implements.

headers[signature] = 'simple-hmac-auth sha256 ' + signature

Usage

Reference implementation libraries for both servers and clients are included.

Server

The server implementation is a class that takes a request object and body data, signs the request, and compares the signature to the one sent by the client. If the signature is not valid, it will throw an error with an explanation as to why.

Middleware implementations for both Express and Koa exist in their own repositories.

Example

First, instantiate the class.

const SimpleHMACAuth = require('simple-hmac-auth');

const auth = new SimpleHMACAuth.Server();

The class requires a secretForKey function that returns the secret for a specified API key, if one exists. This function may return a value, execute a callback, or return a promise.

Assuming a secretForAPIKey objects exists, the three following implementations are all valid.

// Return
auth.secretForKey = (apiKey) => {
  return secretForAPIKey[apiKey];
}

// Callback
auth.secretForKey = (apiKey, callback) => {
  callback(null, secretForAPIKey[apiKey]);
};

// Promise
auth.secretForKey = async (apiKey) => secretForAPIKey[apiKey];

Finally, create the server itself. Because the unparsed body must be hashed to authenticate a request, you must load the full body before calling authenticate().

http.createServer((request, response) => {

  let data = '';

  request.on('data', chunk => {
    data += chunk.toString();
  });

  request.on('end', async () => {

    try {

      const { apiKey, signature } = await auth.authenticate(request, data);

      console.log(`Authentication passed for request with API key "${apiKey}" and signature "${signature}".`);

      response.writeHead(200);
      response.end('200');

    } catch (error) {

      console.log(`  Authentication failed`, error);

      response.writeHead(401);
      response.end(error.message);
    }
  });
}).listen(8000);

Alternatively, Sending a boolean true as the 2nd parameter instead of the raw body instructs simple-hmac-auth to handle the body itself.

http.createServer((request, response) => {

  try {

      await auth.authenticate(request, true);

      response.writeHead(200);
      response.end('200');

    } catch (error) {

      response.writeHead(401);
      response.end(error.message);
    }
}).listen(8000);

Client

There are two ways to use the client class: directly, or by subclassing to make your own client. It supports using callbacks as well as promises, as well as serializing JavaScript objects as the query string or request.

Using Client

To point it to your service, instantiate it with your host, port, and if you've enabled SSL yet.

const SimpleHMACAuth = require('simple-hmac-auth');

const client = new SimpleHMACAuth.Client('API_KEY', 'SECRET', {
  host: 'localhost',
  port: 8000,
  ssl: false
});

Set up the request options

const options = {
  method: 'POST',
  path: '/items/',
  query: {
    string: 'string',
    boolean: true,
    number: 42,
    object: { populated: true },
    array: [ 1, 2, 3 ]
  },
  data: {
    string: 'string',
    boolean: true,
    number: 42,
    object: { populated: true },
    array: [ 1, 2, 3 ]
  }
};

To make a request, execute the .request() function. It returns a promise, but will execute a callback if provided with one.

Callback

client.request(options, (error, results) => {

  if (error) {
    console.error('Error:', error);
    return;
  }

  console.log(results);
});

Promise

client.request(options).then(results => {

  console.log(results);

}).catch(error => {

  console.log('Error:', error);
});

Async promise

try {

  const results = await client.request(options);

  console.log(results);

} catch (error) {

  console.log('Error:', error);
}

Subclassing Client

To write a client for your service, simply extend the class and add functions that match your API routes.

const SimpleHMACAuth = require('simple-hmac-auth');

class SampleClient extends SimpleHMACAuth.Client {

  constructor(apiKey, secret, settings) {
    super(apiKey, secret, settings);

    self.settings.host = 'api.example.com';
    self.settings.port = 443;
    self.settings.ssl = true;
  }

  create(data, callback) {
    return this.request({ method: 'POST', path: '/items/', data }, callback);
  }

  detail(id, parameters, callback) {
    return this.request({ method: 'GET', path: '/items/' + encodeURIComponent(id), query: parameters }, callback);
  }

  query(parameters, callback) {
    return this.request({ method: 'GET', path: '/items/', query: parameters }, callback);
  }

  update(id, data, callback) {
    return this.request({ method: 'POST', path: '/items/' + encodeURIComponent(id), data }, callback);
  }

  delete(id, callback) {
    return this.request({ method: 'DELETE', path: '/items/' + encodeURIComponent(id) }, callback);
  }
}

module.exports = SampleClient;

Because this client's constructor specified the host, port, and SSL status of the service, it can be instantiated with just apiKey and secret.

const client = new SampleClient(apiKey, secret);

Just like its parent class, this example subclass implements both promises and callbacks.

try {

  const results = await client.query({ test: true });

  console.log(results);

} catch (error) {

  console.log('Error:', error);
}
client.query({ test: true }, (error, results) => {

  if (error) {
    console.log('Error:', error);
    return;
  }

  console.log(results);
});

Additional Implementations

Middleware for Express and Koa that leverage the implementation in this client exist in their own repositories. Compatible clients for iOS and PHP have also been implemented.

License

MIT © Jesse Youngblood

simple-hmac-auth's People

Contributors

dependabot[bot] avatar jessety avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

simple-hmac-auth's Issues

'date' header stripped for fetch request?

Hi. I'm using Chrome Version 87.0.4280.88 (Official Build) (x86_64) and simple-hmac-auth v3.3.1.

For the pre-flight request, my server returns the following response headers (note 'date' included here in access-control-allow-headers):

access-control-allow-headers: authorization,content-type,signature,date
access-control-allow-methods: GET,HEAD,OPTIONS,POST,PUT
access-control-allow-origin: http://www.myfakedevhost.com:3000
connection: keep-alive
date: Thu, 31 Dec 2020 03:41:51 GMT
transfer-encoding: chunked

Then, oddly,date isn't included as part of the actual request headers, despite the fact that I've stepped through the code and see that date is explicitly being added as a header to the request.

simple-hmac-auth seems to be doing its job, setting the date header and making the HTTP request as expected, however either the http library or Chrome is stripping it.

Any thoughts on what gives here? Is Chrome filtering it out for some reason (some CORS misunderstanding I might be having)?

Additional data below. Thanks for any help.

			const authClient = new SimpleHMACAuth.Client(
				user.webApiKey(),
				user.webApiSecret(), {
				  host: 'myfakedevhost.com',
				  port: 3000,
				  ssl: false,
				  verbose: true,
				}
			) 
			const options = { method: 'GET', path: '/some/api', query: null, data: { q: 1 } }
			authClient.request(options, (error, result) => onProcessResult(error, result))

Angular HttpClient module dosent allow to send date as header

Beneath $http it uses XmlHttpRequest and XmlHttpRequest isn't allowed to set the Date header as per the standard.

Step 5 states:

Terminate these steps if header is a case-insensitive match for one of the following headers...

and Date is included in that list.

find more details here

I have made changes in the code and changed date gheader to something like requestdate, this works totally fine for me.

I would request you to change date header with some other meaning full word.

'npm run build' failing

Just sync'd latest code as of dcec74f via git clone https://github.com/jessety/simple-hmac-auth.git and there are some errors for me locally, although workflows/actions are succeeding for your repo. Any idea what might be different on my local machine? Thanks.

➜  simple-hmac-auth git:(main) npm run build

> [email protected] build
> npm run clean && tsc


> [email protected] clean
> rimraf lib

src/Client.ts:269:86 - error TS2571: Object is of type 'unknown'.

269             const error = new ExtendedError(`Could not serialize parameter ${key}: ${e.message}`);
                                                                                         ~

src/Client.ts:271:13 - error TS2322: Type 'unknown' is not assignable to type 'string | undefined'.

271             error.details = e;
                ~~~~~~~~~~~~~

src/Client.ts:306:80 - error TS2571: Object is of type 'unknown'.

306             const error = new ExtendedError(`Could not serialize input data: ${e.message}`);
                                                                                   ~

src/Client.ts:308:13 - error TS2322: Type 'unknown' is not assignable to type 'string | undefined'.

308             error.details = e;
                ~~~~~~~~~~~~~

src/Server.ts:130:11 - error TS2571: Object is of type 'unknown'.

130       if (error.code === undefined) {
              ~~~~~

src/Server.ts:131:9 - error TS2571: Object is of type 'unknown'.

131         error.code = 'INTERNAL_ERROR_SECRET_DISCOVERY';
            ~~~~~


Found 6 errors in 2 files.

Errors  Files
     4  src/Client.ts:269
     2  src/Server.ts:130

Posting non string data hangs execution

Hello there @jessety a valuable tool you've made here
I'm using this with the express wrapper and unfortunately a Post request with non null non string data just hangs. I think there may be an issue with the parsing pipeline.

The example in the express wrapper for instance, fails with a the sample post request as it just hangs

Webpack < 5 used to include polyfills for node.js core modules by default

Hello,

The library is no longer working with react-script@5x.

ERROR in ./node_modules/simple-hmac-auth/lib/Client.js 16:31-46
Module not found: Error: Can't resolve 'http' in '.../node_modules/simple-hmac-auth/lib'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "http": require.resolve("stream-http") }'
        - install 'stream-http'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "http": false }

There is any fix scheduled? Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.