mgomes / api_auth

HMAC authentication for Rails and HTTP Clients
MIT License
480 stars 148 forks source link

Any compatible javascript clients? #52

Open raels opened 9 years ago

raels commented 9 years ago

This gem looks cool, but I need to have JS clients access my API. Are there any specific JS libs that are known to work with ApiAuth?

clekstro commented 9 years ago

I've wondered the same thing. While I'm personally not aware of any, because this relies on the creation of a canonical string, I think that any library that follows the same signing algorithm would be compatible.

I think the larger issue is that each client would need access to their private key in order to sign their requests. Where is this stored? Local storage? A cookie? Those things are (likely) ephemeral, meaning the user could easily lock themselves out of your app.

I've been leaning more towards OAuth2 with a username/password flow instead for this very reason, as it's more in line with a typical authentication flow and doesn't require the user to permanently safeguard their token. Should they lose/delete the OAuth2 token, they'd just have to log in again. One could probably implement a similar workflow with this approach, but it doesn't seem as straightforward as OAuth2. Also, combining that with 3rd party authentication tokens might make things even more complex...

Would be happy to get someone else's opinion on this, though.

mgomes commented 9 years ago

There are JS HMAC implementations so one could technically implement the entire algorithm in JS, but there is the issue of the private key. If your implementation uses a separate private key per user, then you could render the private key in the DOM. If you're using JQuery, you would then add the api_auth authorization header to each request. I'm sure other JS frameworks allow you to do the same.

The only catch is to make sure the user that now has access to that private is allowed to see it. If, for example, your private key was account specific and each account had many users with various levels of permissions, you wouldn't want to render that private key in the page.

mhuggins commented 9 years ago

I've got a similar question as to whether there are clients that will use this scheme in other languages as well (Java, Python, C++, etc.). Is this a custom approach, or one that has a common client implementation?

bbhoss commented 9 years ago

What about the date? You can't set the date in an XHR, and this adds one for you. If you don't know the date, you can't create a proper signature.

swaathi commented 8 years ago

I'm facing the same problem. I did a quick search and this came up, https://www.thoughtworks.com/mingle/docs/configuring_hmac_authentication.html. Apparently Thoughtworks also uses this gem, and they've explained how to get around it without library support. I haven't tried it yet, but it looks pretty decent.

disordinary commented 8 years ago

https://www.npmjs.com/package/api_auth

You'd have to use browserify. Older browsers also have appalling crypto support so I don't know how well it will work - but it's worth a shot..

Spone commented 7 years ago

Concerning the date issue mentioned by @bbhoss, I manage to circumvent it by using another header (X-Date) in my javascript XHR call, and overriding on the server-side the Date header before calling ApiAuth.authentic?

# When the client cannot set the `Date` HTTP header (for instance XHR),
# we use `X-Date` instead
if !request.headers.include? 'Date' and request.headers.include? 'X-Date'
  request.headers['Date'] = request.headers['X-Date']
end
ctrlaltdylan commented 7 years ago

I have made a Postman pre-request script to HMAC auth Postman requests, should be easy enough to convert it to node/browser js -

https://gist.github.com/ctrlaltdylan/dd75426527d424bc7a4e93bc6a52ea95

DaKaZ commented 5 years ago

For those that are looking for it, here is our JS api which works with api-auth:

import HMACSHA1 from 'hmacsha1';
import md5 from 'js-md5';
import axios from 'axios';
import Config from './config';

function InvalidToken(message) {
  this.message = message;
  this.name = 'InvalidToken';
}

function InvalidMethod(message) {
  this.message = message;
  this.name = 'InvalidMethod';
}

const baseHeaders = {
  'Content-Type': 'application/json',
  Accept: 'application/json',
  'X-Date': new Date().toUTCString()
};

const signHeaders = (opts) => {
  const headers = { ...baseHeaders };
  const options = { ...opts };
  if (options.data) {
    if (typeof options.data !== 'string') {
      options.data = JSON.stringify(options.data);
    }
    headers['Content-MD5'] = md5.base64(options.data);
  }

  const canonicalString = `${options.method.toUpperCase()},${headers['Content-Type']},${headers[
    'Content-MD5'
  ] || ''},${options.uri},${headers['X-Date']}`;
  // console.log('canonical string: ', canonicalString);
  const signature = HMACSHA1(options.secretKey, canonicalString);
  headers.Authorization = `APIAuth ${options.id}:${signature}`;
  // console.log('Token:', options.secretKey);
  // console.log('Authorization', headers.Authorization);
  return headers;
};

const api = (auth, uri, method = 'GET', data = null) => {
  if (!(auth && auth.apiToken && auth.apiToken.token)) {
    throw new InvalidToken('Authorization token is invalid or expired');
  }
  const fullURI = `${Config().apiBase}${uri}`;
  const url = `${Config().apiHost}${fullURI}`;
  const instance = axios.create({
    headers: signHeaders({
      method,
      data,
      uri: fullURI,
      secretKey: auth.apiToken.token,
      id: auth.user.id
    }),
    timeout: 60000
  });

  switch (method.toUpperCase()) {
    case 'GET':
      return instance.get(url);
    case 'PUT':
      return instance.put(url, data);
    case 'PATCH':
      return instance.patch(url, data);
    case 'POST':
      return instance.post(url, data);
    case 'DELETE':
      return instance.delete(url);
    default:
      throw new InvalidMethod(`Invalid Method: ${method.toUpperCase()}`);
  }
};

export default api;

Because you need the date/time stamp as part of the calc and CANNOT set that in a browser, we use X-Date and then on the server side have this in our API base controller:

  def modify_headers
    # Browser based XHR requests cannot override the Date header nor can they
    # determine the Date header value before sending the request: making it impossible to
    # generate a reliable HMAC-SHA1 digest. FODlink clients set an X-Date header which is
    # used to generate the digest. When present, we overwrite the Date header with the
    # X-Date header to ensure that HMAC-SHA1 digest can be matched.
    request.headers["Date"] = request.headers["X-Date"] if request.headers["X-Date"].present?
    # on get requests the content-type seems to be striped by Rails
    request.headers["Content-Type"] ||= (request.headers["X-Content-Type"] || "application/json")
  end