Integrate IdentityServer 4.1 With ASP.NET Core 3.1 (Part 1-Server Configuration)

Currently we have so many applications which can be accessible via web browser as well as devices such as tablets and mobiles. All of these applications consume web API for various purpose which interacts with one or more databases. We can’t allow anyone to access our API to get things done. We need to restrict people who access the API as well as we need to have limitations over devices as well where those devices are limited to certain functionality. We are going to learn about how to achieve above mentioned requirements in three parts.

  1. Building Identity Server
  2. Securing web API with Identity Server
  3. Access web API through swagger

Let’s jump into first part to build Identity Server which will facilitate the security of the API we are building together during this article 😃

Building Identity Server
First most we need to understand what are scopes and resources in Identity Server

Relationship between API Scopes and API Resources

IdentityResources are the scopes that will be included in the ID-token
ApiScopes is what you ask for as a client and as a user you give permission to. In other words, one or more ApiResources can associated with one ore more ApiScopes.

The ApiScope and ApiResources controls what is included in the access token. ApiResources points out what the aud claim in the access token will contains.

ApiResources gives us options to control what claims needs to be in the access token.

Identity Server 4 supports following grant types.

  • Implicit
  • Authorization code
  • Hybrid
  • Client credentials
  • Resource owner password
  • Device flow
  • Refresh tokens
  • Extension grants

I’m going to cover Resource owner password grant type in this section. Let’s move to the implementation.

  1. Install Identity server 4 nuget package
Install-Package IdentityServer4 -Version 4.1.1

2. Create a empty ASP.NET Core Web application

3. Configure APIClients, APIScopes and APIResources in a static class ‘Config’

public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("webAPI", "WebAPI"),
new ApiScope("UserMgtAPI", "UserManagementAPI")
};
}public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientName="Web API client",
ClientId = "apiClient",
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("automartSecret".Sha256())
},

// scopes that client has access to
AllowedScopes = { "webAPI", "UserMgtAPI", "openid", "profile" }
}
};
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource
{
Name = "webAPI",
// secret for using introspection endpoint
ApiSecrets =
{
new Secret("automartSecret".Sha256())
},
// include the following using claims in access token (in addition to subject id)
UserClaims = {
JwtClaimTypes.Name,
"FirstName",
"LastName",
JwtClaimTypes.Email,
JwtClaimTypes.Role,
"UserVerified",
"Username",
"user_id"
},
// this API defines two scopes
Scopes = { "webAPI","openid", "profile" }
}
};

4. Create a class called ‘ResourceOwnerPasswordValidator’ extending ‘IResourceOwnerPasswordValidator’ interface

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private UserManager _userManager;
public ResourceOwnerPasswordValidator(UserManager userManager)
{
_userManager = userManager;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
//get your user model from db (by username - in my case its email)
var user = await _userManager.FindUserByNameAsync(context.UserName);
if (user != null)
{
//check if password match - remember to hash password if stored as hash in db
if (await _userManager.CheckPasswordAsync(user, context.Password))
{
//set the result
context.Result = new GrantValidationResult(
subject: user.UserId.ToString(),
authenticationMethod: "custom",
claims: await _userManager.GetClaimsAsync(user));
return;
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Incorrect password");
return;
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not exist.");
return;
}
catch (Exception ex)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password");
Console.WriteLine(ex);
}
}
}

This class has a method called ValidateAsync which is used to compare the username and password provided when requesting for a access token. I have a class called UserManager which is used to get users from database for comparison.

5. Create a class called ‘ProfileService’ extending ‘IProfileService’ interface

public class ProfileService : IProfileService
{
private UserManager _userManager;
public ProfileService(UserManager userManager)
{
_userManager = userManager;
}

public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
try
{
//depending on the scope accessing the user data.
if (!string.IsNullOrEmpty(context.Subject.Identity.Name))
{
//get user from db (in my case this is by email)
var user = await _userManager.FindUserByNameAsync(context.Subject.Identity.Name);
if (user != null)
{
var claims = await _userManager.GetClaimsAsync(user);
//set issued claims to return
context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
}
}
else
{
//get subject from context (this was set ResourceOwnerPasswordValidator.ValidateAsync),
//where and subject was set to my user id.
var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");
if (!string.IsNullOrEmpty(userId?.Value))
{
//get user from db (find user by user id)
var user = await _userManager.FindUserByUserIdAsync(int.Parse(userId.Value));
// issue the claims for the user
if (user != null)
{
var claims = await _userManager.GetClaimsAsync(user);
context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
public async Task IsActiveAsync(IsActiveContext context)
{
try
{
//get subject from context (set in ResourceOwnerPasswordValidator.ValidateAsync),
var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "user_id");
if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
{
var user = await _userManager.FindUserByUserIdAsync(int.Parse(userId.Value));
if (user != null)
{
if (user.IsActive)
{
context.IsActive = user.IsActive;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}

This class is used to get user details from database, retrieve the claims of the user and set only the IssuedClaims which are matching the requested claims of the APIResources. In other words, even though user has lots of claims associated to him only the claims defined in the APIResources are set to access token.

IProfileService also has a method called ‘IsActiveAsync’ which is used to set IsActive property of the user context.

6. Injecting all the services and providers in Startup

Add following codes inside ConfigureServices section

// Register database provider
services.Add(new ServiceDescriptor(typeof(IDbProvider), DBProviderFactory.GetProvider(ProviderTypes.MySql, Configuration.GetConnectionString("DBConnection"))));
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryClients(Config.Clients);
//Inject the classes we just created
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
services.AddTransient<IProfileService, ProfileService>();
services.AddScoped<UserManager>();
services.AddScoped<IUserRepository, UserRepository>();

AddDeveloperSigningCredential can be used only when Identity Server host is running on a SINGLE machine, for production you need to use AddSigningCredential. In order to setup this please follow this “How to set and renew AddSigningCredential after 30 days”

Add following codes inside Configure section

app.UseIdentityServer();

That’s all for implementation and configuration 😤

Now you can run the Identity Server and use POSTMAN to call request for token

  1. Header
    * Content-Type : application/x-www-form-urlencoded
  2. Body
    * grant_type- password
    * username- <<username>>
    * password- <<password>>
    * scope- webAPI
    * client_id- apiClient
    * client_secret- <<secret>>
  3. URL
    * <api_endpoint>/connect/token POST

Please find the sample token request and response below.

This concludes part 1 “Building Identity Server” of the article. 😄

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store