DEV Community

ImTheDeveloper
ImTheDeveloper

Posted on

Critique My Plan: API Key for Authentication

I'm getting quite involved in a web browser game to take my mind away from my dev work for a few hours. It appears the community playing this game are big fans of user scripts, allowing them to change certain aspects of the UI. Many of the community also host their own servers which allow them to organise and add structure outside of the game.

As a true dev at heart, I've also become one of those people. Hosting my own additional systems and producing my own scripts to augment the in-game experience with new data.

I'm looking into safe methods of passing data from my own server, via a user script to the users' browser once they are logged into the game. A live link between their browser and my system tools will allow for context related and personalised data to be injected into their game, such as live stats.

Since I only have the option of a user script, there are no open APIs for this game I am thinking of authenticating the user by the usage of a user API key which they generate from my system (once logged in) and then once they are logged into the game I can request this key via a form to authenticate their browser for any subsequent calls to my API endpoints.

There are a few questions I have and to be honest I absolutely love doing work like this as it allows me to gain knowledge in areas I take for granted in my day to day work. I've listed them below and it would be great for others to chip in with their thoughts and experiences.

API Key Generation

I did a little research and my initial thought of just using a UUID as an API key brought up some interesting conversations from StackExchange. UUID generation can sometimes have too little entropy, which means to ensure an attacker does not just cycle through generating and trying these ID's I should also add some crypto on top of this. I've seen many approaches of hashing, adding salt, base64 encoding and then I stumbled upon this https://www.npmjs.com/package/generate-safe-id Am I right in thinking this is the only consideration I need to make?

API Key vs API Secret

This is quite an open-ended question. I've noticed when dealing with certain APIs in the real world they often show an API Key and an API Secret. What is the purpose of the differentiation between the two?

API Key Usage / Policies

I do not plan on storing any information within my API Key, it is purely a random string without clashes (I hope) that I can run through my middleware policies to look up the user making the request and checking their access levels/look up their profile from such key. Are there any better approaches here? I wanted something lightweight and the usage of JWT with meta-data seems like overkill to me.

API Key Storage (Client side)

Where does it make sense to store the API Key on the client side? Since I am effectively injecting in code via my user script I have the option to store the key in a cookie or local storage. Which is better for persistence or which is typically best practice?

API Key Storage (Server side)

I plan to allow the user to login to my system and locate their profile page where they can generate their unique API Key. I will then store that key against their profile, with the option for them to re-generate (revoke the old) if they so wish. As an admin, I could re-generate / remove keys as I wish also. For the storage of API keys, I'm thinking it is perfectly fine for me to just store "as-is" within my DB, without any extra hashing. The reason being, if an attacker has access to the database then I have bigger problems, but also I wanted to keep this lightweight and any deciphering during my middleware policy checks will just add overhead to the request being made. Does this sound like a sensible approach?

Authentication vs. Authorisation

Dealing with authentication of a users browser is one thing, but you can't ignore authorisation. As mentioned I will use the API key to authenticate the user against a profile stored in my database. During the middleware policy check, the user profile will be retrieved and I will then check to see if they have an access level of "member" or above. For those who have a key which is not associated with a profile and for those requests which return a profile with an access below "member", I will just send a 403 forbidden response back to the request. As far as I can see, this should satisfy my authorisation needs, but are there other practices/checks I should be making? I'm thinking along the lines of browser fingerprinting to check if the key is being used by a browser that initially saved the key to its storage.

Latest comments (14)

Collapse
 
endorama profile image
Edoardo Tenani

Hey Chris, what is the game you are referring to? Seems fun XD

Thanks!

Collapse
 
antonfrattaroli profile image
Anton Frattaroli

If it's not terribly important I'd have an interface to auto-hardcode the generated key when they copy the script from your server (having no idea what game or how the "user scripts" feature works in it). Don't need to store it then. Assuming also the user scripts are secured behind an authentication wall.

I'd pass the key in the "Authorization" header, just raw. Gotta support CORS limited to the game's domain, which shouldn't be too difficult since most backend stacks have a switch/config for it.

I would use this, for example... if I wanted to stream iHeartRadio and store a default station per user to start playing when the game loads (I did this recently with a Discord bot for funsies, they have a nice API).

Collapse
 
imthedeveloper profile image
ImTheDeveloper

I ended up going with a JWT and encoding the user id into it. This works good as I've left bare minimum information in there and I just use this ID when calls to my APIs occur to run the user ID through a policy on the route which checks their access level. I can then accept or deny plus attach in their user profile if I want to then use it further down stream. Revoking access is nice and simple this way too as each member has an access level I can switch and just block them at the policy.

Interesting you are using discord bots. I've built a few telegram bots now it's definitely a lot of fun and oddly satisfying running commands through a messenger!

Collapse
 
sebringj profile image
Jason Sebring • Edited

