Closed RalfEggert closed 8 years ago
Okay, so as you had answer my issue, I'm answering yours :).
We are currently doing a project that is based on Shopify e-commerce platform. The authentication is actually pretty basic, and the authorization is too (basically, the only thing we need is make sure that the authenticated shop is the right one).
What we've done is creating two pre_routing middlewares that we have piped to the path "/api", so those are executed only for the /api endpoints that require authentication.
The first one is "ApiAuthenticationMiddleware". It reads the access token in the Authorization header, find the shop (in our context, shop is equivalent to the logged user). If we find a valid shop, we store it as part of the "shopify.shop" request attribute (or "logged_user").
Then, we have a second pre_routing middleware called "ApiAuthorizationMiddleware". This middleware only check if the shopify.shop is not null. If it is, it throws a 403 exception.
Interestingly, it got a bit more complex: for some very specific API endpoints, we needed to make sure that there was public (so it MUST NOT throws a 403 exception even if no one is authenticated). In order to achieve that, I've created an "AppOptions" class with an "unauthenticatedRoutes" array, that I fill with route names that do not require authentication.
I've modified the ApiAuthorizationMiddleware to include the RouterInterface instance, and actually do a first route match on the pre_routing middleware (so yes, we actually have match method executed twice, but FastRoute is fast and I think it can caches results):
/**
* {@inheritDoc}
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
$match = $this->router->match($request);
$unauthenticatedRoutes = $this->appOptions->getUnauthenticatedRoutes();
// If the request is an API request and is not in the unauthenticated whitelist, we MUST make sure the request has the "Authorization" header of just the shop domain
if ($this->isApiRequest($match)
&& !in_array($match->getMatchedRouteName(), $unauthenticatedRoutes, true)
&& !$request->hasHeader('Authorization')
) {
throw new RuntimeException('You are not allowed to access this resource', 403);
}
return $out($request, $response);
}
Interestingly, the middlewares open a lot of simplicity. In Shopify app we have actually two different authentication mechanisms: the first one is used to authenticate the initial request coming from Shopify, while all API requests need to be authenticated using our own mechanism.
By segregating by path, we were able to attach our API authentication/authorization to the "/api" path, and specific Shopify authentication to the "/shopify" path.
Of course, this may be different from your application, as in Shopify application development, there is no advanced authorization mechanism (basically, everyone installing the app have all permissions of a given app). I'd suggest that you actually use a Rbac library (ZfcRbac is not really compatible with Expressive but I definitely want to port this to middleware logic), and do that kind of checks in your actions middleware:
public function __invoke($request, $response)
{
$user = $request->getAttribute('user');
if (!$this->rbac->hasPermission($user, 'article.edit')) {
throw Exception();
}
}
Thanks for your explanations. So you added two pre_routing
middlewares to a single path? And all your requests go through this path? How did you do the config for that?
Well, I already have two dozens routes so far with about four different leading paths like /user, /whatever, and so on. Having to add my AuthorizationMiddleware
to each route sucks and if someone forgets to add the AuthorizationMiddleware
to a new route than there will be no authorization.
Running the router match()
method twice sounds interesting. But looks like an overhead. I am using the zend router and have no idea for the caching of the route result.
Throwing an exception for unauthorized users is an interesting idea but the catching gets tricky. I guess the Zend\Expressive\FinalHandler
will handle this but that would mean to overwrite the TemplatedErrorHandler
for production and the WhoopsErrorHandler
for development.
Exactly.
Well, I set it up this way:
'middleware_pipeline' => [
'pre_routing' => [
[
'path' => '/api',
'middleware' => [
ApiAuthenticationMiddleware::class,
ApiAuthorizationMiddleware::class
]
]
]
]
Mmhh throwing an exception for unauthorized user sounds to me the only solution, if you have specific permission per route. How would you do it otherwise? In my case I can do it very generic way and don't care (as the only permission if allowed / not allowed), but for other use cases, having more granular permission is essential.
Well, after further investigation,there will be no need to overwrite the WhoopsErrorHandler
since it will display the exception as wanted. For production the TemplatedErrorHandler
could be enough since it displays an error page as well. I just need to tweak the error.phtml
a little to display nice forbidden message and an optional login form.
Closed
Nice one! Throwing an exception with exception code 403 results in a 403 HTTP status code
@RalfEggert there's another place you can do it now: between routing and dispatch. This allows you to have authentication and/or authorization checks only if the matched route is one that needs it, which can save you some cycles when you don't. The approach would be the same as @bakura10 suggests, with one change: you'd check the route result for the matched route name prior to deciding whether or not to check authentication or authorization.
I'd couple this with an additional piece of functionality: an error middleware that looks for a specific exception type and/or code. This allows you to then provide an alternate response in such conditions. As an example, on my site, I have an Unauthorized
error middleware that checks for a specific exception type, and provides a login screen.
@weierophinney
Thanks for the further comments. I already moved by Auth middleware between routing and dispatch. Adding an Unauthorized error middleware sounds great. Need to play with it.
have someone an authorization , authentication sample code for an API?
I wonder when and where is the best place to do the authorization check depending on the routing. I already have authenticated a user and know its role or the user is not logged in so he has the guest role. Depending on the routing I want to check, if the current user is allowed to access this route. If not he should be redirected to a login page (if not logged in) or to a forbidden page (if logged in).
My first idea was to add a middleware to the
pre_routing
middleware pipeline. But these middlewares are always executed before the routing took place. So, it is not a good idea.Then I thought of adding my authorization middleware to each route before the right middleware for this route will be accessed. That would work, but would mean that I need to add this middleware to almost each route.
I also thought of implementing the
RouteResultObserverInterface
to get informed directly after the routing. But that won't help neither in thepre_routing
nor in thepost_routing
middleware pipeline path.Then I thought of an abstract middleware class which all my middleware classes need to extend and add the authorization check there. But that would feel bad somehow. Having similar as the controller plugins for middleware actions would be helpful.
So, how and where is the best practice to handle the authorization? Has anyone implemented it yet and can push me in the right direction?