We have set up our identity server but we lack UI for the users to enter their credentials. We’ve shown how we can retrieve our token but for better user experience, we have to add UI to our Authorization Server. So, adding IdentityServer4 UI is our goal for this article. Additionally, we are going to learn how we can protect our API and the way to access protected resources. This will be a basic protection setup, but we are going to enhance it during this series.
To navigate through the entire series, visit the IdentityServer4 series page.
Let’s start.
Installing IdentityServer4 UI and Overview
If we start our authorization server now, we are going to see a not-found page in a browser for sure. Even though the server works, this is not user-friendly at all. So let’s do something about that. There is already created UI for the IdentityServer which is easy to install and modify, and we are going to use it for our project.
So, the first thing we are going to do is to open a PowerShell window and navigate to the folder where we have our OAuth project (…\CompanyEmployees.OAuth\CompanyEmployees.OAuth). Once the window is up, let’s execute a command to download the entire UI project:
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/main/getmain.ps1'))
After a couple of seconds, we can see a confirmation about installed Quickstart, Views, and wwwroot folders:
File Inspection And Configuration
If we inspect the SolutionExplorer window of our project, we are going to see three additional folders:
We can see the wwwroot
folder with the static files required for the UI. There is the Quickstart
folder with different controllers, models, and additional classes. Finally, there is the Views
folder with different views (.cshtml files) inside. Everything we downloaded is easy to maintain and modify so, this speeds up the UI creation process a lot. For our examples the default created files will be quite enough.
Now, we have to configure this UI.
So, the first thing we are going to do is to modify the Configure
method in the Startup
class:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); }
With the UseStaticFiles
method, we enable serving static files from the wwwroot
folder. Additionally, we are adding routing and authorization to the pipeline and configuring endpoints to use a default /Home/Index
endpoint. If we open the Quickstart/Home
folder, we are going to find a HomeController
with the Index
action inside. So, this is our entry point.
We have to modify the ConfigureServices
method as well:
public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddInMemoryIdentityResources(InMemoryConfig.GetIdentityResources()) .AddTestUsers(InMemoryConfig.GetUsers()) .AddInMemoryClients(InMemoryConfig.GetClients()) .AddDeveloperSigningCredential(); //not something we want to use in a production environment; services.AddControllersWithViews(); }
Excellent. Now, we can test our UI.
Testing IdentityServer4 UI
Let’s start the application. It should automatically navigate to localhost:5005
and show the Welcome
page:
If we click the discovery document link, we are going to see different endpoints we’ve been talking about in a previous article.
Once we click the second link, we are going to be directed to the login page and after we enter Mick’s credentials, we are going to see all the claims:
Now, if we click the second “here” link, for the stored grants, we are going to be directed to the login page but as soon as we enter credentials, we are going to see the message informing us about not given access to any applications:
So, we see everything is working as expected. We can logout as well by clicking the username (Mick) and then the Logout link. In the rest of the series, we are going to use this UI quite frequently.
Now, let’s see how we can protect our API using the authorization server.
Configuring API Resources
The first thing we have to do is to add our API scopes in the Authorization Server:
public static IEnumerable<ApiScope> GetApiScopes() => new List<ApiScope> { new ApiScope("companyApi", "CompanyEmployee API") };
Then, as we did with the IdentityResources
, in a previous article to support different identity-related data, we are going to support our API resource as well. So, let’s add additional code in the InMemoryConfig
class in the OAuth
project:
public static IEnumerable<ApiResource> GetApiResources() => new List<ApiResource> { new ApiResource("companyApi", "CompanyEmployee API") { Scopes = { "companyApi" } } };
As you can see, we can add multiple API resources, but for now, we add only one with the name and displayname parameters. Also, we have to specify the scopes related to this resource.
Additionally, we have to modify the client:
public static IEnumerable<Client> GetClients() => new List<Client> { new Client { ClientId = "company-employee", ClientSecrets = new [] { new Secret("codemazesecret".Sha512()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, "companyApi" } } };
We just add additional scope to support our API scope.
Finally, we have to include this configuration in the ConfigureServices
method:
services.AddIdentityServer() .AddInMemoryApiScopes(InMemoryConfig.GetApiScopes()) .AddInMemoryApiResources(InMemoryConfig.GetApiResources()) .AddInMemoryIdentityResources(InMemoryConfig.GetIdentityResources()) .AddTestUsers(InMemoryConfig.GetUsers()) .AddInMemoryClients(InMemoryConfig.GetClients()) .AddDeveloperSigningCredential(); //not something we want to use in a production environment;
That is all regarding the IdentityServer configuration and we can continue with the API security logic.
Securing Web API
Before we configure our middleware to support IdentityServer, we have to install a Nuget package to help us in the process:
After the installation, we can modify the ConfigureServices
method in the Startup
class:
services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", opt => { opt.RequireHttpsMetadata = false; opt.Authority = "https://localhost:5005"; opt.Audience = "companyApi"; }); services.AddControllers();
We use the AddAuthentication
method to add authentication services to the IOC in our project. Moreover, we use the AddJwtBearer
method to configure support for our Authorization Server. In that method, we specify several options:
- RequireHttpsMetadata – We set this to false because we are in a development environment and we don’t require HTTPS
- Authority – Address to use when sending OpenID Connect calls
- Audience – Audience value for received OpenID Connect tokens. This value has to be the same as the one provided in the authorization server configuration for the API resource
We have to do one more thing in the Configure
class:
app.UseAuthentication(); app.UseAuthorization();
Now, all we have to do is to protect our action in the CompaneesController
by placing the [Authorize]
attribute on top of the action or controller itself. For that, we have to include Microsoft.AspNetCore.Authorization
endpoint in the controller.
Let’s start both applications and send a request with Postman:
As expected, we get the 401 – Unauthorized response.
Now, to provide access to the authorized resource, we are going to send another request, but to the authorization server first:
Great, we have a token. Let’s copy our token and add it to the previous request as the Authorization parameter in the Headers tab:
And, there we go. We have the required data. So, we are authorized to communicate with our API.
Inspecting Console Logs
Now, let’s take a look at the console window of the IdentityServer project:
What we can see here is that our API sent a request to the /.well-known/openid-configuration/jwks endpoint to retrieve a public key from the identity server to validate our token.
We can validate that this communication is taking place by modifying our token in Postman. If we do that, we are going to get 401 for sure as a result, because the token is tempered with and is valid no more:
Excellent. We can see the communication between our API and the Identity Server is working flawlessly.
Inspecting Claims
Finally, we can inspect the claims from the token. To do that let’s add a single code line in the GetCompanies
action:
var claims = User.Claims;
After we acquire a valid token again and send a request to our API, we can see all the claims:
So, we can see all the data from the access token provided as a Bearer token in our request. And we can confirm that this token is not tampered with, because at this point, the token is already validated and the API has all the required information from it.
Of course, we’ve used the Client_Credentials flow in this example, but if you use the ResourceOwnerPassword flow, we are going to get additional claims for sure:
If we decode this token, we are going to get the same result:
Excellent. Everything works as expected.
Conclusion
So, we have learned how to add the UI part to our authorization server and also how to protect our Web API by using two different flows. Of course, we are going to learn more about API protection with additional flows and we are going to use this UI for that purpose as well.
In the next article, we are going to learn how to use the Hybrid flow to protect our web application.