IdentityServer with ASP.NET Core Identity and MongoDB as Database [Detailed Guide]

IdentityServer with ASP.NET Core Identity and MongoDB as Database [Detailed Guide]

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.

This tutorial is a part of “IdentityServer with ASP.NET Core Identity” series and contains 4 tutorials, these are:
  1. ASP.NET Core Identity with MongoDB as Database
  2. IdentityServer with ASP.NET Core Identity and MongoDB as Database
  3. IdentityServer Role and Policy Based Authentication
  4. ASP.NET Core – Duende IdentityServer authentication and authorization with Identity

What is IdentityServer?

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.

Identity Server Architecture

IdenttityServer has following features:

  1. Authentication Service
  2. Sign-on/Sign-out
  3. Access Control for APIs
  4. Federation Gateway for external identity providers like Azure Active Directory, Google, Facebook etc

What we will build?

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 OpenID Connect OAuth 2.0

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:

  1. The Web API will say – ‘hey I want to verify your identity so I will redirect you to IdentityServer through OAuth 2.0 protocol’.
  2. IdentityServer will present you a login form so that you can login to your account.
  3. Once login, OAuth 2.0 will redirect you back to the Web API but this time you also have JWT access token with you. This access token is provided by IdentityServer.
  4. The Web API will now use OpenID Connect to verify your identity based on the token provided by IdentityServer. And now, you can access the Web API.
You can also understand this thing from real life world. A 16 year old kid (You) want to go to a bar but is blocked by the bouncer (Client) on the door. The kid goes to his uncle (IdentityServer) who happens to be a Senator. The senator uncle calls the bouncer and now the kid can enter the bar.

Creating IdentityServer 4 Project

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.

login form identity server 4

Next, we will setup Identity Server 4 in this project.

Setup IdentityServer in ASP.NET Core

First install the following 2 packages to the project.

  • IdentityServer4
  • IdentityServer4.AspNetIdentity

IdentityServer Packages install NuGet

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.

IdentityServer settings in appsettings.json

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.

What are IdentityServer Clients

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.

  • Suppose the user opens the browser to a secured app and then he will be redirected to IdentityServer.
  • IdentityServer redirects the user back with an authorization code (which is just a random string) in the query string.
  • The browser then exchanges this authorization code for an access token.

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"
]
  • urn:ietf:wg:oauth:2.0:oob – this uri is for API Client Postman and is used for testing. In production you should not add this uri.
  • https://localhost:6001/signin-oidc – this uri is for the client project. The “localhost:6001” is the uri of the client project and “signin-oidc” endpoint is used by OpenID Connect to know the identity of the user after he is authenticated. Note that the client project will have OpenID Connect configured in it. We will come back to this thing when we will create the Client Project.

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.

What are IdentityServer ApiScopes

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"
  }
],

What are IdentityServer ApiResources

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.

Defining IdentityServer settings class

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.

Configure IdentityServer on Program.cs

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.

  • AddAspNetIdentity : set it as a ApplicationUser type.
  • AddInMemoryApiScopes : adds in-memory API scopes.
  • AddInMemoryApiResources : adds in-memory API resources.
  • AddInMemoryClients : adds in-memory clients.
  • AddInMemoryIdentityResources : adds in-memory identity resources.

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();
Run the application on Visual Studio

Choose the self-host option by click the small down arrow on the right to the run button.

self host asp.net core

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.

tempkeyjwk identityserver

Congrats, IdentityServer has been successfully setup in our project.

IdentityServer Discovery Endpoint

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.

IdentityServer Discovery Endpoint

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 –

  1. authorization_endpoint
  2. token_endpoint
  3. scopes_supported
  4. claims_supported
  5. grant_types_supported

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.

Creating the Client Project

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.

ASP.NET Core Web API Template

This Web API comes prebuilt with a WeatherForecast API which can be accessed from the uri – /WeatherForecast.

Weather Forecast API

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:

  1. IdentityServer project.
  2. Client project holding the Web API and runs from 6001 port.

Securing Web API with IdentityServer

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:

  • The authentication scheme is JwtBearerDefaults.AuthenticationScheme which is “Bearer”.
  • The JWT Bearer option’s authority value is the uri of the IdentityServer project which is https://localhost:44312.
  • The JWT Bearer option’s Audience value should be the same as set on the “ApiResources” section on the appsettings.json file of the ISExample project. Recall, it was set as “IS4API”.
ApiResources/Audience value is a security feature and must not be revealed. Unauthorized clients can never generate access tokens from IdentityServer since they do not know this value.

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
    {
        ...
    }
}

Fetching IdentityServer Token from Postman

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.

403 unauthorized postman

Check the console window, you will find a clear message telling what’s going on:

DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

DenyAnonymousAuthorizationRequirement IdentityServer

Now let us request access token from IdentityServer. In Postman, do the following things:

  • Select Authorization tab.
  • Select OAuth 2.0 for Type and Request Headers for “Add authorization data to”.
  • For the Header Prefix select Bearer.
  • For Grant Type select Authorization Code (With PKCE).
  • Set Callback URL to urn:ietf:wg:oauth:2.0:oob. Recall it is the one we set for the RedirectUris in appsettings.json.
  • Set Auth URL to the value of authorization_endpoint in the discovery endpoint which is https://localhost:44312/connect/authorize.
  • Set Access Token URL to the value of token_endpoint in the discovery endpoint which is https://localhost:44312/connect/token.
  • Client ID should be set at zorro. Recall we set this on the appsettings.json.
  • Set Code Challenge Method to SHA-256.
  • Set Scope to the 3 values which we set on the AllowedScopes in the appsettings.json. These were openid profile fullaccess.
  • Set Client Authentication to Send as Basic Auth header.

Check the below 2 images where I have marked all of these settings.

postman authorization settings identityserver

Postman Authentication Configurations identityserver

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.

Postman Identity Login Screen identityserver

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:

  1. Access token
  2. Id token

IdentityServer Tokens Postman

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.

access token decode

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.

IdentityServer id Token decode

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.

Console Logs IdentityServer asp.net core app

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.

calling asp.net core web api with access token postman

Now check the Body tab in Postman where you will find the Weather details returned by the Web API in JSON format.

Weather Forecast JSON Web Api postman

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.

Implementing OpenID Connect

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:

  • Authority – url of the IdentityServer project
  • ClientId – zorro
  • ResponseType – code
  • Added openid, profile and fullaccess scopes with “Scope.Add” method.
  • SaveTokens – true. So, ASP.NET Core will automatically store the resulting access and refresh token in the session.

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.

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.

openid connect implementation video asp.net core

Calling IdentityServer secured Web API

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.

  1. Makes sure the token is coming from a trusted issuer like IdentityServer.
  2. Validates the token so that it can be used with the api.

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.

call web api identityserver asp.net core

Conclusion

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.

SHARE THIS ARTICLE

  • linkedin
  • reddit
yogihosting

ABOUT THE AUTHOR

I hope you enjoyed reading this tutorial. If it helped you then consider buying a cup of coffee for me. This will help me in writing more such good tutorials for the readers. Thank you. Buy Me A Coffee donate

Leave a Reply

Your email address will not be published. Required fields are marked *