Authentication vs. Authorization
This is episode 71 of an ASP.NET Core tutorial. In this video, we'll discuss authorization in ASP.NET Core.
Authentication is the process of identifying who the user is, whereas authorization is the process of identifying what the user can and cannot do. Authorization usually happens after authentication because in order for us to be able to determine what the logged-in user can and cannot do, we first have to identify who the user is. For example, if the logged-in user is an administrator user, he may be able to create, read, update, and delete—that is, perform all the CRUD operations. On the other hand, if the logged-in user is a normal user who usually has less privileges than an administrator user, he may only view an order but not create, update, or delete orders.
In ASP.NET Core MVC, authorization is implemented through the use of the Authorize attribute. Let's take a look at a couple of examples.
Securing Controller Actions with the [Authorize] Attribute
At the moment, all users—that is, anonymous users as well as authenticated users (that is, logged-in users)—are able to see the list of all employees, view a specific employee's details, edit an existing employee's details, and even create a new employee. Now, what we want to be able to do is limit these two actions, that is, creating a new employee and editing an existing employee's details. We want to limit these two actions to authenticated users, that is, logged-in users. If the user is not logged in, then they should not be able to create a new employee or edit an existing employee's details. On the other hand, all users, that is, both anonymous users and authenticated users, should be able to see the list of all employees and view a specific employee's details if they want to. Let's see how to implement this through the use of the Authorize attribute in ASP.NET Core MVC.
Here is the Home controller of our application. And then the Index action returns us the list of all employees. This Details action method returns us the details of a specific employee's details. Now, we do not want to protect these two actions from anonymous access, so I'm not going to decorate them with the Authorize attribute. On the other hand, we want to protect the Create action, so let's decorate this with the Authorize attribute. Let's bring in the required namespace by pressing Control+Period. We also want to protect the Edit action from anonymous access, so let's also decorate it with the Authorize attribute. These two actions, Create and Edit, respond to HTTP GET. We also have their counterparts that respond to HTTP POST, and we need to protect them as well from anonymous access. So let's decorate them also with the Authorize attribute.
Understanding the Login Redirect and ReturnUrl
At the moment, I'm not logged in, but I should still be able to see the list of all employees and view a specific employee's details. But when I try to create a new employee or edit an existing employee, notice I'm redirected to the login page. This is because we decorated both the actions with this Authorize attribute. At the moment, we're using the Authorize attribute in its simplest form, without any parameters. By using the Authorize attribute like this, we're basically telling, we want the users to be at least logged in to be able to reach these actions, and that's the reason we see this login page.
Notice in the URL, we'll now have a new query string parameter, ReturnUrl, and its value has got these strange characters: %2F. These are the encoded characters for a forward slash. To see these characters, let's copy the URL and decode them. So to decode the URL, I'm going to go to this website and paste the URL here and click the Decode button. Notice the value now, it is /home/edit/1. This is the URL that we were trying to access, but since we are not logged in, we are redirected to the login page.
If you're wondering what is the use of this ReturnUrl query string parameter, well, we could use that to redirect the user to the page that he was trying to access after a successful login. We'll discuss how to do that in our upcoming videos. Now let's log in and quickly test if we can get to the Create and Edit actions. We are logged in. Let's try to edit this employee details. I'm able to do that. Let's also create a new employee. Notice we have access to both the actions. Log out.
Controller-Level Authorization with [AllowAnonymous]
At the moment, we are using the Authorize attribute on the individual controller action methods. We could also apply this on the controller itself. When we do that, it's applicable to all the actions within that controller. Let's quickly test that. First, let's remove the Authorize attribute from the individual actions. Now, let's include the Authorize attribute at the controller level. With this change, if we are not logged in, we'll not be able to access any of the actions within the Home controller, including the Index and Details actions. Let's quickly test this. At the moment, we are on the list view. If I try to view a specific employee details, notice we are redirected to the login page.
But in our case, we want to allow anonymous access to the Index and Details actions. At the moment, by using the Authorize attribute at the controller level, it's applicable to all the actions, including Index and Details. But we want to allow anonymous access to these action methods. To achieve that, we could use the AllowAnonymous attribute. Let's do the same with the Details section as well. Notice now we are able to see the list of employees as well as view specific employee details.
At the moment, we have applied the Authorize attribute at the controller level and the Anonymous attribute on the individual actions. If we use these attributes the other way around, that is, the AllowAnonymous attribute at the controller level and Authorize attribute on individual actions, will it have the desired effect that we want? The answer is no. If we use the AllowAnonymous attribute at the controller level, then it's going to ignore the Authorize attribute that is applied on the individual actions within that controller. I will leave that for you to test and verify.
Configuring Global Authorization in Startup.cs
Now, is there a way to apply this Authorize attribute globally instead of applying it on each controller like this? The answer is yes, and we do that in Startup.cs. Notice in the ConfigureServices method, we are calling AddMvc() extension method to add the required MVC services. At the moment, we are using the AddMvc method that does not take any parameters. We have another overloaded version that takes an action of MvcOptions as a parameter. We are going to use this overloaded version to configure MVC. So I'm going to pass a parameter here, let's name it options. We need to build an authorization policy first. For that, let's create a variable named policy and build the authorization policy. I'm going to use the AuthorizationPolicyBuilder class. Bring in the required namespace by pressing Control+Period. And then to this, we want to chain RequireAuthenticatedUser() method and then build the authorization policy by calling the Build() method.
With this authorization policy, we are basically saying to reach any of the controllers or their actions within our application, we want the users to be authenticated, that is, logged in. Next, we want to add the AuthorizeFilter along with this policy. For that, on the options parameter, we have Filters. We want to add a new filter, so we call Add, and the filter that we want to add is a new AuthorizeFilter. Bring in the required namespace, which is Microsoft.AspNetCore.Mvc.Authorization, and then to the constructor, let's pass the policy that we have created. Note now, when we reload the page, we're able to get to the Details section of the Home controller because on the Details section, we are using the AllowAnonymous attribute. So we have anonymous access to the Details section of our Home controller.
Fixing the Infinite Redirect Loop
Now, look what happens when I try to go to the login page. We have an error: 404.15. The request filtering module is configured to deny requests where the query string is too long. And that's exactly the problem we have. Look at the query string, it's too long. Let's copy this and paste it in a notepad. Look at the query string parameter: ReturnUrl=account/login, ReturnUrl=account/login. It appends the ReturnUrl to the URL as a query string parameter several times until it becomes excessively large, so the web server gives up processing and throws this error. Why is this happening?
Well, because the application is stuck in an infinite loop. At the moment, we have our application configured to apply the Authorize attribute globally. So this means even to get to this Login action within our Account controller, we must be already logged in. So when I click on the login link to log in, the application detects I am not logged in, so it tries to redirect me to the login URL, which is /account/login. But again, to get to that URL, we must be logged in. So it again tries to redirect me to that same login URL. So it is stuck in this infinite loop until the query string becomes excessively large, so the server gives up processing and it fails with this error message.
To fix this, all we need to do is allow anonymous access to the login actions within our Account controller. Let's bring in the required namespace. We also want to allow anonymous access to the register actions, otherwise new users will not be able to register with our application.
Recap and Advanced Authorization Topics
Now let's navigate to the root URL. We see the list of employees. Now because we allowed anonymous access to both the Login and Register actions, we are able to get to them without logging in. This is the same piece of code that we used to apply the Authorize attribute globally. Notice we are creating an authorization policy and an AuthorizeFilter. Don't worry if you are new to these concepts. We'll discuss both of them in detail in our upcoming videos.
At the moment within our application, we're using the Authorize attribute in its simplest form, without any parameters. If we use the Authorize attribute like this, it only checks if the user is authenticated. In addition to this simple authorization, ASP.NET Core also supports role-based, claims-based, and policy-based authorization. We'll discuss these authorization techniques in our upcoming videos. That's it in this video. Thank you for listening.