stashapp / metadata-api-discuss

This repo is the laziest possible way we can have threaded conversations about metadata collection and curation for StashApp
MIT License
6 stars 1 forks source link

Authorization flow proposal #7

Open ghost opened 4 years ago

ghost commented 4 years ago

For the POC I've been building I've been using JWTs to authorize requests, and I'd like to propose that we use that going forward as well.

For a quick primer regarding JWTs see https://jwt.io/introduction/, but the tl;dr: is that they're small json objects which are cryptographically signed and tamper-proof. The advantage to this is that rather than a random string they contain meaningful data that can trusted without having to do database lookups.

As an example, user example wants to log in to stashdb. The user inputs username/password which is sent to the backend. The backend fetches the database information related to that username, appends the salt for that user, and hashes it with bcrypt. The resulting hash is then compared to the hash saved in the database. Since the hash is a match, and the user has the userType admin, the backend creates a JWT containing the information: { username: 'example', userRole: 'admin' }. It then signs the token with the private key and sets the token in the users cookies.

The next time the user wants to load a page or request an object, the graphql library picks up the token from the cookie and sticks it in the authorization header. The backend sees the header and simply has to validate the signature with its public key. If the token is validated, the backend can trust that the request is valid, and that the data contained in the token is untampered. In other words, the backend knows the username and the usertype of the request, without having to do any database lookups.

This can also be used for API-keys if we introduce a third field for tokenID, which uniquely identifies the token. This id can be saved in the database for revocation/query limitation purposes. If the user wants to query with stash, all they have to do is go to stashdb, generate a new token, and stick it in stash. Stash can even validate the token itself, given that only a public key is required.

For the backend the authorization flow will be more or less exactly the same, it picks up the token from the header the exact same way, and simply has to look for the additional tokenID field to validate that it's a valid API-key, and not someone trying to sneak by with a cookie token. This will be crucial since we will want the frontend and api consumers to share the graphql schema, but have different authorization tokens to be able to rate limit and block api requests.

Here are a couple of jwt go libraries that can be used: https://github.com/dgrijalva/jwt-go https://github.com/gbrlsnchs/jwt

Leopere commented 4 years ago

Please be sure to use the Bernstein curves ed25519 or curv25519 when applicable and at least PBKDF2 for any containers. Other than this I think you have a good system. I like the idea of storing the users Private Key inside of a PBKDF2 container in our database that is then decrypted user side for use in creating authorization tokens and so on for API access.

WithoutPants commented 4 years ago

I'm ok with this, but I do have some queries and feedback.

I don't think we should include the role in the JWT - or we should at least verify that it is current - since the role can be changed after the user has logged in; the role value in the token may not necessarily be accurate. We'll want to be hitting the database anyway to ensure that the user still exists.

Are we including an expiry on the JWT login tokens (presumably the API keys should not include an expiry)? If so, how do we handle refreshing of the token?

How do we handle logging out of the system?

ghost commented 4 years ago

Yeah, excluding role makes sense.

Expiration date is up for debate, I haven't looked into it yet and don't have strong opinions. We can for instance either have a shorter expiration and refresh whenever the token expiration date is closer than a set threshold, or a longer expiration and simply let it expire and force the user to log in. We should probably research what the tradeoffs and best practices are.

Logout can be handled by deleting the cookie and hard redirecting the user to the root.

WithoutPants commented 4 years ago

Based on my reading, it appears the general view is that JWTs should not be used for session management. The most glaring weakness is the inability to revoke JWTs. Logging out deletes the cookie on the client end but does not revoke the JWT, meaning that someone else could continue to use the JWT after the user has logged out.

I think JWTs make the most sense for api keys. We can generate an API key and store it on the user table. The authorisation code can verify that the API key matches the user key. That way any old API keys are revoked if the user regenerates the key.

For the UI though, I think we're better off using an established session management library (such as gorilla/sessions for example). Sessions can be managed in the backend so that when the user does log out, that session is invalidated immediately. This also gives us flexibility to add idle timeouts and refreshing can be sorted out on the backend, rather than requiring the front-end to specifically request a refresh. Given this, JWTs don't sound like a good fit for UI session management.

Leopere commented 4 years ago

Can we have account-wide auth attempt limitations perhaps let the user determine the limit but require a limit?