IdentityServer4 based Authentication Service for a Serenity.is based microservice setup using C#

Gayan Fonseka
7 min readDec 5, 2019

--

Assume a scenario where a microservice architecture based solution had to be provided in a short time frame. Using a framework that had taken care of the systemwide concerns (explained in another article) and that supports code generation will be critical for success. Serenity is one such framework that has most of these concerns covered for you. There are few other advantages in using the framework such as getting a fully developed user interface to manage the users, roles, and permissions (authorization) and being able to use one front-end as the base for the remaining front-ends. Some of the micro-frontends in the solution used the serenity native MVC frontend, some others used ReactJS hosted with the app and, also included in the solution were some React Native mobile apps that needed tokens from an Authentication server for access.

The best option was to extend/ customize the asp.net core identity framework with IdentityServer4, which will provide an Authentication server that would support the existing data and help establish an authentication server that would issue tokens. It had to support the authentication of the MVC based micro-frontends that were part of the serenity framework. The sample Architecture diagram is as follows.

High-level architecture diagram

Authentication Service Setup

As the first step, we’ll look at the work that needs to happen in the Authentication Service. It is assumed that the reader has a basic knowledge of the ASP.Net core-based identity framework and its facility to be extended. The below code from the startup file of the authentication service will provide you a glimpse into what needs to happen,

public void ConfigureServices(IServiceCollection services)
{
.
.
.
services.AddScoped<IPasswordHasher<ApplicationUser>, CustomPasswordHasher<ApplicationUser>>();
services.AddTransient<IUserStore<ApplicationUser>, UserStore>();
services.AddTransient<IRoleStore<ApplicationRole>, RoleStore>();
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
}
)
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants() //Avoid using InMemory for actual .AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<IdentityProfileService>();
.
.
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseIdentityServer();
}

As you can see, an AppplicationUser Class, UserStore Class, RoleStore Class, CustomPasswordHasher and an IdenitityProfileService Class of our own must be written. (Please refer the IdentityServer documentation for clarity regarding IdentityServer4 ). In an actual implementation scenario, the configuration details have to be persisted properly ideally in a database and identity server has an entity framework(EF) implementation for that purpose as a NuGet package that could be easily used. By scaffolding, you can get a UI generated for configuration easily. What, why, how of these configurations are discussed in a separate article. My expectation for this article is to show the entire spectrum of customizations at a high level, that needs to happen to get the authentication going with the serenity backend.

The ApplicationUser Class could be a collection of user properties as shown below.

