-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add JWT support #89
Comments
First, my disclosure: I'm coming from the 'old school' RESTful background. I haven't looked at CraftQL in depth so this may be a little short sighted. Issue Note: Once a JWT token is created, its in the wild for use until expiration or another mechanism for invalidation. My guess is you'll want to send the current token and re-issue a new token on each SPA action ... similar to a standard page load w/ CSRF. Your front end will be responsible for storing the latest token from the last response. Consume I won't get into everything that happens upon consumption, but when it's all said and done, you should have a 'logged in' user (accessing the identity via Authorization You could register and assign users to native Craft user groups/permissions. Depending on the complexity, you could roll your own RBAC style (I believe this is on the Craft roadmap too). I can provide more resources on RBAC too. Please don't get lazy w/ security. I've seen the use a global authorization / access token (IE: you're using sending the same token, code, obscure string, as I). Don't do it. It's garbage. No exceptions; we have modern, secure frameworks at our disposal for this. |
Thanks for jumping in @nateiler! Some questions…
Wouldn't your JWT be encrypted with secret key, so you'd be able to trust it?
How would you envision non-logged (guest) in access to work? E.g. I need the SPA client to be able to have access, to the API, but doesn't login credentials like a user. Would the SPA client still have to request a token up front, same as a user? |
I was incorrect. The body could be tampered with, but on the token verification side this SHOULD be caught via the signature verification process. There a couple forms of JWT: A JWE type token would be encrypted and the the contents are unknown. Contents are unknown to the end user A JWS type token is not encrypted and the contents could be viewed. It's up to consumer to verify the signature which usually contains the body. |
Thanks for all this info! I'm still a little hazy on the benefit of JWT over just a "token" in the traditional sense? Right now: you generate a token in CraftQL. That token can have permissions applied so it can only query and not mutate, for example. Or, can only query one entry type not all entry types. You can have any many of these tokens as you'd like and they're all random so they shouldn't be "guessable." JWT: would allow me to encode some information within the token, but I'm not sure what information I would even need, other than the account information. But isn't that account information the token, as it's currently written? So, what benefit would JWT provide? I could encrypt the query, itself, as it's sent over? I see the advantages of JWT but I'm just not seeing how they could be practically included in to CraftQL, yet. I'd love to discuss this more though because it does seem like something I could add in without too much trouble (once I get my head around the benefits). |
Mark, the point you made about issuing a token for non-mutation actions is pretty harmless. While the tokens are random, and not easy to guess the outcome is security thru obscurity. All you would need is some user to send a tweet with a token and then the entire world could use it and access the same data. Rare, but it could happen. A JWT token contains an identity in the body. It's issued from your web app and (in this case) validated/verified by your web app. If a security concern raises, you can address it on a per-use bases instead of potentially re-issuing a new token to all consumers. Mutations should really be cautious of this as one tech savvy user could exploit this (and you wouldn't be able to pinpoint who did it). In the case of CraftQL, the request would include a JWT authorization header along with the a standard GraphQL request/data. On the server side, you would grab that Authorization header, validate the token and establish an identity. From this point forward, all actions (service requests) are taken on behalf of the identity provided ... so |
I think I’ve got my head around this now. Originally I was assuming that the client would create the token and send it over to the server, which would then be validated, and accepted or rejected by the server. I guess that flow could work, but for CraftQL it would be more realistic to have CraftQL generate the token and have the client store it and send it back with each request. This article helped me, too, Under the current flow a user creates a token in the Craft UI and copies/pastes it. Is this okay for your specific use case or do you need a sign in route that a user can enter Craft credentials and be granted a JWT? |
@markhuot right, in my case, I need to have the token returned from a sign-in. |
Here's the part I'm not quite clear on: So if the token is encrypted with a secret, that means only the server (Craft in this case) can know the secret (obv. if the client had the secret, it would be exposed). What that also seems to imply is that the client can never use the
UPDATE Ok I think I finally understand (mostly). What my above confusion was missing was the fact the JWT body itself is not encrypted, so the client can receive the token and freely read the body data w/o the secret key. Only the server needs the secret to verify and issue new tokens. SO - I think the only remaining gray area is how the server returns the new/refreshed tokens it creates. I was thinking this would be standardized in a header it is for requests ( |
I wasn’t thinking of including anything important in the body of the token (since it's passed in the clear). So, for the following example: “user logs in and is presented with a dashboard of their content” the flow would be,
The {
authorize(username: String!, password: String!) { #returns a User object
token
id
groups {
id
name
}
}
} |
@timkelty Correct. A token that is signed only, doesn't have an encrypted body. The signature is technically hashed, not encrypted, using the algo identified in the header of the token. Since you're issuing it to yourself, this doesn't really matter much. The signature will probably be handled via the JWT library. You'll need to provide a secret. Craft/Yii have a Security component which may help with this. @markhuot I would say that CraftQL doesn't need to support JWT authorization natively. There are numerous ways that one may want to authorize with your API. My assumption is, all incoming API requests run through a single controller; if that's the case you could implement a behavior event (similar to https://github.com/craftcms/cms/blob/c0532b799d48e45b614f85466cc85db11bd54891/src/base/Component.php#L33) which would give devs the ability to get creative. |
That makes sense @nateiler, I may take that approach in the short term. I would like to allow CraftQL to handle authorization at some point so you could (in theory) spin up a SPA with Craft and CraftQL without writing any PHP code. |
That might be more of a long term approach. You (or another developer) could write a 'JWT Authorization for CraftQL' plugin which registers itself with that behavior to provide the authorization. Still click administration, for some end users; but super flexible for edge cases. Similar idea to registering an OAuth provider, payment gateway, etc. One could also handle access control via controller behaviors as well. |
I'm open to beta test this.. :) |
It's happening (but not even close to done yet…). |
I have a very early pass of this over on the First, ask for a token, {
authorize(username:"foobar" password:"foobar") {
token
user {
...userFields
}
}
} The In order to implement this I had to add CraftQL permissions to the user permission system. That means you'll need to check your user and for anyone other than an admin add the correct CraftQL permissions. That should look like this, Right now the user tokens do not expire. I'd like to offer that as a setting, but haven't gotten to that yet. Please let me know if any of this seems usable for your use cases or if there's something missing. |
This is ready to merge. I'm going to do a bit more testing on it, but it'll probably be in |
I'll try and take a look this/next week too! Excited! |
Has anyone had a chance to test this out yet? |
@markhuot tragically, no. I SWEAR I'm getting to it early this week though 😀 |
No worries! I just wanted to make sure I wasn't holding anyone up. I'm using this on |
Regarding the expiry of tokens. I was working on an own solution but maybe switch to this project if JWT Tokens are fully implemented. Form my experience it could work something like this (i'currently using firebase/php-jwt):
|
@jan-thoma, this is implemented almost as you describe. Each JWT token has an A request does not automatically increase that expiration… although I like that idea. Currently you would need to do the following: query (username: String!, password: String!) {
authorize(username:$username, password:$password) {
token
}
} That would get you the initial token. Then to refresh it you'd need to, query(token: String!) {
token: refresh(token:$token)
} That would get you a new token with an updated What I like about this is you don't have to keep swapping out tokens on every request. However, I get the feeling this isn't the way to do JWT. Do you have any examples of where JWT is implemented and uses a constantly updating |
This here was my approach, the code is heavy alpha stage but it might helps:
|
Yup, seems similar to https://github.com/markhuot/craftql/blob/user-tokens/src/Types/Query.php#L362-L373. Also with I'll look in to the rolling expiration times since it shouldn't affect backwards compatibility and removes the need to keep making a |
It would be great then to have the option to create an application wide key. With the same scope options as an user has which not expires, to access everything that is considered public. |
This branch doesn't forego the existing Token system that is non-user-based. So, you should still be able to create a token and manage the permissions down to what you need. That token (as is currently the case) will never expire. In short there will be two token types moving forward:
|
I've added in support for rolling JWT tokens. Basically the initial request would still be the same, query (username: String!, password: String!) {
authorize(username:$username, password:$password) {
token
}
} That'll get you back a
The change, is that when CraftQL responds it'll send an
I did some digging and don't really see a standard header name for CraftQL to respond with so |
Following to RFC 6750 which describes 0AuthWhen sending the access token in the "Authorization" request header For example:
So using the "Authorization: Bearer" in the request and the response should follow the specs in the closest manner. |
Thanks @jan-thoma. I updated this to use the However, I'm not seeing anything in RFC 6750 that indicates this should be the case for responses. The text you copied seems to be discussing the request instead. Nonetheless, it seems okay to sync the two up. |
@markhuot finally digging into this, and it seems just what I was hoping for! 👏 To clarify: If you're sending all your requests with your updated token, you shouldn't need to manually call For my uses, for this to be useful, I need a bit more info in the JWT body, namely which member groups the user is in. Obviously different applications will need different jwt body specs…is it possible to expose this via event hook, so it can be customized? |
once you have the token you just can call your userdata via the api. i think you shouldn't expose as less as possible userdata in the token because anybody can decode the payload. |
@timkelty, I'm worried about using the JWT body for this since GraphQL actually supports this sort of multidimensional retrieval on any request. For example, if I added a {
entries: entries(limit: 5) {
id
title
url
}
user: me {
id
groups {
id
name
}
}
} Like you say, the benefit here is that you have more control over the data you're requesting and it's not as public. What do you think? |
@markhuot yep, that seems like a good solution to me. |
@markhuot been working on integrating this, and came across my first hurdle: Apparently, to be able to inspect the Adding Another option might be to not bother with responding the token in the Authorization header response, and instead require the request to query for it to get a new one? Then you don't have to worry about standardizing on a response header |
Any update on this one? I think the only remaining issues I'm having are:
|
Can we have an example on how to implement this? |
+1 for this. I'm holding off on until #103 closes out |
@markhuot Hey Mark, do you have any updates on this? JWT authentication is exactly what I need and I would much rather use your product than have to roll my own API. |
@markhuot I have locally tested the user-tokens branch, seems to work, but lacks proper permissions. On Entries:
Currently it only supports viewing all OR only viewing my own. |
I have created a PR into user-tokens to support individual entry type permissions. |
I have a project where I need to implement a GraphQL api, using JWT for access and authentication.
Still working out the best way to get this should work, but starting this ticket just to get some things down.
For my use-case, I'm looking at two types of access:
Authentication: bearer
headerIn both cases, the body of the JWT would be the user info/access/permission levels.
For authenticated access, it seems the tricky part is syncing/refreshing the expiration of the token. It seems like you'd have to refresh the expiration of the token with every subsequent request somehow.
There are some low-level JWT methods here:
These articles have made the most sense to me:
Would love any input from @nateiler as well!
The text was updated successfully, but these errors were encountered: