I have set-up a MVC/PAC-like structure for a web application (unsure if it fits any of these design patterns fully). In short it is:
- Routing in
index.php
, which selects the controller and method using the URLhttp://example.com/controller/method/<params>
- Controller's method requests data from 'model(s)' and assigns it to a view.
Now I am wondering what is the best spot for an logged in check. Let's say I have a page at http://example.com/controller-one/method-one/
which requires the user to be a logged in administrator; Where do I check whether the user actually is? In the routing, controller or model?
Please note that a controller and/or model might contain methods with different 'rights'.
Note: There is a model called Authentication
which contains a method called isLogged()
that returns true or false based on whether the user is logged in and another method which is called IsLoggedAdmin()
which returns true or false based on whether the user is a logged in administrator.
So...: What is the best location to call the method isLogged
or isLoggedAdmin
. In the controller's contruct and/or method(s) or the model's contruct and/or method(s)?
3 Answers 3
Authentication should happen after routing but before calling controller or its methods.
At that point you know which route was requested and can check if user has privileges to perform a certain action (call controllers).
This allows not only to separate concerns, but also to decide how to handle unathorized requests before they hit controllers - eg. redirect them to other controller internally with 403 response.
Authentication/authorisation happens in higher layer than models. If they depend on user instances - you pass already authenticated/authorized instance. Also - when requirements change, and other roles are allowed to call previously prohibited methods - you change only ACLs and not models.
Symfony framework had a nice description how they did, it's still available for v.2: http://symfony.com/doc/2.0/book/security.html
I'm from the Java World, but things are pretty similar, so here are some hints about rights checking in my opinion.
If your right is a right like "have the right to perform action X". Then you should check before routing and in the service layer.
- The check in the service layer is to be sure that whenever you call that service, you'll have a said check.
- The really early check is in order to not have quite some of your code (data fetching,...) that can happen before checking the right. I was told it was done too to sustain more efficiently DDOS attacks. The earlier you reject a request, the better.
If you have a right like "have the right to perform action X on Y" then i guess you can only have it in the service layer, as the earlier instruction possible. However if your framework provide facilities to perform that when performing the routing, go for this, however i frankly prefer to have the service check, as the service's function can be used from various places.
You could perfectly have two sets of Rôles :
- One like : ADMIN/GUEST/USER : used for fast routing check, stored in user session
- Rights : used by service layer, stored in database or any persistent storage.
-
Before routing, but you mean before instantiating a Controller. Checking user stuff after route determination seems to be key, as one must know a route exists before testing authentication and authorization.Anthony Rutledge– Anthony Rutledge2018年08月29日 00:47:18 +00:00Commented Aug 29, 2018 at 0:47
-
When I hear Java people say things like "Service Layer" in a conversation about MVC, it feels like they do not know where the "Service Layer" would reside in an MVC architecture (even if that is not the case). I think the key is to envision the
Model
as having layers, instead of looking at theModel
as a monolithic thing.Anthony Rutledge– Anthony Rutledge2019年05月08日 12:38:47 +00:00Commented May 8, 2019 at 12:38 -
1In a classic "Anemic way" of doing thing, the Model contains the Service Layer, the data model,the persistence management, and also the DTOs used for communications and even some more stuff. You could say it contains everything except view and controller, but that still can make it quite big in moderate applications that include authentication, authorization, multiple data sources, multiple interconnection with others systems and so on (be carefull here : when interconnecting with others system you also have potentially controllers and dedicated views of your data between the systems).Walfrat– Walfrat2019年05月09日 09:50:55 +00:00Commented May 9, 2019 at 9:50
An attempt to simplify and clarify.
Step 1: Determine if the route exists. If the route does not exist, remember to send a 404 (Not Found)
HTTP response code in addition to anything else you do.
Step 2: If the route does exists, determine if it requires authentication (identity establishment).
Step 3: If authentication is required, check the authentication status (is user logged in?), before instantiating a Controller
. Note, this means that a route should be defined by the combination of an HTTP request method, the name of the Controller
, the action / command, and a Boolean flag that indicates the route is either public or private (secure).
Without these four things being pre-staged and available to the Router
, you would waste compute resources in the Controller
, or worse the Model
, attempting to resolve authentication status.
The key is to have abstract classes for Controller
, PublicController
, and PrivateController
, and then extend the latter two accordingly. However, only instantiate children of PrivateController
when authentication matters have been satisfied. This is vital if you are using constructor injection, as a Model
and View
must be selected for the PrivateController
s to manange.
There is no point in instantiating children of PrivateController
(along with their dependencies) if the HTTP request method, route (controller/action), and authentication have not been resolved.
Step 4a: If the user/visitor is not logged in, redirect to LoginController
(a child of PublicController
) with a 303 (See Other)
HTTP response code. If login fails, remember to send a 401 (Unauthorized)
HTTP response code.
Step 4b: If the user has already authenticated (logged in), check if he/she/it has the rights required to access the resource and action/method in question (before instantiating the PrivateController
child). If he/she/it has insufficient rights, remember to send a 402 (Forbidden)
HTTP response code.
You determine where someone ends up after logging in / an error / failure to authenticate for access to protected content.
Step 5: Now, after the Controller
has been instantiated, make sure the user is still logged in and verify authorization (rights) to take a specific action.
Summary
In effect, the first authentication and authorization check is about the nature of the HTTP request itself. Should the request be allowed to make the system do lots of work to get to the action / method being requested?
The second authentication and authorization check is about making sure the state of the request is still valid across a time domain (although the time window may be very small, indeed). Accounts get suspended, demoted, and force logged out all of the time. Just because the initial request is good does not mean it is still good .005 seconds later.
Implementation
One could use a class like this to help a Router
to decide if it should attempt to instantiate a child of PublicController
straight away, or deal with authentication matters first.
/**
* Class that holds HTTP request method / Controller / action / secure relationships.
*
* @version 4.0
* @author Anthony E. Rutledge
* @link https://www.linkedin.com/in/anthony-rutledge-2988b0125/
*/
class MvcRouteMap
{
private $routes = [
'GET' => [
'Root' => [
'index' => ['secure' => false]
],
'Contact' => [
'index' => ['secure' => false]
],
'Error' => [
'index' => ['secure' => false]
],
'Login' => [
'index' => ['secure' => false]
],
'Newsletter' => [
'index' => ['secure' => false]
]
],
'POST' => [
'Contact' => [
'send' => ['secure' => false]
],
'Login' => [
'login' => ['secure' => false]
],
'Newsletter' => [
'signup' => ['secure' => false]
]
],
'PUT' => [
],
'PATCH' => [
],
'DELETE' => [
]
];
public function __construct()
{
}
public function isSecureRoute(string $httpRequstMethod, string $controller, string $action): bool
{
if ($this->isRoute($httpRequstMethod, $controller, $action)) {
return $this->routes[$httpRequstMethod][$controller][$action]['secure'];
}
throw new RuntimeException("The resource requested does not exist.");
}
private function isRoute(string $httpRequstMethod, string $controller, string $action): bool
{
return isset($this->routes[$httpRequstMethod][$controller][$action]);
}
}
Explore related questions
See similar questions with these tags.
before_filter :authenticate_user, only: [:new, :create, :edit, :update, :destroy]
which would require authentication for those methods in the controller.UseCase
/Action
). See how "middlewares" in popular frameworks are implemented or read aboutChain of responsibility pattern
(if you like to keep frameworks on distance).