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 IdentityServer with ASP.NET Core Identity Core from the absolute beginning. Also the ASP.NET Identity is configured with a MongoDB database. When a client comes to IdentityServer for authentication, the client’ credentials will be checked by ASP.NET Core Identity in the MongoDB database. We are going to implement all this along with a Web API secured with IdentityServer, so sit tight and enjoy this tutorial.

We will create a complete project from scratch and it’s source codes can be downloaded from my GitHub repository.

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 clients 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 are 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 which contains 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 in the MongoDB database by ASP.NET Core Identity, and provide you with 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 managers users, passwords, profile data, roles, claims, tokens, email confirmation, 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 a user. OpenID connect helps client 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 access token (JWT) 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 Web Application project 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 Identity Accounts.

login form

I will proceed with the project ASP.NET Core Identity with MongoDB. I created it on my last tutorial, if you want to know the steps for Identity Integration then definitely visit this tutorial.

Otherwise you can download the source code from this tutorial itself (link given at the bottom) to find the project by the name of “ISExample”. All the IdentityServer configurations (which I will do here) are performed in the same project.

Setup IdentityServer in ASP.NET Core

We will need to install the following 2 packages to our ASP.NET Core app:

  • IdentityServer4
  • IdentityServer4.AspNetIdentity
IdentityServer Packages

Just run the following 2 command on Package Manager Console window to install these pacakges.

Install-Package IdentityServer4
Install-Package IdentityServer4.AspNetIdentity

You can see I have added the packages for the newest version of IdentityServer which is IdentityServer 4. It is time to configure IdentityServer.

IdentityServer settings in appsettings.json

We will keep the settings of IdentityServer in appsettings.json file. The Startup class of the app will read these settings from the appsettings.json and configure IdentityServer with them.

Add these settings shown in highlighted way to your “appsettings.json” file. These will serve as the settings for IdentityServer.

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

We will discuss the 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 on 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 interacting with IdentityServer. Some important ones are:

AllowedGrantTypes : the grant type which the client will use during token request with 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 Clients 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 the IdentityServer. 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 tokesn – IdentityServer provides 2 token for authenticated users. These are Access and Id tokens. The IdToken is a security token that contains information about a user. Access tokens, on the other hand, simply allow access to certain defined 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 and so the value of the ApiScopes will define what scopes the API require to be accessed.

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 the claim in the access token will contains.

"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 contains. 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 your app and add the following code to it.

using IdentityServer4.Models;
using System.Collections.Generic;

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 to the appsettings.json.

There is another property called IdentityResources which will hold the user data like userId, email, a 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 Startup class where we will actually configure IdentityServer.

Configure IdentityServer on Startup.cs

We configure IdentityServer on the ConfigureServices method of Startup class. Add the highlighted code lines given below to your app.

public void ConfigureServices(IServiceCollection services)
{
    var mongoDbSettings = Configuration.GetSection(nameof(MongoDbConfig)).Get<MongoDbConfig>();

    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddMongoDbStores<ApplicationUser, ApplicationRole, Guid>
        (
            mongoDbSettings.ConnectionString, mongoDbSettings.Name
        );

    var identityServerSettings = Configuration.GetSection(nameof(IdentityServerSettings)).Get<IdentityServerSettings>();

    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();

    services.AddControllersWithViews();
}

First we populate the IdentityServerSettings.cs we created earlier with the values stored in appsettings.json. This is done by reading ASP.NET Core appsettings.json from GetSection method of IConfiguration.

var identityServerSettings = Configuration.GetSection(nameof(IdentityServerSettings)).Get<IdentityServerSettings>();

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.

services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
    })
    .AddAspNetIdentity<ApplicationUser>()
    .AddInMemoryApiScopes(identityServerSettings.ApiScopes)
    .AddInMemoryApiResources(identityServerSettings.ApiResources)
    .AddInMemoryClients(identityServerSettings.Clients)
    .AddInMemoryIdentityResources(identityServerSettings.IdentityResources)
    .AddDeveloperSigningCredential();
  • 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 only case 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.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    …

    app.UseRouting();
    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 change the “applicationUrl” given in the launchsettings.json file to 5000 and 5001 ports. You can have any ports you want. So, this means my IdentityServer will run from 5001 port in https. Note – launchsettings.json file inside the Properties folder of the application.

"applicationUrl": "https://localhost:5001;http://localhost:5000"

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

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 my IdentityServer project it’s url is – https://localhost:5001/.well-known/openid-configuration . So, when I open this uri on the browser I am presented with the metadata information in json.

IdentityServer Discovery Endpoint

The json view 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 4 things in the jsons – authorization_endpoint, token_endpoint, scopes_supported, claims_supported and grant_types_supported. See below:

{
    "issuer": "https://localhost:5001",
    "jwks_uri": "https://localhost:5001/.well-known/openid-configuration/jwks",
    "authorization_endpoint": "https://localhost:5001/connect/authorize",
    "token_endpoint": "https://localhost:5001/connect/token",
    "userinfo_endpoint": "https://localhost:5001/connect/userinfo",
    "end_session_endpoint": "https://localhost:5001/connect/endsession",
    "check_session_iframe": "https://localhost:5001/connect/checksession",
    "revocation_endpoint": "https://localhost:5001/connect/revocation",
    "introspection_endpoint": "https://localhost:5001/connect/introspect",
    "device_authorization_endpoint": "https://localhost:5001/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 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 application and name it \ISClient. Make sure to select the Web API template when creating this project.

ASP.NET Core Web API Template

This template 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 runs 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.

"applicationUrl": "https://localhost:6001;http://localhost:6000",

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": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.Authorization": "Information"
    }
  },
  "AllowedHosts": "*"
}

Now we have 2 projects:

  1. IdentityServer project which runs from 5001 port.
  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 Startup.cs class.

using Microsoft.AspNetCore.Authentication.JwtBearer;

Now head on to the ConfigureServices method and add authentication to the service collection.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:5001";
        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.
  • 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.

Finally, on the Config method, 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.

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase

Fetching IdentityServer Token from Postman

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

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:5001/connect/authorize.
  • Set Access Token URL to the value of token_endpoint in the discovery endpoint which is https://localhost:5001/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 Postman Authentication Configurations

Now click the Get New Access Token button. Postman will now 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

I had created the Username and Password in my earlier tutorial, so I log-on with these credentials

.

On clicking the Log In button, we am 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 can see it contains audience, client_id and scopes which we have 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 for 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

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 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

Congratulations, our Web API is protected by IdentityServer and 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. The command is:

Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect

Next, update the AddAuthentication method in the ConfigureServices method to include default scheme to “cookies” and defaultchallengescheme to “oidc”. We also added 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 in highlighted manner.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:5001";
        options.Audience = "IS4API";
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "https://localhost:5001";
        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

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 Bearer authentication on the ConfigureServices method of Startup class if we haven’t done so.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);

Bearer authentication does the following validations.

  1. Make sure the token is coming from a trusted issuer like IdentityServer.
  2. Validates the token is valid to 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 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;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

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);

Next create the Index view which accepts model of type List<WeatherForecast>, it will show this 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
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 *