99
votes

I've been reading up on REST and there are a lot of questions on SO about it, as well as on a lot of other sites and blogs. Though I've never seen this specific question asked...for some reason, I can't wrap my mind around this concept...

If I'm building a RESTful API, and I want to secure it, one of the methods I've seen is to use a security token. When I've used other APIs, there's been a token and a shared secret...makes sense. What I don't understand is, requests to a rest service operation are being made through javascript (XHR/Ajax), what is to prevent someone from sniffing that out with something simple like FireBug (or "view source" in the browser) and copying the API key, and then impersonating that person using the key and secret?

6
one of the methods I've seen is to use a security token, there are really a lot of methods out there. Have you a concret example. I may think you get confused with "REST" vs. "make available a javascript API for registered users only" (ex google maps).PeterMmm
Since you asked almost 2 years ago: what did you eventually use yourself?Arjan
I didn't actually use anything, I was more just trying to wrap my head around creating the concepts. PeterMmm's comment above is probably true...still haven't had a need to implement any of this, but I wanted to better myself...thanks for following up.tjans

6 Answers

65
votes

We're exposing an API that partners can only use on domains that they have registered with us. Its content is partly public (but preferably only to be shown on the domains we know), but is mostly private to our users. So:

  • To determine what is shown, our user must be logged in with us, but this is handled separately.

  • To determine where the data is shown, a public API key is used to limit access to domains we know, and above all to ensure the private user data is not vulnerable to CSRF.

This API key is indeed visible to anyone, we do not authenticate our partner in any other way, and we don't need REFERER. Still, it is secure:

  1. When our get-csrf-token.js?apiKey=abc123 is requested:

    1. Look up the key abc123 in the database and get a list of valid domains for that key.

    2. Look for the CSRF validation cookie. If it does not exist, generate a secure random value and put it in a HTTP-only session cookie. If the cookie did exist, get the existing random value.

    3. Create a CSRF token from the API key and the random value from the cookie, and sign it. (Rather than keeping a list of tokens on the server, we're signing the values. Both values will be readable in the signed token, that's fine.)

    4. Set the response to not be cached, add the cookie, and return a script like:

      var apiConfig = apiConfig || {};
      if(document.domain === 'expected-domain.com' 
            || document.domain === 'www.expected-domain.com') {
      
          apiConfig.csrfToken = 'API key, random value, signature';
      
          // Invoke a callback if the partner wants us to
          if(typeof apiConfig.fnInit !== 'undefined') {
              apiConfig.fnInit();
          }
      } else {
          alert('This site is not authorised for this API key.');
      }
      

    Notes:

    • The above does not prevent a server side script from faking a request, but only ensures that the domain matches if requested by a browser.

    • The same origin policy for JavaScript ensures that a browser cannot use XHR (Ajax) to load and then inspect the JavaScript source. Instead, a regular browser can only load it using <script src="https://our-api.com/get-csrf-token.js?apiKey=abc123"> (or a dynamic equivalent), and will then run the code. Of course, your server should not support Cross-Origin Resource Sharing nor JSONP for the generated JavaScript.

    • A browser script can change the value of document.domain before loading the above script. But the same origin policy only allows for shortening the domain by removing prefixes, like rewriting subdomain.example.com to just example.com, or myblog.wordpress.com to wordpress.com, or in some browsers even bbc.co.uk to co.uk.

    • If the JavaScript file is fetched using some server side script then the server will also get the cookie. However, a third party server cannot make a user’s browser associate that cookie to our domain. Hence, a CSRF token and validation cookie that have been fetched using a server side script, can only be used by subsequent server side calls, not in a browser. However, such server side calls will never include the user cookie, and hence can only fetch public data. This is the same data a server side script could scrape from the partner's website directly.

  2. When a user logs in, set some user cookie in whatever way you like. (The user might already have logged in before the JavaScript was requested.)

  3. All subsequent API requests to the server (including GET and JSONP requests) must include the CSRF token, the CSRF validation cookie, and (if logged on) the user cookie. The server can now determine if the request is to be trusted:

    1. The presence of a valid CSRF token ensures the JavaScript was loaded from the expected domain, if loaded by a browser.

    2. The presence of the CSRF token without the validation cookie indicates forgery.

    3. The presence of both the CSRF token and the CSRF validation cookie does not ensure anything: this could either be a forged server side request, or a valid request from a browser. (It could not be a request from a browser made from an unsupported domain.)

    4. The presence of the user cookie ensures the user is logged on, but does not ensure the user is a member of the given partner, nor that the user is viewing the correct website.

    5. The presence of the user cookie without the CSRF validation cookie indicates forgery.

    6. The presence of the user cookie ensures the current request is made through a browser. (Assuming a user would not enter their credentials on an unknown website, and assuming we don’t care about users using their own credentials to make some server side request.) If we also have the CSRF validation cookie, then that CSRF validation cookie was also received using a browser. Next, if we also have a CSRF token with a valid signature, and the random number in the CSRF validation cookie matches the one in that CSRF token, then the JavaScript for that token was also received during that very same earlier request during which the CSRF cookie was set, hence also using a browser. This then also implies the above JavaScript code was executed before the token was set, and that at that time the domain was valid for the given API key.

      So: the server can now safely use the API key from the signed token.

    7. If at any point the server does not trust the request, then a 403 Forbidden is returned. The widget can respond to that by showing a warning to the user.

