-
Notifications
You must be signed in to change notification settings - Fork 177
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
jwt-backend local but without db. Use rsa public-key for secret. #90
Conversation
…k and aclquery in regex-form to check acl's. The Secret-property should be stored as interface{} to interface better with ParseRSAPublicKeyFromPEM. Problem is that Secret is exported (why?), so this might cause problems in other uses. Other solution might be to convert the parsed RSA to a byte-sequence. Not sure how... For now a RSA is assumed. Would be nice to have some sort of auto-type-select for this.
He @pmous! Could you update the title and give a description of what's the PR intention, along with any relevant details for it? Thanks! |
Hi @iegomez I am not sure I understand what's wrong with the title. The PR adds a non-db option to the local mode of the jwt-backend. That is the first part of the title. The second part is there because I did a small addition to allow easy inclusion of a rsa-public file. But I will shorten the title, because it is now cut off. Perhaps I am missing something, so if you have another suggestion, please let me know. |
I came upon the following problem. The acl check is done for every publish and also for every listener apart. The plugin is sending the user (which is the jwt) to that call. In my code that jwt is validated and is expired at some point. But since the listener is passive, the jwt is not renewed. The client is completely unaware that starts missing messages, because mosquitto simply ignores that listener if the acl fails. And even for an active event, like publising, what should the client do? Disconnect?? There is no way the client can tell why the acl is disallowing the event: is it because I really have no rights, or because the token is expired? So... this brought me back to think about what OAuth is actually doing. It gives you a token that grants you access for a period of time to a certain resource. But the way mosquitto is communicating with the plugin makes it impossible to honour this: The only way to handle an expire is to hang up the connection and let the client reconnect (right?). This is not possible for the auth-plugin. So I think we have to bend the OAuth rules a bit. Let's interpret it like: the token grants you to connect to mqtt for a period of time. After that, you may stay connected for as long as you want. This is actually better for a client to handle. So this is what I suggest: On connect, the GetUser call is done. This checks the jwt with a public key and validates the expire. Then every acl check is only checking the jwt with the public key, but ignores the expire. I think this is reasonable safe, since the signing is protecting the acl checks pretty strongly. It is actually safer, since I can now set the expire very short, so the token is only valid for one connect-action, say 10 secs. This is enough time for the OAuth cycle and mqtt-connect after it. |
…e checked as well. For this to work I use the MapClaims now to use arbitrary fields and added an option jwt_acl_scope_field.
Hi @iegomez |
Hi, Paul. I've been a bit busy, but I'll review everything as soon as I get some time. |
@pmous Hey, Paul, I have some comments on your ideas. I don't think I fully understand how Now, regarding the expiration problem you hit, I have a few thoughts:
That's it for now, we can go through your other concerns like the secret's type to adjust for different keys, but please share your thoughts first and we'll go from there. Also, sorry for taking so much time to review and reply, just today I got to check your comments and glance over the code. I know it's far from ideal to have you wait so much, but this is not a light change I can respond to in a few minutes as some issues are, so it's hard to get the time to do it. Cheers and thanks again! |
Hi @iegomez |
Sounds like a plan. Talk to you soon! |
About that. For my application the whole thing works with only one rule. So it is not cumbersome at all. I use the regex to support as many schema's as possible. I assume that in most scenarios there is a clear relation between a topic and a user, so that a single rule can cover all users. But maybe I am wrong in that assumption. An example:
Which means: auth_opt_jwt_acl_scope_field is the field in the jwt that holds the requested scope. First alle %...% are replaced with requested scope, access and topic, and any field from the jwt, like sub in this example. In my app, sub is actually the user-id. Than the regex on the right side of the : is matched against the string on the left. So in my example, if the scope is either filter-extension or mqtt-extension, access is either subscribe or read, the topic myndr/preferences/ |
Yes, I can understand that you would say so. But, like I said, the whole idea of checking the expire once the socket is connected, is flawed in the first place. If the client side is listening, the server will simply stop sending updates to the client. There is no way the client can tell that it should renew the token, except if the client itself would keep track of the time, of course. But that is not supported in the lib I use and I really think it is the wrong place to do so anyway. So the question is: do we rely on the safety of a socket, once connected? I think we should. There are lots of applications, like chat for example, that do this: login to open a socket, then continue to use this socket for as long as you like. A socket is of course something else then a stateless website: For a website we need to make sure of the safety every single request.
Exactly my point: Since the plugin structure of Mosquitto is as it is, there is no way around it. I just said that IF Mosquitto would support a callback to force disconnect, that could also solve this problem. In a pretty brute manner, and I agree, very hacky, I just felt I should make clear how limited we are here...
Yes of course. Like I said in the reply just before, it was just to point out that there is not really a valid other solution in my opinion.
Yes, I agree. But if the flow should change, if the rejection should cause another action on the client, I think that we could make an exception in favour of a cleaner flow. But this would not solve the passive listener problem anyway, just the publish. So this discussion is a bit academic. |
Thanks for the explanation, now it's a lot clearer! It seems quite a clever and nice way of defining rules, though oddly specific. That is, I'm not sure anyone else would use this way of granting auth because it looks tied to your use case, which in turn hints it could possibly be better suited for the custom plugin backend: you can always change it later according to your domain requirements while keeping up to date with the plugin without needing your own fork. I could be wrong and others may be inclined to use it, but in that case once it's out then there's no adjusting to suit any later specific need you may have that would break compatibility, you're married to the original implementation and only non-breaking enhancements may be added.
Yeah, with one rule it's clean, but I don't think the assumption of a direct relation between topic and user is a given one. Just look at Mosquitto's static files auth where topics may make use of Thanks also for the discussion on expiration. I'm still inclined to say that expirations should be honored unless explicitly said not to, I don't believe it to be safe to just allow a listener whose token is no longer valid to keep getting messages. Not the best one for sure, but short expirations are used as a way of quickly revoking access to users that have been compromised: you may not be able to instantly kick the user out by invalidating the token, but at least you can deny them a new one and thus mitigate the situation by ensuring they are forbidden access once the current token expires. So though I understand your reasons, I do think you'd be covered by some Those are my thoughts, now you need to sell me this is a general use case and not just specific to your domain and we're on 😅. I kid but half joking only: adding this means maintenance, documentation, tests, etc., so adding domain specific functionality is something I try to avoid. Cheers! |
I understand. So if you are not merging my code, how can I use it without forking? This is not clear to me. Unfortunately the plugin-code is not generic enough to write a completely independent plugin backend, I think. But perhaps I misunderstood...
Actually, it is a good example. If it is true that most applications work like that, then I can see my code is indeed not suitable. But I guess that every flat-file solution is cumbersome if there are a lot of users. So, I can imagine that there are:
This is actually a good point: compromised users should be handled. Also in my app this is actually a thing: Our users can have a subscription that is expired and should then be inactive.
Ok. Sound good. Except you should call it something like jwt_skip_expiration_acl_checks, or something, because the other checks should keep check it! Very important!
I do understand. So like I said: lets just wait if others like the regex-acl checks. But I do hope you want to merge the jwt_skip_expiration_acl_checks-thing. How do we proceed for that? Should I write the code for this option and then do another merge-request without the regex-acl-code? I can also document this option for you. However, testing is for me not an option. I have very limited time, sorry. I don't know how much work it is for you to extend your tests with this one option? Thanks again, I do enjoy our discussion!
|
I believe you're thinking about writing an entirely new backend, but what I meant is that you can use the
Fair point! I'd still fall in the side of making it a file, even if you ever have one rule: the rule will look the same in the file and the
Yeah, i totally agree there's an intrinsic flaw in JWT tokens used as an auth method and expecting the plugin to be able to "handle" expirations properly because it simply can't, but on the other hand you don't want a connection gone rogue to stay alive. So sure, there are cases where letting it live is fine or can't really be handled and this would be a pain point, but that's resolved by having the
Absolutely!
Absolutely yes to adding the Finally, sorry if it seemed like I'm inclined to discard the contribution; as said, I find it very interesting and clever, but additions come in with a burden and is hard to justify a very targeted change for a general purpose plugin. I'm not sure how to get some other opinions, maybe asking in Chirpstack's forum or Mosquitto related ones? Thanks again for a very interesting discussion! |
No problem at all, I think I said already that I understand completely, and actually agree not to merge all my changes. Ok. I will pick up this task of creating an additional option for skipping the expire. I think I will copy the original code and put a big IF around it, and put my code in de ELSE. That way the diff will be small (in favour of rewriting your original code as well, what would give us less code). For the other option: I agree it would be nicer to put the regex-acls it in a file, leaving the option jwt_acl_query like it is, just for databases. Probably calling it jwt_acl_regex_file or something. Done that, I will let you know. Once I am working on it again, I will look for the best option: forking and merging your updates, using your plugin skeleton, or making the third mode (not remote nor database) on another way generic. But not right now! I am madly busy! Hope to pick this up in a couple of weeks or so. Cheers! |
Hi @iegomez, @pmous and @intolerance . I have a similar use case - in my case subscribing directly to topics from an angular app. I do like the idea of having a common syntax for mapping jwt claims to pub/sub/read topics, so I like the concept behind @pmous' work. If we could find common ground on this I'd be keen to help come up with a spec and implementation. I will admit though to having trouble understanding @pmous' proposed syntax - I think it would benefit from some case driven unit tests/and or examples - again I'd be happy to collaborate. |
@sirockin Could you elaborate on your Angular subscription needs? I first started this project because I needed to subscribe from a React app that served as a frontend for a Postgres backed application that used JWT tokens for auth. I was looking yesterday into ways of skipping the expiration time in the Anyway, please share any ideas you may have. Cheers! |
@iegomez thank you once again for responding so quickly to my comments and issues, and also for developing and maintaing such a useful project. My own use case is fairly simple: I have a set of users each of whom will use the Angular SPA to access and respond to real-time data on one or more sites defined by an array of IDs in their JWT claim. The data for each site is defined under the topic So of course I could implement this logic fairly easily by using the remote JWT backend but given that the number of checks which need to be done and the fact that all the necessary information to carry out the auth decision is contained in the claim, I thought it may be useful and possible to eliminate the overhead of the http calls. On the other hand, I get that any solution like that involves inventing a Domain Specific Language (or embedding an existing one like JavaScript) and this has its own pitfalls. |
Regarding the expiry time issue, if I understand correctly, the issue is that the Mosquitto plugin interface doesn't give us a way to feedback an expiry time or request a disconnection at any point. So our only option is to deny publish requests and read requests once the token has expired, leaving the client in silence but not disconnected - definitely not ideal. I don't know how flexible/inflexible the Mosquitto plugin interface is, but wouldn't the best thing be to submit a PR to allow a disconnection request to be fed back, say by specifying the expiry time when the user is first validated. I'm really surprised this issue hasn't been raised with them before given the widespread use of JWTs and expiry times. If that's not possible, I do think the default situation should be to honour the expiry time by refusing further reads/writes/subscriptions. I'd be happy to look into it some more - are you able to point me to any documentation on the Mosquitto plugin interface because so far I've had difficulty finding anything? |
@sirockin Thanks for sharing your thoughts, it's very rewarding an valuable to get that kind of feedback.
Those are my thoughts. I'll definitely try to come up with a solution to handle expiration to ease that pain point in the meantime, but will also consider my first point of making it easy to use JWT in a custom way. Cheers! |
Update: I tried a bunch of strategies for skipping the expiration in a sane way and I hated all of them (and JWT too, but not so sure if it's their fault or the library's). I guess I'm gonna go back to my idea of porting the plugin to a better language some day. 😪 So, at least for now, exposing JWT library for use in the custom plugin is my way to go unless anyone of you comes up with something. Cheers! |
Hi @sirockin, glad to hear my idea is supported! I am happy to give you help in how to use it. I did supply a rather complete explanation at my pull request: #90. Did you find that? Could you explain what part you don't understand, so I can update the explanation. So other people understand it too in the future. |
@iegomez thanks for you latest responses. First, regarding the issue of simplifying JWT claims, I've spent some time thinking about this over the weekend and it occurred to me there are potentially two overlapping objectives:
For point 1, I like most of what @pmous has done but would suggest (as he has already has) putting the claims logic in a file and keeping the syntax as close as possible to the existing ACL file syntax - perhaps it already is For point 2 - cases where the file ACL logic wouldn't be sufficient - (for example in my use case, which requires iterating an array of sites and producing an ACL for each of these) we could reduce the number of http calls made by allowing the return of a complete ACL in the response to the |
@iogomez on the question of jwt expiry, I think the priorities should be:
|
This sounds like a use case exactly where my proposal is for. In my syntax it would be something like: |
This is exactly my point.
That would indeed be the cleanest solution.
Right now, the default is to honour the expiry. And that results in a listening client to just be ignored. New publishes or subscribes would not be granted, of course, so those are no problem. But listening is on an existing socket, so there is no way the client can be informed other than simply disconnect the socket. Which would, again, be the best solution. |
Hi @pmous thanks for the further feedback. I'm going to try to find time to elaborate on the ACL syntax proposal in the next couple of days. In the mean time, I'll raise a new issue here and on the mosquitto repo, regarding JWT expiry. |
Regarding your syntax I think I understand it now having reread your example and explanation. So from your example above:
A claim such as However a claim such as It's a nice terse syntax but I do agree with @iegomez that it seems quite specific to your use case. It also may not be easy to understand or to explain - it took me a few reads and I can't think of a better way of explaining it than you have. In your second example, applied to the single-site version of my use case you've allowed the
As I said in an earlier post, I would prefer the logic to be closer to the existing ACL syntax so how about extending the
or even
For my (single-site) use case the file would read
I'll be interested to hear your thoughts! |
Correct!
In saying "you've allowed the scope_id claim ...", I think you look at this a bit too much as fields. The fact that I separated the left string with spaces is arbitrary. Think of it as left side of : is a string, right side is a regex. Super simple. So you decide what you like to match against what. So if you need to check the scope, just put it in. Once you understand just how generic it is (arbitrary string matched to regex), you will see that it is not specific to my use case. Like you see in your example, your use case is also simple. Except for the site_id. But that problem is also not solved in your proposal, as far as I can see?
I like the thought a lot. My syntax is grown simply from the easiest path to success. Yours is better for the community, I think. However, much much more work! You need to plug into the ACL parse-code (didn't even look at that yet) and write code for several parts of the plugin. Like I suggested earlier: First you need to support placeholders into the ACL syntax (or does that work already??), then you need to write some extension code for jwt-claims. But isn't the ACL syntax specific for the file-backend, I wonder? It is only used there. The database and jwt backends use their own logic... So I wonder if my though is true: first support overal, then expand for jwt. I think we don't need regex per se, we just might support delimiter-separated multiple values, like you suggest for "jwtclaim.scope mqtt-extension, filter-extension". We will get there! Nice work so far. |
@pmous thanks for the very positive feedback. I do agree that this would be harder from an implementation point of view, but hopefuly with judicious unit testing and refactoring, we can resuse a large part of the file-based ACL logic. Regarding my array use case, I think it's just too specific for this type of approach - the only possible way I could think of incorporating it would be to allow some kind of 'for each' statement in the ACL and I think that would be a departure too far from the core concepts used in this project. |
Well, perhaps not. I think some sort of multi-value claim is not so strange at all. For example, the scope is usually an array. |
Hey guys, I don't know what stupidity I was doing the other day, but hereś a PR for skipping expiration: #109. Now don't mind me, carry on. |
@iegomez @sirockin Good thing you found that. But, if I understand correctly, this option disables the check all together. That is really not the way to go: you want to keep using expire for user-check (connect/publish), since they don't suffer from the problems that the acl-check does. If I remember correctly, my proposal only disables it when needed: in checkAclLocal() I don't use getClaims(), but use &jwt_go.Parser{ Anyway, for me it is not really a problem right now. The local-jwt code is for me most important. The expire-problem is perhaps best solved in the client for now. If a better API to Mosquitto becomes available, as we discussed, then perhaps that method would be cleaner and more robust in the future. As long as local-jwt is not formalized and merged, I keep using my fork for the time being. Meanwhile we keep looking for a common solution, of course! |
@pmous - I'm glad we're thinking along similar lines. It would definitely be interesting to discuss extending the ACL syntax for an array but I agree lets keep the scope of this PR to the same as your original one. I may find some time to work on that. If I do, ould you be happy for me to commit to this PR or would you prefer I open a new one? @iegomez thanks for your replies (and also for dealing with #107 so quickly). What are your thoughts about extending the ACL syntax as @pmous and I have discussed. Do you think you would accept a PR along those lines? |
I'm not very experienced in sharing code this way, but I guess that depends on whether you would go from the master repo or fork from mine. Isn't it? As long as I am kept in the loop, I don't mind if you fork your own. Problem with my code is that the two issues, ACL-extend and expire-issue, are both in there... So perhaps quickest is to just fork from master, then manually diff with my version, if you like. But really that is up to you of course. I think, if I resume working on this, I would actually do the same :-) |
@pmous I could pass the option as an argument to @sirockin I said earlier in this convo that I'm not sold and I really don't wanna have to test and maintain custom syntax that I might even not fully understand. I get what @pmous says about not everyone being able to fill the custom plugin blanks, but on the other hand I can't really add every way of checking auth for whatever use cases people may come up with. Honestly, though I said I wasn't sure about embedding stuff, I'd prefer throwing a Javascript interpreter in there to lower the barrier and call it a day. That said, I haven't been following the discussion, too busy this week, so maybe you 2 come up with some beautiful solution and then I'll be happy to add it. |
@iegomez: I do though think there is still some mileage in allowing some jwt token substitutions into a file-based ACL. Maybe I'll expand on this in a new issue and we can discuss there. @pmous regarding the PR, agreed. I'll raise an issue first though so that @iegomez can respond |
BTW, you might be interested in the notes about Mosquitto's v2.0 release: https://projects.eclipse.org/projects/iot.mosquitto/releases/2.0 Also, check the |
OK, I finally read the whole discussion (and the related issues) and it's a lot more clear now, sorry for taking so long and keeping you hanging. As I mentioned in #113, this #116 PR will make it much easier and cleaner to add this feature. Of course, it's Friday and we all have different timezones, but would you be willing to have a call next week to talk about it? Let me know and we'll schedule a call to get it done. Cheers, guys, an thanks for your interest! |
No problem at all! We will get there!
I'm sorry, not quite clear who you want to talk to, me, or sirocking, or both? And what about exactly?
That is great news indeed! But again not quite sure what exactly, since this PR is about a couple of things. I assume you mean you now want so support an extended ACL?
|
@pmous Yup, I was referring to your extended ACL suggested addition, but let's just keep it async and I'll try to start implementing it next week, then you guys can tell me what's wrong or missing along the way. |
@pmous @sirockin It's been a while, some other issues came up, work, vacations, etc., so sorry for leaving this discussion hanging. Are you guys still interested in adding it? If so, a proper definition of the format to follow should be enough to implement it rather sooner than later this time. Let me know what's your opinion on this. Cheers! |
Hi guys, it has been a while for me too. Sorry about that. Lots to do, being the only developer at a startup (besides my bos, who has no time to develop). But he, not complaining, still love my job :-) Cheers! |
As with the original issue, I'm closing this for now to avoid noise, but feel free to open it again if needed. |
Hi @iegomez and @sirockin |
Thanks for all, @pmous, I enjoyed the talks as well. Best of lucks! Cheers! |
Btw, @pmous, I'm curious at what scale you'll be operating where Mosquitto (or is it the plugin the bottleneck?) is no longer viable. Was it the plugin, have you looked into Mosquitto's integrated auth since 2.0? Or is it not about throughput from the broker but due to other requirements? |
@iegomez Well, the plugin is not the problem. The situation is that for now Mosquitto is doing fine, but we are a startup that aims for world domination :-). The product relies for 100% on the correct working of mqtt. If our product grows, then hundreds and later hopefully thousands connections will be made. Mosquitto has pour support for clustering. And we are not sure that Mosquitto is reliable enough in general. Of course we are not addressing this growth right now, but it makes no sense to spend too much time in a product we know is not going to be sufficient. |
Gotcha! Thanks for sharing. 🙇♂️ |
I want to use the jwt-backend without remote calls, but also without a local database. This is possible, because the jwt is signed and can be checked with a public key. So I made a couple of changes:
jwt_aclquery contains a comma separated string with expressions. Each expression consists of a string to match with, then a column (:), then a regex to match against. Both string and regex can have placeholders.
The placeholders in the form %fieldname% are substituted first with as values the fields from the claims (short, lowercase variants, as they are in json). Additionally, %clientId%, %topic%, %access% and %scope% can be used. %access% is the requested access as string, so either read, write or subscribe. If access is readwrite, the test is done for write and read apart and AND'd. %scope% is taken from the claims, the field is set in config jwt_acl_scope_field. It is then split on spaces and for each %scope% is filled and the expression is evaluated. On the first match access is granted (so OR'd). If no scope is given, ["default"] is used.
Example:
auth_opt_jwt_aclquery %scope% %access% %topic%:read-scope read topic/%sub%,%scope% %access% %topic%:test-scope (read|write|subscribe) other/%clientId%/%sub%
auth_opt_jwt_acl_scope_field scope
The Secret-property should be stored as interface{} to interface better with ParseRSAPublicKeyFromPEM. Problem is that Secret is exported (why?), so this might cause problems in other uses. Other solution might be to convert the parsed RSA to a byte-sequence. Not sure how...
For now a RSA is assumed. Would be nice to have some sort of auto-type-select for this.