I've done something where on authentication, I create a secret that is passed only a single time that both client and server share then a GUID is created and passed around as the "public key". Each request, wherever it originates, client or server, the client or the server signs the request using the private and public "keys" (secret and guid), passes the guid in the request and then the receiving end checks the hash. This is a pretty common approach for oauth and amazon etc. Authorization can be passed around as long as it is part of the signature but I recommend just keep a lookup on the server for that as you don't need to broadcast more info than necessary. I use node so I get to use crypto on both sides. This is pretty secure because the signature is also signed with a timestamp and passed around plain as well so you both check how old the request is directly before having to check the signature, then only check the signature if its not too old to verify it matches. You could also store on the server the last timestamp of the request for that user and have a rule that it can't repeat.

Collapse
 
keithmo profile image
Keith MšŸš²re

Regarding UUID generation, the node-uuid NPM package uses node's built-in crypto primitives to generate random V4 uuids. The package's random number generate is located here: github.com/kelektiv/node-uuid/blob...

Collapse
 
tom profile image
tom • Edited

Disclaimer: this isnā€™t my specialism but Iā€™ve been working on my security chops and took this opportunity to dive deep into this particular area. I hope someone more experienced & knowledgable can correct me if Iā€™ve made mistakes!

You have some options here, and you also can dial your choices up based on how private the data you are storing is. It also depends on what you control and what is in the control of the game.

If you control the almost all the code on the client side ā€” that is, you can configure how requests are made to your API ā€” then you can use something relatively simple.

The words are overloaded, but I think the right nomenclature here is that you will give each user a token. Keys tend to used for signing and encrypting, but tokens carry an assertion of identity and capability that you will need to verify.

From what I understand, each user will have a username and password for your service, and theyā€™ll need to log in before using your service from the game.

If so, you should be able to use a token written to an HTTP-only, secure cookie. Youā€™d write the cookie when the user logs in to your service. When making requests from the game to your service, youā€™d use CORS with credentials enabled so that the cookies are included (developer.mozilla.org/en-US/docs/W...).

With this kind of design, the tokens must be protected from disclosure in storage and in transport. Youā€™ll be relying on the browser for the former (same-origin policy) and HTTPS for the latter.

