Thanks for the clarification Evan.
That was actually my first approach: Getting a macaroon with a caveat with all the allowed routes (for each HTTP method), and then verify that the requested route exists inside the caveat.
The flow was: whenever a user logs in, the auth server would send one macaroon for each HTTP verb that had at least one authorized route for that user, and some expiry time of course. The idea was that the user could have access to e.g. GET method on routes=[/restricted1, /restricted2] and also POST to routes=[/restricted2].
The problem I had was when I tried to implement this with a satisfyGeneral like you suggested, is that the current implementation (at least in macaroons.js) only passes the caveat as a parameter, and there's no way to pass the current request context to the verifier.
So if I want to pass the current "req.path" value so that I can search for it in the routes=[...] caveat, I would need to define the satisfyGeneral method inside the scope of the verification routine so that the variables are accessible.
i.e. (this is an Express middleware):
function verify_macaroons(req, res, next) {
verifier.satisfyGeneral(function(caveat){
// verify that the requested resource
// (req.path) exists inside the
// provided macaroon
var authorizedRoutes = parseRoutes(caveat);
if (authorizedRoutes.indexOf(req.path) > -1){
//is authorized
}
else{
//unauthorized
}
});
};
I could also modify the source code of the library so that it takes a context variable that I can use to verify the macaroon, but when I started down this path I realized that maybe I was doing something wrong. That's why I thought of asking about the correct way of implementing a disjunction.
I mean in terms of code it would be pretty easy to add a param to the satisfyGeneral method to take the context, i.e.
verifier.satisfyGeneral(caveat, context){
...
}
And so far it seems like it would be in line with the rest of the paper, but I am not really sure. What do you think? it could be a pretty useful addition to increase the functionality of satisfyGeneral, right?
Whatever the answer is on how to actually implement this check, I think it seems definitely doable, but I would prefer to make meaningful changes to the library instead of just to accommodate my use case.
If it indeed is the case that we can use satisfyGeneral, then I can see basically that the two approaches have several differences with interesting tradeoffs.
In the first approach, the process would be something like this:
- The user contacts the auth service, sends username and password, and in turn gets a macaroon for each of the HTTP verbs that can be used to request a resource in the API. So if the user has low-level access, maybe only one macaroon is sent, for the routes that can be requested via GET. An admin user in turn would probably get all four macaroons (GET, POST, PUT, DELETE) with the authorized routes where they can be used.
- The corresponding "access macaroon" will be sent with each request, the API can then verify said macaroon and grant or deny access.
This would be mostly like using cookies like Robert pointed out, except that the added macaroon functionality could be used for enhanced functionality later on. E.g. adding a third-party caveat that needs to be discharged via a 2FA service.
The other problem is that since we need to provide a user/password to get these macaroons, we would probably need a longer expiry time. I don't think its a very pleasant user experience to ask for the user and password frequently, so the expiry time would need to be maybe 1 or 2 days? maybe longer?. This in turn has certain side-effects like having a longer attack window and policy updates won't be propagated instantly.
On the other hand, if I understand correctly, Robert's suggestion would boil down to something like this.
- Each resource has an access macaroon associated with it, and these macaroons have a third-party caveat that needs to be discharged by the auth service.
- When a user needs to interact with a resource (endpoint, instance, etc) and doesn't have the access macaroon that authorizes said interaction, the server responds with an access macaroon that needs to have the TP-caveat dispatched to grant access.
Then, two things can be done to get the discharge macaroon:
- The user contacts the auth service, and if the correct password is sent, the auth service responds with a discharge macaroon that proves who the user is and what route is he authorized to use. The problem is this would need to be done for every request, which is unfeasible.
The alternative:
- Add *two* third party caveats to the access macaroon. One for the user and one for the authorized method and route (i.e. the access policy).
The user would then need to:
- Contact the auth service to get a dispatch macaroon for the user part (this one can have a long-ish expiry time, say 2 days).
- Then contact the "policy service", and request a discharge macaroon for the needed HTTP method for the specified resource. We use the previous discharge macaroon that proves we are the correct user, which the policy service uses to determine if the user has access to the requested resource and issues a "policy discharge macaroon". This macaroon can have a short expiry of say, 5 seconds, so that every request would require a fresh discharge macaroon, increasing the probability to defend against a MITM, since like Robert said, at most the attack would be able to do a replay attack inside that 5 sec window only.
- With both discharge macaroons in hand, we take the original "access macaroon", bind both DMs to it, and finally send it along with the request to the resource we wanted to interact with.
That way an attacker would need to get a hold of both the user discharge and the policy discharge macaroons if I understand correctly.
This is a bit more complex and would require, depending on the expiry time of the "policy discharge macaroons", several API requests per each effective resource request, which might be a valid trade off if you care about the increased security a short lived macaroon gives you. Otherwise maybe the first approach is simpler and even though it might have a longer attack window, maybe in particular use cases this is not a problem. Then again the first approach is going back to cookies pretty much, so not sure if it's worth it.
Am I missing something? are my assumptions sane or am I just making this more complicated than it needs to be?
Maybe there's a third way I'm not seeing here?
In any case, I appreciate any feedback!
Akram