public class ApplicationUser
{
public int UserId { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string DisplayName { get; set; }
public string PasswordHash { get; set; }
public string PasswordSalt { get; set; }
public int UserTypeId { get; set; }
public string Email { get; set; }
public string Mobile { get; set; }
public string Zip { get; set; }
public bool EmailVerified { get; set; }
.
.
.
.
}

Implementing the UserStore is somewhat tricky and the Interfaces that must be implemented have to be decided based on the customizations that are required for the UserStore methods. Please refer to the IdentiryServer4 / Microsoft Identity framework documentation to understand the interface implementations in detail. I did the following implementation,

public class UserStore: IUserStore<ApplicationUser>, IUserPasswordStore<ApplicationUser>,   IUserLoginStore<ApplicationUser>, IUserEmailStore<ApplicationUser>
{
// to be filled by you
}

RoleStore implementation was straightforward as there was only a single interface to be implemented,

public class RoleStore : IRoleStore<ApplicationRole>
{
//to be filled by you
}

Now we have an interesting implementation to be done, which is the CustomPasswordHasher() Class, which makes it possible to work with Serenity generated Hashes. Serenity uses SHA512 algorithm for password hashing which is not the default used by asp.net identity. Asp.Net uses the following,

ASP.NET Identity Version 2: PBKDF2 with HMAC-SHA1, 128-bit salt

ASP.NET Core Identity Version 3: PBKDF2 with HMAC-SHA256, 128-bit salt

So, the questions arise, how can we figure if it is SHA256 or SHA512? We have a simple mechanism to do that. We could check the length of the hashed password as it varies depending on the algorithm used. Below implementation of the class will explain this well (Make sure you do your validations),

public class CustomPasswordHasher<TUser> : PasswordHasher<TUser> where TUser : ApplicationUser{public override PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword){
//Logic to check if the hash is generated by SHA512
if (hashedPassword.Length > 84){byte[] buffer = System.Text.Encoding.UTF8.GetBytes(providedPassword + user.PasswordSalt);
var sha512 = SHA512.Create();
buffer = sha512.ComputeHash(buffer);
string passwordHash1 = Convert.ToBase64String(buffer).Substring(0, 86);
string passwordHash2 = Convert.ToBase64String(buffer).Substring(0, 88);if (hashedPassword == passwordHash1)
{
return PasswordVerificationResult.Success;
}
else if (hashedPassword == passwordHash2)
{
return PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
}
else
{
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}
}
}

As you can see, I am converting to Base64 with two lengths 86 and 88. That is because some of the existing passwords had salts and some didn’t.

One other key factor that you should be aware of is the need to have a security helper class that would do the salt generation and hash creation as support for the implementation of ChangePassword, etc in the UserStore() Class. You may check the code for this class in the serenity codebase in GitHub and modify it as required as shown below.

public static class CustomSecurityHelper
{
public static string Encode(byte[] bytes)
{
const string base32Chars = "abcdefghijklmnopqrstuvwxyz234567";
if (bytes == null)
throw new ArgumentNullException("bytes");
int len = bytes.Length;
StringBuilder base32 = new StringBuilder((len * 8 + 4) / 5);
int currByte, digit, i = 0;
while (i < len)
{
// INVARIANTS FOR EACH STEP n in [0..5[; digit in [0..31[;
// The remaining n bits are already aligned on top positions
// of the 5 least bits of digit, the other bits are 0.
////// STEP n = 0; insert new 5 bits, leave 3 bits
currByte = bytes[i++] & 255;
base32.Append(base32Chars[currByte >> 3]);
digit = (currByte & 7) << 2;
if (i >= len)
{ // put the last 3 bits
base32.Append(base32Chars[digit]);
break;
}
////// STEP n = 3: insert 2 new bits, then 5 bits, leave 1 bit
currByte = bytes[i++] & 255;
base32.Append(base32Chars[digit | (currByte >> 6)]);
base32.Append(base32Chars[(currByte >> 1) & 31]);
digit = (currByte & 1) << 4;
if (i >= len)
{ // put the last 1 bit
base32.Append(base32Chars[digit]);
break;
}
////// STEP n = 1: insert 4 new bits, leave 4 bit
currByte = bytes[i++] & 255;
base32.Append(base32Chars[digit | (currByte >> 4)]);
digit = (currByte & 15) << 1;
if (i >= len)
{ // put the last 4 bits
base32.Append(base32Chars[digit]);
break;
}
////// STEP n = 4: insert 1 new bit, then 5 bits, leave 2 bits
currByte = bytes[i++] & 255;
base32.Append(base32Chars[digit | (currByte >> 7)]);
base32.Append(base32Chars[(currByte >> 2) & 31]);
digit = (currByte & 3) << 3;
if (i >= len)
{ // put the last 2 bits
base32.Append(base32Chars[digit]);
break;
}
///// STEP n = 2: insert 3 new bits, then 5 bits, leave 0 bit
currByte = bytes[i++] & 255;
base32.Append(base32Chars[digit | (currByte >> 5)]);
base32.Append(base32Chars[currByte & 31]);
//// This point is reached for len multiple of 5
}
string s = base32.ToString();
return s;
}
public static string RandomFileCode()
{
Guid guid = Guid.NewGuid();
var guidBytes = guid.ToByteArray();
var eightBytes = new byte[8];
for (int i = 0; i < 8; i++)
eightBytes[i] = (byte)(guidBytes[i] ^ guidBytes[i + 8]);
return Encode(eightBytes);
}
public static string ComputeSHA512(string s)
{
if (string.IsNullOrEmpty(s))
throw new ArgumentNullException();
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(s);
#if COREFX
var sha512 = SHA512.Create();
#else
var sha512 = System.Security.Cryptography.SHA512Managed.Create();

#endif
buffer = sha512.ComputeHash(buffer);return System.Convert.ToBase64String(buffer).Substring(0, 86); // strip padding
}
public static string GenerateSalt()
{
//your single line implemenation
return salt;
}
public static string CalculateHash(string password)
{
//your single line implementation
return CalculateHash(password, salt);
}
public static string CalculateHash(string password, string salt)
{
return ComputeSHA512(password + salt);
}
}

IdentityProfileService class helps in enabling the claims. It is an implementation of the IProfileService interface.

This sums up the work that needs to happen at the identity service end. Some other features that can come handy in the service include adding support for other well-known authentication providers such as Google, Facebook and etc.

Other Back-end service setup

All the other backends too have to implement a mechanism to consume the tokens/cookies for security purposes. This brings up the need to have a dual authentication schema where both are required (that is when a single back-end service supports default serenity pages, Javascript-based SPAs, and APIs for mobile consumption ). Here I am using the default settings provided by IdentityServer and also you need to have a relevant client configuration in the authentication server for this to work.

services.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}
)
.AddCookie(o =>
{
o.Cookie.Name = “.AspNetAuth”;
o.LoginPath = new PathString(“/Account/Login/”);
o.AccessDeniedPath = new PathString(“/Account/AccessDenied”);
o.Cookie = new CookieBuilder()
{
Path = “/”
};
}
)
.AddIdentityServerAuthentication( o =>
{
o.Authority = “ https://auth.abc”; // URL of the hosted authentication server
o.RequireHttpsMetadata = false;
o.ApiName = “ xyz “; // API Name of the client
});

A user in a single web session might access various APIs and hence it is a good idea to enable caching to avoid round trips to identity server, every time there is a request from a known user. For this, you can modify the above as follows,

.AddIdentityServerAuthentication( o =>
{
o.Authority = “ https://auth.abc”; // URL of the hosted authentication server
o.RequireHttpsMetadata = false;
o.ApiName = “ xyz “; // API Name of the client
o.EnableCaching = true;
o.CacheDuration = TimeSpan.FromMinutes(Convert.ToDouble(“200”));
}

Now that service configuration is done it is time to consume. When the API is consumed only by the default serenity pages it is good enough to have the attribute

[Authorize]

but when it is consumed by SPA or mobile the schema has to be specified clearly for it to work as below,

[Authorize(AuthenticationSchemes = "Bearer")]

That is it to get up and running with an authentication service based on IdentityServer4 and serenity back-end. This works well in the service setup mentioned at the beginning. Thanks for reading and feel free to share.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Gayan Fonseka
Gayan Fonseka

No responses yet

Write a response