100

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?

tjans
  • 1,503
  • 2
  • 16
  • 26
  • *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 Mar 29 '11 at 13:56
  • 1
    Since you asked almost 2 years ago: what did you eventually use yourself? – Arjan Jan 10 '13 at 21:45
  • 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 Jan 24 '13 at 15:30

6 Answers6

65

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:

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

  3. 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.

  4. 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.)

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

    var apiConfig = apiConfig || {};
    if(document.domain === 'example.com'
          || document.domain === 'www.example.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.

  6. 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.)

  7. 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.

Arjan
  • 22,808
  • 11
  • 61
  • 71
  • 1
    When using CORS, *maybe* one can safely extend that. Instead of the above, when handling a pre-flighted `OPTIONS` request with some public API key in the URL, the server might tell a browser which domains are allowed (or cancel the request). Beware though that [some requests do not require a pre-flighted request, or won't use CORS at all](http://www.w3.org/TR/cors/#security), and that CORS needs IE8+. If some Flash fallback is used for IE7, then *maybe* some dynamic `crossdomain.xml` can help to achieve the same for that. We've not tried CORS/Flash yet. – Arjan Jan 08 '13 at 21:18
  • Great answer. But I don't think it works for Stateless REST calls. You can correct me, if wrong. – Madhur Bhaiya Nov 21 '20 at 03:56
  • 1
    @MadhurBhaiya that depends on your definition of state. I'd say that the signed CSRF token and cookie are indeed state, but: the validation only relies on a valid signature, and does _not_ need any state on the server side. (Also, this answer is 8 years old and IE8 is dead. If your use case needs CORS and does not need CSRF, then the CORS approach from my first comment above may be much easier to implement. But, without state you may not be using any CORS either?) – Arjan Nov 21 '20 at 12:27
23

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.

James.Xu
  • 8,249
  • 5
  • 25
  • 36
  • 10
    So then if it's just the sign that's passed...isn't that still exposed in javascript...so if I put a flicker photo on my webpage via their API (called by javascript), and you visit my page, aren't I exposing my API key to anyone who visits my page? – tjans Mar 29 '11 at 13:38
  • if we put the api secret in the javascript file which everyone can have access to, then it is not "secret" anymore, isn't it? will anyone do that? – James.Xu Mar 29 '11 at 13:58
  • 6
    I don't think I'm asking my question correctly...probably part of the reason I wasn't finding what I was looking for in the first place. when i make my ajax call, say using jquery, I'd have to embed the api key in the ajax call so that it gets passed to the server...at that point someone can see the API key. If I'm understanding that wrong, how does the API key get sent with the request if its not embedded into the client script? – tjans Mar 29 '11 at 15:14
  • api key can be sent with request, but secret will not be sent with request. api key and api secret are two things. what api are you looking at? it might be helpful if you give a link to the api description. – James.Xu Mar 29 '11 at 17:07
  • My apologies, I was going off of memory (poorly, at that, so it seems). Perhaps I need to look at that old code again and start there. The goal of this whole question is to learn how to setup some basic security/authentication for some rest services I was going to write (strictly for educational purposes and personal growth). – tjans Mar 29 '11 at 18:51
  • 4
    to conclude: people will be assigned an apikey + apisecret pair before using an openapi/restapi, apikey + sign will be transfered to serverside to make sure the server know who is making the request, the apisecret will never be transfered to the serverside for security. – James.Xu Mar 30 '11 at 01:27
  • It's perfectly possible to use a public API key without any server side signing, if you're willing to limit it to specific domains. That, of course, is some additional work while registering for such API key. – Arjan Dec 15 '12 at 09:51
  • So, let's say i want to generate a sign request and send ajax from javascript. To generate a sign request, I need the api secret. However, the api secret is on my backend. How can I do it? – Chamnap Feb 07 '13 at 02:34
  • @Chamnap, you can't. (Any pre-calculated signature you pass to the JavaScript code, could easily also be fetched by others.) – Arjan Feb 22 '13 at 14:19
  • 8
    So @James.Xu' 's statement that 'secret is used to generate a sign of current request' is FALSE! Because the client doesn't know the secret, because it would be unsafe to send it to him (and how else would he know that?) The 'secret' which is technically a 'private key' is used ONLY BY THE SERVER (because nobody else knows it) to generate a sign to be compared to the client's sign. So the question: What kind of data is being combined with the 'api key' that nobody else knows beyond the client and the server? Sign = api_key + what? – ACs Mar 20 '14 at 13:20
  • 2
    You're right, @ACs. Even if both servers (the website and the 3rd party API) know the same secret, one cannot calculate some signature on the website server and then put *that result* in the HTML/JavaScript, and then make the browser pass it along to the API. Doing so, *any* other server could request that HTML from the first web server, get the signature out of the response, and use that in the HTML on their own website. (I really think the above post does not answer the question about how a *public API key in the HTML* can be safe.) – Arjan Apr 20 '14 at 08:58
  • Did we arrive at any solution ? In my scenario , I am consuming an api via Azure api management server and i don't have clue on protecting the api key (subscription key) for my mobile clients. The mobile app I am developing doesnt have any authentication and its open to all. – Matt Jul 04 '16 at 06:43
12

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.
chris
  • 6,653
  • 6
  • 41
  • 54
  • True, but by design your answer does *not* expose the API key. However, in some APIs the API key *is* publicly visible, and that's what the question was about: *"requests to a rest service operation [...] made through javascript (XHR/Ajax)"*. (The accepted answer is wrong about that too, I feel; your point 2 is clear about that, good.) – Arjan Oct 17 '14 at 23:25
3

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.

MG Developer
  • 859
  • 11
  • 17
  • I agree with this, Javascript should not call another REST API directly, it should have its own thin layer as backend to call another REST API – sendon1982 Nov 16 '20 at 05:20
1

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.

Iain Porter
  • 101
  • 1
  • 4
  • 1
    But how can there be any login when *a third party* uses your API? If *the user* is going to login, then things are easy: just use a session? But when other *websites* need to authenticate to your API, that doesn't help. (Also, this smells a lot like promoting your blog.) – Arjan May 08 '13 at 18:06
1

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.

PeterMmm
  • 24,152
  • 13
  • 73
  • 111
  • 8
    I actually did mean the key. If I remember correctly, to use an API, you're passing the API key and secret to the rest service to authenticate, correct? I know once it gets passed over the wire it'd be encrypted by SSL, but before it gets sent, that's perfectly visible by the client code that uses it... – tjans Mar 29 '11 at 13:01