This is similar to the OAuth2 Bearer spec (tools.ietf.org/html/rfc6750) and the security recommendations from that spec hold true (tools.ietf.org/html/rfc6750#sectio...).

JSON web tokens (jwt.io/) are a pretty good standard for tokens, and you have the support of existing libraries for generation and verification.

The most important bit of the JWT is the secret, which will be the same for all of your tokens.

Your best bet for the secret is randomness from a cryptographically strong (pseudo-random) source: openssl is good for this (openssl rand -base64 32).

API Key vs API Secret

As I understand it, this is distinction is usually because an authentication protocol requires client-side cryptography, as in the OAuth1 specification (which is quite readable: oauth.net/core/1.0a).

Client-side cryptography is nearly always necessary but if youā€™re using HTTPS (and you should be) then itā€™s hidden away from you. OAuth1 was designed for an HTTPS-less world, so itā€™s all built into the protocol.

For your use-case I donā€™t believe you will need to provide API keys and secrets. It should be enough store tokens, users, roles, and a way to relate each to the others.

Token usage & policies

Your idea here is sensible, although keep in mind that, if youā€™re using JWT, you can encode the capabilities of the user in the token.

My only suggestion is that, if you want many-to-many relationships ā€” many users with many different combinations of roles ā€” that you keep the canonical list of roles separate from your user storage, and keep some kind of relation between the two.

Client-side storage

As mentioned above, I suggest a cookie, associated with the origin of your server (which must be secured with TLS). Since youā€™re operating in a relatively hostile environment ā€” lots of user scripts ā€” I suggest also making it HTTP-only, meaning that it cannot be read by JavaScript. You are relying on the same-origin policy in the browser to enforce that the key isnā€™t exposed to third-parties.

The auth0 docs are good for the pros & cons of various approaches to this: auth0.com/docs/security/store-tokens

Server-side storage

You wonā€™t be storing the tokens server-side, but you will need to the secret in order to verify the token.

I suggest that you store it away from your user data, probably not even in a database, and you supply it to processes that need it via their environment. Cloud providers normally have tools for doing this (e.g. cloud.google.com/kms/docs/secret-m...).

Notes

Since you mentioned them: hashing and salting are used for storing information that must remain private even if the storage is compromised and that doesnā€™t need to be retrieved later but does need to be verified (like a password).

Base64 encoding is just a way of representing some underlying bytes. In this case, the library you link to (npmjs.com/package/generate-safe-id) uses a URL-safe base64 encoding of the random bytes it generates. Base64 is not a kind of encryption or hashing; itā€™s equivalent to plain-text and reversible, though of course you may base64 encode something thatā€™s already encrypted or hashed.

My suggestion assumes that the game has a relatively permissive content-security policy (developer.mozilla.org/en-US/docs/W...) that will allow connections to your serviceā€™s origin.

This design is also not great if you intend to support other games, or if you have less control over the client-side that I have assumed.

(Season's greetings!)

Collapse
 
iyedb profile image
iyedb

The cookie cannot be http only if you want to use it with CORS (which implies js)

Collapse
 
tom profile image
tom • Edited

Why so?

As I understand it, HttpOnly just means the cookie can't be read by JavaScript; the browser can still send it along with a cross-origin request.

Thread Thread
 
iyedb profile image
iyedb

But how would you send a cross origin request without js? A cross origin request is basically a xhr request to different server (not only the domain but even the port number makes it different btw) than the one serving the current page. If you mark the cookies as http only you can of course still send a cors request but you don't have access to the cookies so you can't send then along with your cors request.

Thread Thread
 
tom profile image
tom • Edited

What's missing from this picture is that the browser can read, and send, cookies even if JavaScript can't.

That's why HttpOnly cookies exists: they are only available within the HTTP request.

So, a cross-origin HTTP request made using an XMLHttpRequest (or the fetch API) can include cookies that the JavaScript itself can't read. This is referred to as "credentials".

With XMLHttpRequest:

// from https://a.com
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://b.com/c.json', true);
xhr.withCredentials = true;
xhr.send();

and fetch:

fetch('https://b.com/c.json', {
  credentials: 'include'
})

Hopefully that makes sense!

By the way, this system (cross-origin, secure, HTTP-only cookies) is how TweetDeck and Twitter Lite's authentication works against the Twitter API.

Thread Thread
 
iyedb profile image
iyedb

Makes perfect sense. But it's not possible to configure that request by for example adding a authorization header with the value of a cookie.

Thread Thread
 
tom profile image
tom

Good point!

Collapse
 
imthedeveloper profile image
ImTheDeveloper • Edited

Hi Tom,
Firstly thank you for the detailed response and efforts put into answering all of my questions. The time taken is appreciated!

To answer some of your assumptions made. I do not control the game code at all, however I can I inject anything post page render using my user script. This will allow me to control the way in which my API is accessed from the client, however I should probably make the assumption that anyone can see that code and make a request using whatever client e.g. postman.

I think it's interesting you mentioned JWT. I'm going to do some reading up on that vs oAuth2.

I think at the moment I'm toying with a key design decision:

  1. In the short term do I allow a user to generate a token from their user profile in my system (not the game) and they just then enter this in an injected form I place in the game. This can then be stored as a cookie with http only enabled as you suggested. The benefit here is mainly that I remove a bit of coding on my side for some aspects and also allows the user to revoke the token or regen quite easy. I see this route as being the easy/lazy route.

  2. Do I inject a form into the game where the user can enter their user/pass for my system tools, which in turn returns the packaged token and saves it within an encrypted cookie with http only again. This feels a bit more user friendly but also a bit more overhead on my implementation. I'm wondering since I have only the user script to access data (via javascript) whether I can even access a http only cookie. I assume you need to read the cookie to send it in the subsequent API calls? Or do you send the cookie in the request and the server reads it?

Alternatively I just store it in the web storage.

jwt flow
Source: auth0.com/docs/jwt

I've actually just read up on jwt some more on auth0 and it's offered some great validation of your reply. I think I'll go with generating a JWT and then have this stored on the client in their local storage. I'll create a new API route for login/creation of the JWT in my sails.js app and then build out a policy that does the checks against the supplied JWT. I can then just drop this policy in against my user script routes since I generate the javascript on the fly that is injected into the game pages.

Collapse
 
tom profile image
tom • Edited

I should probably make the assumption that anyone can see that code and make a request using whatever client e.g. postman.

Absolutely make this assumption. Itā€™s that which makes me suggest using an HTTP-only, secure cookie: it defends it from code that knows what itā€™s looking for and where to look.

I think it's interesting you mentioned JWT. I'm going to do some reading up on that vs oAuth2.

This isnā€™t so much of a ā€œvsā€ as two separate (complimentary) ideas. JWT is just a standard for creating and validating tokens. OAuth2 is a set of authentication protocols.

JWT is a totally logical choice for a token within an OAuth2 flow.

Itā€™s not explicitly mentioned in the JWT docs but the recommended use of a JWT is in the Authorisation header, which is the same recommendation as in the OAuth2 Bearer spec (tools.ietf.org/html/rfc6750#sectio...).

Iā€™m toying with a key design decisionā€¦

I wonā€™t suggest anything specific but if thereā€™s going to be a login form for the service, and an embedded login form, could they be the same form? (think iframes, CSP)

I'm wondering since I have only the user script to access data (via javascript) whether I can even access a http only cookie. I assume you need to read the cookie to send it in the subsequent API calls? Or do you send the cookie in the request and the server reads it?

The first step is setting the cookie from the server, using the Set-Cookie header (developer.mozilla.org/en-US/docs/W...).

After that, the browser does all this for you when you either (1) set withCredentials = true on the XMLHttpRequest (developer.mozilla.org/en-US/docs/W...) or (2) set credentials: 'include' if youā€™re using the fetch API (developer.mozilla.org/en-US/docs/W...).

It slurps up all the cookies for the origin itā€™s talking to and sends them along with the request automatically.

You donā€™t need your client side to be able read the cookie at all.

One other thing you could consider is writing a cookie that indicates that the user is logged in that can be read from JavaScript: that letā€™s you make decisions in your client-side code that donā€™t require ā€œcheckingā€ with your API.

And I strongly suggest you donā€™t use local storage: you will have to write it against the origin of the game which makes it accessible to any and all other user scripts running.