In this tutorial we will set up IdentityServer4 with ASP.NET Core Identity from absolute beginning. The ASP.NET Identity will be configured with a MongoDB database. When a client will come to IdentityServer4 for authentication, the client’ credentials, stored in the MongoDB database, will be checked by ASP.NET Core Identity. We are going to implement all this along with a Web API that will be secured with IdentityServer, so sit tight and enjoy this very long and detailed tutorial.
We will create a complete project from scratch and it’s source codes are given in GitHub repository.
Page Contents
IdentityServer acts as a central Authentication Server for applications allowing sign-on/sign-out and access control. IdentityServer uses OpenID Connect to verify the identity of clients and OAuth 2.0 framework for authorizing resources to authenticated clients. IdentityServer provides JWT tokens to clients and with these token they can identify themselves on the secured endpoints.
Centralizing the authentication provider is an excellent logic since you now don’t have to define the Authentication Logics in each and every Application. The IdentityServer will centralize the access control so that each and every application is secured by it.
IdenttityServer has following features:
Project 1: An IdentityServer4 project with users stored in a MongoDB database.
Project 2: A Client Project containing secured Web API. This Web API is protected by IdentityServer.
You as a user will try to access the Web API on the browser. But before that you will need to authenticate yourself before IdentityServer. IdentityServer will check your credentials stored in the MongoDB database by ASP.NET Core Identity. Then provide you with an access token which you can then use to access the Web API on the browser.
IdentityServer vs ASP.NET Core Identity : IdentityServer provides authentication services via JWT tokens and uses OAuth 2.0 and OpenID Connect. ASP.NET Core Identity on the other hand is a framework that manages users, passwords, profile data, roles, claims, tokens, email confirmations, and more. Both IdentityServer and Identity are used together for creating highly secured systems.
IdentityServer uses OpenID Connect and OAuth 2.0 protocols for providing authentication features. OAuth 2.0 is an authorization protocol that provides a grant of third-party application access to clients. OpenID connect helps clients to verify identity of a request based on the authentication performed by IdentityServer.
Let us understand it with an example.
Suppose you make a request to a Web API secured by IdentityServer. The following things will happens:
Create a new ASP.NET Core MVC app in Visual Studio and name it ISExample. Then configure ASP.NET Core Identity to it. Identity database can be anything like SQL Server, MongoDB. But keep in mind it should have login and logout views for enabling users to log-on/logout to their Accounts. I have created this project on my previous tutorial – ASP.NET Core Identity with MongoDB. You can download this project from that tutorial itself. Below image shows the image of the login screen of the project.
Next, we will setup Identity Server 4 in this project.
First install the following 2 packages to the project.
Just run the following 2 command on Package Manager Console window to install these packages.
Install-Package IdentityServer4
Install-Package IdentityServer4.AspNetIdentity
After the installation of the packages, it’s time to configure IdentityServer in the ASP.NET Core app.
We will keep the settings of IdentityServer in appsettings.json file. In Program.cs, we will read these settings and configure IdentityServer.
So add these settings shown in highlighted way to your “appsettings.json” file.
{
"MongoDbConfig": {
"Name": "Identity",
"Host": "localhost",
"Port": 27017
},
"IdentityServerSettings": {
"Clients": [
{
"ClientId": "zorro",
"AllowedGrantTypes": [
"authorization_code"
],
"RequireClientSecret": false,
"RedirectUris": [
"urn:ietf:wg:oauth:2.0:oob",
"https://localhost:6001/signin-oidc"
],
"AllowedScopes": [
"openid",
"profile",
"fullaccess"
],
"AlwaysIncludeUserClaimsInIdToken": true,
"AllowOfflineAccess": true
}
],
"ApiScopes": [
{
"Name": "fullaccess"
}
],
"ApiResources": [
{
"Name": "IS4API",
"Scopes": [
"fullaccess"
],
"UserClaims": [
"role"
]
}
]
}
}
Note: The “MongoDbConfig” section contains the MongoDB database settings. As already discussed, I configured MongoDB to be used as Identity Database. I have already configured it on my previous tutorial. You should definitely check it to get high quality knowledge on this subject.
Now we will discuss these IdentityServer settings one by one.
A client requests authentication tokens from IdentityServer. They must be registered in IdentityServer because only the registered clients can ask for tokens, requests from unregistered clients are not entertained at all.
In the json, I have defined the different properties of the client, these are ClientId, AllowedGrantTypes, RequireClientSecret, RedirectUris, and so on.
"Clients": [
{
"ClientId": "zorro",
"AllowedGrantTypes": [
"authorization_code"
],
"RequireClientSecret": false,
"RedirectUris": [
"urn:ietf:wg:oauth:2.0:oob",
"https://localhost:6001/signin-oidc"
],
"AllowedScopes": [
"openid",
"profile",
"fullaccess"
],
"AlwaysIncludeUserClaimsInIdToken": true,
"AllowOfflineAccess": true
}
]
ClientId: It is the id of the client which can be any name like test, postman, MyApp. etc. Here I have given it name as “Zorro”. Later, you will see that the Client Project has to specify this name when it will ask for tokens. Failing to provide a correct ClientId will result in getting invalid_client error response from IdentityServer.
More than one client can be added by adding another section:
"Clients": [
{
// client 1
},
{
// client 2
}
]
Each client is provided with individual values which they will use during interaction with IdentityServer. Some important ones are:
AllowedGrantTypes : the grant type which the client will use during token request from IdentityServer. I have defined it’s value as “authorization_code”. This is actually a part of OAuth 2.0, in short let me explain.
When Client provides a wrong authorization_code the OAuth 2.0 flow is broken and IdentityServer returns Code_Challenge is missing.
Well, you don’t have to perform these steps as they will be taken care by OpenID Connect. If you want to dive deep then see my tutorial on Implementing Google Contacts API where I have implemented these steps manually in C#.
RequireClientSecret : specify if the client needs a secret to request tokens. I have specified this as false. It’s default value is true, in that case you will have to add the Client Secret on the appsettings.json, and also the client project will have to provide this secret when it will ask for tokens.
Providing invalid secret when RequireClientSecret is true will result in invalid_client error returned by IdentityServer.
RedirectUris : it specifies where to redirect user after successful login. I have specified 2 uri here:
"RedirectUris": [
"urn:ietf:wg:oauth:2.0:oob",
"https://localhost:6001/signin-oidc"
]
AllowedScopes – contains the scopes which the client can request from Identity Server. Scopes are something that allows access to a particular set of resources. Scopes helps to limit access to a user’s data. Here I provided just 3 scopes:
"AllowedScopes": [
"openid",
"profile",
"fullaccess"
]
openid – you must have this scope for OpenID Connect to work.
profile – this scope will provide you access to the user’s profile information and email address.
fullaccess – it is a custom defined scope.
AlwaysIncludeUserClaimsInIdToken – it specifies if you want to include user claims in Id token. It must be set to true. Claims are name/value pairs that contain information about a user eg Name is a claim of a user, similarly role, email, address are also claims.
What are Access and Id tokens – IdentityServer provides 2 token for authenticated users. These are Access and Id tokens. The Id Token is a security token that contains information about a user. Access tokens, on the other hand, simply allow access to certain secured resources.
AllowOfflineAccess – Specifies whether this client can request refresh tokens. Refresh tokens are used to obtain new access tokens when old access tokens get expired, they do this automatically without the need for the user to re-login to the app.
ApiScopes define what Scopes the secured APIs have access to. We will be protecting our APIs with IdentityServer.
I defined “fullaccess” for the ApiScope. The “fullaccess” is my custom scope which I have earlier added to the “AllowedScopes” section.
"ApiScopes": [
{
"Name": "fullaccess"
}
]
I have defined just a single ApiScopes but we can also have multiple API Scopes, see the example given below.
"ApiScopes": [
{
"Name": "fullaccess"
},
{
"Name": "read"
},
{
"Name": "write"
}
],
The ApiResources contains what claims the access token will contain.
"ApiResources": [
{
"Name": "IS4API",
"Scopes": [
"fullaccess"
],
"UserClaims": [
"role"
]
}
]
I gave it a name “IS4API”, when we will request for token then we will have to provide this same name for the “Audience”. We will see this thing in details later on.
Next, we define the scopes and the UserClaims that the access token will contain. These will be “fullaccess” scope and “role” claim.
We can now define a C# class that will be populated with the settings stored in the appsettings.json. So, create a new class called IdentityServerSettings.cs inside the “Settings” folder of the project and add the following code to it.
using IdentityServer4.Models;
namespace ISExample.Settings
{
public class IdentityServerSettings
{
public IReadOnlyCollection<ApiScope> ApiScopes { get; init; }
public IReadOnlyCollection<ApiResource> ApiResources { get; init; }
public IReadOnlyCollection<Client> Clients { get; init; }
public IReadOnlyCollection<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
}
The properties – ApiScopes, ApiResources, Clients are the same once which we have added in the appsettings.json.
There is another property called IdentityResources which will hold the user data like userId, email, and phone number. We defined 2 resources – OpenId and Profile. Defining these 2 resources will add specific claims on the access token. OpenId will add “sub” claim while Profile will add claims like first name, last name, etc
With all that said it’s time to go to Program class where we will actually configure IdentityServer.
We configure IdentityServer on the Program.cs class. Add the below code lines to your program class.
var identityServerSettings = builder.Configuration.GetSection(nameof(IdentityServerSettings)).Get<IdentityServerSettings>();
builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
})
.AddAspNetIdentity<ApplicationUser>()
.AddInMemoryApiScopes(identityServerSettings.ApiScopes)
.AddInMemoryApiResources(identityServerSettings.ApiResources)
.AddInMemoryClients(identityServerSettings.Clients)
.AddInMemoryIdentityResources(identityServerSettings.IdentityResources)
.AddDeveloperSigningCredential();
First we populate the IdentityServerSettings.cs which we created earlier with the values stored in appsettings.json. This is done by reading ASP.NET Core appsettings.json from GetSection method of IConfigurationSection.
Next, we add IdentityServer to IServiceCollection with the AddIdentityServer method. There are number of methods to configure IdentityServer on our project. These are provided with the corresponding values of the “IdentityServerSettings” class.
We also enabled events for success, error and failure. These will show the helpful messages on the console, an approach which you should use for debugging.
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
AddDeveloperSigningCredential : Creates temporary key during the application startup, this key will sign the IdentityServer settings which we have applied so that they cannot be forged. This is for development case only and should not be used in production. The generated key will be persisted to the file system so it stays stable between server restarts. In production we should use AddSigningCredential method.
The final thing we have to do is to add the IdentityServer middleware (on the Configure method of startup class) that exposes the OpenID Connect endpoints. This is done by adding app.UseIdentityServer() just after the UseRouting middleware.
app.UseIdentityServer();
Choose the self-host option by click the small down arrow on the right to the run button.
Now run the application for the first time, and check the Solution Explorer window to find a new file is create by the name of tempkey.jwk. This file contains the key to sign the credentials. Recall, we defined it by AddDeveloperSigningCredential method.
Congrats, IdentityServer has been successfully setup in our project.
IdentityServer’s Discovery Endpoint provides it’s metadata like supported scopes, authorization endpoint, token endpoint and many other information. All these information are collectively called as Discovery Document. These metadata can be retrieved by the client applications to configure themselves accordingly.
Identity Server Discovery Endpoint can be accessed at the uri /.well-known/openid-configuration. In our IdentityServer project it’s url is – https://localhost:44312/.well-known/openid-configuration. The port will be different in your case. So, when we open this uri on the browser we are presented with the metadata information in json.
This json presented on the browser is raw and difficult to read. I suggest to open Postman and make a get request to the uri of the discovery endpoint. Postman will present the discovery endpoint in a pretty tree view manner.
Important things to note here are the 5 things –
See below:
{
"issuer": "https://localhost:44312",
"jwks_uri": "https://localhost:44312/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://localhost:44312/connect/authorize",
"token_endpoint": "https://localhost:44312/connect/token",
"userinfo_endpoint": "https://localhost:44312/connect/userinfo",
"end_session_endpoint": "https://localhost:44312/connect/endsession",
"check_session_iframe": "https://localhost:44312/connect/checksession",
"revocation_endpoint": "https://localhost:44312/connect/revocation",
"introspection_endpoint": "https://localhost:44312/connect/introspect",
"device_authorization_endpoint": "https://localhost:44312/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"openid",
"profile",
"fullaccess",
"offline_access"
],
"claims_supported": [
"sub",
"name",
"family_name",
"given_name",
"middle_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"gender",
"birthdate",
"zoneinfo",
"locale",
"updated_at",
"role"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"implicit",
"password",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code",
"token",
"id_token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"response_modes_supported": [
"form_post",
"query",
"fragment"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"subject_types_supported": [
"public"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"request_parameter_supported": true
}
The authorization_endpoint is used to interact with the client project and obtain an authorization Grant while the token_endpoint is the IdentityServer’s uri which provides token for authenticated users.
The scopes_supported is what we set in the appsettings.json file. Notice the “authorization_code” is available in the grant_types_supported, we set it on the appsettings.json.
Our IdentityServer is up and running, it’s time to create the Client Project. So, create a new ASP.NET Core Web API and name it ISClient.
This Web API comes prebuilt with a WeatherForecast API which can be accessed from the uri – /WeatherForecast.
Our goal is to secure this Web API from IdentityServer. We also need this project to run on Self-host manner and not on IIS Express so that we can see application logs on the console. So make sure you select this option from the run button on visual studio (we did the same thing for the IdentityServer project previously).
Next, go to the launchSettings.json and change the applicationUrl to 6001 and 6000 ports for the https {…} block.
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:6001;http://localhost:6000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
For debugging purpose we want to display Authorization messages on the console. So go to the appsettings.json file and add the highlighted code line to it.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Authorization": "Information"
}
},
"AllowedHosts": "*"
}
A quick recall, now we have 2 projects:
We will now Secure the ASP.NET Core Web API with IdentityServer so that access to the protected endpoints are done only with access tokens. First install the package called IdentityModel which provides a libraries for working with OpenID Connect, IdentityServer and OAuth2.0.
It can be installed by running the following command on your Package Manager Console.
Install-Package IdentityModel
We will also need JWT package called Microsoft.AspNetCore.Authentication.JwtBearer package which adds middleware for the application to receive bearer tokens. It’s installation command is:
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Next, Import the JwtBearer namespace on the Program.cs class.
using Microsoft.AspNetCore.Authentication.JwtBearer;
Next, add the below code.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:44312";
options.Audience = "IS4API";
});
Three things to note here:
Also add the authentication and authorization middewares.
app.UseAuthentication();
app.UseAuthorization();
Go to the WeatherForecastController.cs located inside the “Controller” folder and import the namespace:
using Microsoft.AspNetCore.Authorization;
Now add an [Authorize] Attribute to the Controller. With this we have secured our Web API Endpoint with IdentityServer.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ISClient.Controllers
{
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
...
}
}
Make sure both the “ISExample” & “ISClient” are running. Also the MongoDB docker container should be running. We did all this setup on our previous tutorial. Next, open POSTMAN and send a GET Request to the Weather Forecast url – https://localhost:6001/WeatherForecast. You will be getting a 401 Unauthorized Error. The reason is quite obvious, we need access token.
Check the console window, you will find a clear message telling what’s going on:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Now let us request access token from IdentityServer. In Postman, do the following things:
Check the below 2 images where I have marked all of these settings.
Now click the Get New Access Token button. Postman will open a dialog which will show the login screen. So login on this screen with the credentials of your Identity account.
I had created the Username and Password in my earlier tutorial. This is stored on MongoDB database, so I log-on with these credentials
.On clicking the Log In button, we are presented with 2 tokens by IdentityServer, these are:
Copy the access token and decode it on jwt.io website. You will see it contains audience, client_id and scopes which we had set earlier.
Next, decode the Id token (scroll down the postman screen to find the id token). The Id token contains the claims like name, preferred_username, etc.
You can also check the logs on the console in the ISExample project. First it shows the message “Showing login: User is not authenticated”. After we performed the login, it shows client_id, granttype, scopes, redirecturi and so on. Logs are very helpful for debugging purpose.
Coming back to Postman, click the Use Token button, this will copy the access token to the Access Token field and close the current dialog box. Now we can call the secured Web API with this token.
Add the url of the web api which is https://localhost:6001/WeatherForecast to the url text box in Postman. Also make sure that “GET” is selected on the dropdown then click the Send button. This time you will see 200 OK response which means the api is called successfully with the access token.
Now check the Body tab in Postman where you will find the Weather details returned by the Web API in JSON format.
Congratulations, our Web API is protected by IdentityServer and now we can call it through the access token provided by IdentityServer. It’s time to implement the final thing which is the OpenID Connect on the Client Project.
So far, we have seen that when a request is made to the IdentityServer regarding authentication, a login screen is presented. We have the login screen and the login action method created in the IdentityServer project. Now after the access token is generated, we made the call to the web api by adding access token on the Request header. Have you noticed, we were doing this manually in Postman?
So the question arises, how to automate these things. That is, the client project must be called automatically with Access Token and we get the Weather Forecast data.
The answer is through OpenID Connect, by using it, the client project will be able to automatically find out the identity of the user whose authentication is done by IdentityServer.
First install the package called Microsoft.AspNetCore.Authentication.OpenIdConnect to the “ISClient” project. The command is:
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect
In the Program.cs, update the AddAuthentication method to include default scheme to “cookies” and defaultchallengescheme to “oidc”. Also add AddOpenIdConnect method for adding the following things:
All these changes are shown below.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:44312";
options.Audience = "IS4API";
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:44312";
options.ClientId = "zorro";
options.ResponseType = "code";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("fullaccess");
options.SaveTokens = true;
});
So now we are using a cookie to locally sign-in the user and we will be using the OpenID Connect protocol. The AddCookie(“Cookies”) method species that we will be processing the cookie via OpenID Connect protocol.
Finally, SaveTokens is used to persist the tokens from IdentityServer in the cookie as they will be needed later.
One thing more, make sure you have correctly added signin-oidc in the RedirectUris value in appsettings.json of ISExample project. Since the client project is running from 6001 port therefore it’s value will be https://localhost:6001/signin-oidc.
"RedirectUris": [
"urn:ietf:wg:oauth:2.0:oob",
"https://localhost:6001/signin-oidc"
]
That’s all is the integration part. We can now do the testing.
Run both the ISExample and ISClient projects. Now in the browser open the url of the secured web api which is https://localhost:6001/WeatherForecast. You will be redirected to the login screen. After performing successfully login, you will be able to see the weather forecast json. I have shown this in the below given video.
We can now call the Web API from our ISClient project by using HttpClient class. First thing to do is to change the authentication type on the Web API to “Bearer”. This is done by adding AuthenticationSchemes to JwtBearerDefaults.AuthenticationScheme in the [Authorize] attribute.
[ApiController]
[Route("[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class WeatherForecastController : ControllerBase
{
}
We should also add have Bearer authentication on the program class.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
Bearer authentication does the following validations.
You can learn more about Bearer tokens on my JWT Series which contains just 2 tutorials:
Next, add a new controller called CallApiController.cs from where the API call will be made. It’s code is given below.
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace ISClient.Controllers
{
[Authorize]
public class CallApiController : Controller
{
public async Task<IActionResult> Index()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var weather = new List<WeatherForecast>();
using (var client = new HttpClient())
{
client.SetBearerToken(accessToken);
var result = await client.GetAsync("https://localhost:6001/WeatherForecast");
if (result.IsSuccessStatusCode)
{
var model = await result.Content.ReadAsStringAsync();
weather = JsonConvert.DeserializeObject<List<WeatherForecast>>(model);
}
else
{
throw new Exception("Failed");
}
}
return View(weather);
}
}
}
This controller is itself secured by IdentityServer as I have added Authorize attribute to it.
We get the access token from the code line:
var accessToken = await HttpContext.GetTokenAsync("access_token");
We add this token to the authorization header of the HttpClient object.
client.SetBearerToken(accessToken);
Then make GET request to the Web API.
var result = await client.GetAsync("https://localhost:6001/WeatherForecast");
The JSON returned by the API has to be deserialized into List<WeatherForecast> so we need to install Newtonsoft.Json package in the project.
weather = JsonConvert.DeserializeObject<List<WeatherForecast>>(model);
We also have to add the following 3 codes for enable controller with views and routing in the program class.
builder.Services.AddControllersWithViews();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Next, create the Index view called Index.cshtml inside the “Views > CallApi” folder. The view accepts model of type List<WeatherForecast>, it will show weather data in an HTML table.
@model List<ISClient.WeatherForecast>
@{
ViewData["Title"] = "Weather";
}
<h1>Weather</h1>
<table class="table table-striped">
@foreach (var weather in Model)
{
<tr>
<td>@weather.Date</td>
<td>@weather.Summary</td>
<td>@weather.TemperatureC</td>
<td>@weather.TemperatureF</td>
</tr>
}
</table>
Run the project, and open the url of the CallApiController.cs which is – https://localhost:6001/CallApi. You will be redirected to Login page, once you performed the login, IdentiityServer will provide you tokens. With the access token the api call is made and Weather is shown on the browser.
In this tutorial we learned to Setup IdentityServer, it’s scopes, clients, resources and later on protected Web APIs through it. In the next tutorial we will perform Role and Policy based authentication with IdentityServer.