It's not required to sign the CSRF validation cookie, as we're comparing it to the signed CSRF token. Not signing the cookie makes each HTTP request shorter, and the server validation a bit faster.

The generated CSRF token is valid indefinitely, but only in combination with the validation cookie, so effectively until the browser is closed.

We could limit the lifetime of the token's signature. We could delete the CSRF validation cookie when the user logs out, to meet the OWASP recommendation. And to not share the per-user random number between multiple partners, one could add the API key to the cookie name. But even then one cannot easily refresh the CSRF validation cookie when a new token is requested, as users might be browsing the same site in multiple windows, sharing a single cookie (which, when refreshing, would be updated in all windows, after which the JavaScript token in the other windows would no longer match that single cookie).

For those who use OAuth, see also OAuth and Client-Side Widgets, from which I got the JavaScript idea. For server side use of the API, in which we cannot rely on the JavaScript code to limit the domain, we're using secret keys instead of the public API keys.

23
votes

api secret is not passed explicitly, secret is used to generate a sign of current request, at the server side, the server generate the sign following the same process, if the two sign matches, then the request is authenticated successfully -- so only the sign is passed through the request, not the secret.

11
votes

This question has an accepted answer but just to clarify, shared secret authentication works like this:

  1. Client has public key, this can be shared with anyone, doesn't matter, so you can embed it in javascript. This is used to identify the user on the server.
  2. Server has secret key and this secret MUST be protected. Therefore, shared key authentication requires that you can protect your secret key. So a public javascript client that connects directly to another service is not possible because you need a server middleman to protect the secret.
  3. Server signs request using some algorithm that includes the secret key (the secret key is sort of like a salt) and preferably a timestamp then sends the request to the service. The timestamp is to prevent "replay" attacks. A signature of a request is only valid for around n seconds. You can check that on the server by getting the timestamp header that should contain the value of the timestamp that was included in the signature. If that timestamp is expired, the request fails.
  4. The service gets the request which contains not only the signature but also all the fields that were signed in plain text.
  5. The service then signs the request in the same way using the shared secret key and compares the signatures.
3
votes

I will try to answer the the question in it's original context. So question is "Is the secret (API) key safe to be placed with in JavaScript.

In my opinion it is very unsafe as it defeats the purpose of authentication between the systems. Since the key will be exposed to the user, user may retrieve information he/she is not authorized to. Because in a typical rest communication authentication is only based on the API Key.

A solution in my opinion is that the JavaScript call essentially pass the request to an internal server component who is responsible from making a rest call. The internal server component let's say a Servlet will read the API key from a secured source such as permission based file system, insert into the HTTP header and make the external rest call.

I hope this helps.

1
votes

I supose you mean session key not API key. That problem is inherited from the http protocol and known as Session hijacking. The normal "workaround" is, as on any web site, to change to https.

To run the REST service secure you must enable https, and probably client authentification. But after all, this is beyond the REST idea. REST never talks about security.

1
votes

What you want to do on the server side is generate an expiring session id that is sent back to the client on login or signup. The client can then use that session id as a shared secret to sign subsequent requests.

The session id is only passed once and this MUST be over SSL.

See example here

Use a nonce and timestamp when signing the request to prevent session hijacking.