This post cover Blazor WebAssembly Authentication with some customizations, allow full control over authentication process.
UPDATE 28/06/2021 – Project updated and published on GitHub: https://github.com/CodeDesignTips/CustomBlazorAuthentication
The main reason i design this authentication is because the default authentication on Blazor (Identity server 4) had some drawbacks:
- Does not allow integrate custom database or custom db schema
- Does not allow custom authentication interface (yes, you can with scaffolding but you need to use server side rendering with old cshtml razor pages)
- Does not allow full control over authentication process
The following implementation support:
- ASP.NET Core Identity Authentication for Blazor WebAssembly
- Custom database provider (this sample use Sql Server but you can integrate any other)
- Custom database schema
- Any database framework (you can use EntityFramework, Dapper, ADO.NET or any other)
- Full control over authentication query
- JWT Authentication
- Custom user interface
Table of contents:
- Init Blazor WebAssembly project
- Create user and role model
- Define password salt and hash generation
- Configure JWT parameters in appSettings.json
- Define the data layer
- Create the authentication service
- Create the users service
- Create the authentication controller
- Create the users controller
- Customize ASP.NET Core Identity
- Update server Startup.cs to configure the authentication
- Implement client custom AuthenticationStateProvider
- Implement client HttpService
- Update razor pages
- Conclusions
- References
Init Blazor WebAssembly project
Let’s start creating a new Blazor App from Visual Studio:
Select Blazor WebAssembly App and make sure to check “ASP.NET Core hosted”
Create user and role Model
Create user and role model with the properties based on your db schema:
public static partial class Model
{
public class User
{
#region Properties
/// <summary>
/// User id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// Username
/// </summary>
[Required]
public string UserName { get; set; }
/// <summary>
/// Password
/// </summary>
[Required]
public string Password { get; set; }
/// <summary>
/// Password salt
/// </summary>
[SwaggerIgnore]
public string PasswordSalt { get; set; }
/// <summary>
/// Check if the password is encrypted
/// </summary>
[SwaggerIgnore]
public bool IsPasswordEncrypted { get; set; }
/// <summary>
/// Password confirm
/// </summary>
[Required]
[Compare("Password")]
[SwaggerIgnore]
public string PasswordConfirm { get; set; }
/// <summary>
/// Email
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// User role
/// </summary>
public UserRole UserRole { get; set; }
/// <summary>
/// Name
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// Surname
/// </summary>
[Required]
public string Surname { get; set; }
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public User()
{
UserRole = UserRole.User;
}
#endregion
}
}
public static partial class Model
{
public class Role
{
#region Properties
/// <summary>
/// Role Id
/// </summary>
public Guid RoleId { get; set; }
/// <summary>
/// Role name
/// </summary>
public string RoleName { get; set; }
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public Role()
{
}
#endregion
}
}
Define password salt and hash generation
public static partial class Utils
{
#region Password hashing
/// <summary>
/// Return the salt to use in password encryption
/// </summary>
/// <returns>Salt to use in password encryption</returns>
public static string GeneratePasswordSalt()
{
//base64 salt length:
//You need 4*(n/3) chars to represent n bytes, and this needs to be rounded up to a multiple of 4.
var size = 48; // = base64 string of 64 chars
var random = new RNGCryptoServiceProvider();
var salt = new byte[size];
random.GetBytes(salt);
return Convert.ToBase64String(salt);
}
/// <summary>
/// Return hash of salt + password
/// </summary>
/// <param name="password">Password</param>
/// <param name="salt">Salt</param>
/// <returns>Hash of (salt + password)</returns>
public static string GetPasswordHash(string password, string salt)
{
//https://crackstation.net/hashing-security.htm
//http://www.codeproject.com/Questions/1063132/How-to-Match-Hash-with-Salt-Password-in-Csharp
//base64 salt length:
//You need 4*(n/3) chars to represent n bytes, and this needs to be rounded up to a multiple of 4.
//512bit = 64bytes => stringa base64 di 88 caratteri
var combinedPassword = string.Concat(salt, password);
var bytes = new UTF8Encoding().GetBytes(combinedPassword);
byte[] hashBytes;
using (var algorithm = new System.Security.Cryptography.SHA512Managed())
{
hashBytes = algorithm.ComputeHash(bytes);
}
return Convert.ToBase64String(hashBytes);
}
#endregion
}
Configure JWT Parameters in appSettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=SERVERNAME;Database=BlazorAuth;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"JwtSecurityKey": "RANDOM_KEY_MUST_NOT_BE_SHARED",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 1
}
Define the data layer
Data layer is implemented in DataLayer project, it contain a base class and 2 derived classes for Sql Server and Oracle code. This sample project implement only the structure definition using an in-memory storage to manage users.
A default user: demo with password demo is created by default.
Create the authentication service
Create the authentication service class in the ServiceLayer project
public class AuthenticationService: BaseService
{
#region Private members
/// <summary>
/// Configuration settings
/// </summary>
private readonly IConfiguration configuration;
/// <summary>
/// Authentication manager
/// </summary>
private readonly SignInManager<Model.User> signInManager;
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="configuration">Configuration</param>
/// <param name="signInManager">Authentication manager</param>
public AuthenticationService(IConfiguration configuration, SignInManager<Model.User> signInManager)
{
this.configuration = configuration;
this.signInManager = signInManager;
}
#endregion
#region Public methods
/// <summary>
/// Manage the authentication
/// </summary>
/// <param name="request">Authentication info</param>
/// <returns>Request result</returns>
public async Task<(bool Result, string AccessToken)> LoginAsync(string userName, string password)
{
var accessToken = "";
var ret = false;
try
{
var user = await signInManager.UserManager.FindByNameAsync(userName);
ret = user != null && user.Password == Utils.GetPasswordHash(password, user.PasswordSalt);
if (!ret)
HandleError("User name or password not valid!");
if (ret)
{
await signInManager.SignInAsync(user, false);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.UserRole.ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JwtSecurityKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expiry = DateTime.Now.AddDays(Convert.ToInt32(configuration["JwtExpiryInDays"]));
var token = new JwtSecurityToken(
configuration["JwtIssuer"],
configuration["JwtAudience"],
claims,
expires: expiry,
signingCredentials: creds
);
accessToken = new JwtSecurityTokenHandler().WriteToken(token);
}
}
catch(Exception ex)
{
ret = false;
HandleError(ex.Message);
}
return (ret, accessToken);
}
/// <summary>
/// Manage user logout
/// </summary>
/// <returns>Request result</returns>
public async Task<bool> LogoutAsync()
{
var ret = false;
try
{
await signInManager.SignOutAsync();
ret = true;
}
catch (Exception ex)
{
HandleError(ex.Message);
}
return ret;
}
#endregion
}
Create the users service
Create the users service class in the ServiceLayer Project
public class UsersService: BaseService
{
#region Private members
/// <summary>
/// User service
/// </summary>
private UserManager<Model.User> userManager;
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="userManager">User manager</param>
public UsersService(UserManager<Model.User> userManager)
{
this.userManager = userManager;
}
#endregion
#region Public methods
/// <summary>
/// Manage user registration
/// </summary>
/// <param name="user">Use info</param>
/// <returns>Request result</returns>
public async Task<bool> InsertAsync(Model.User user)
{
var ret = false;
var errorMessage = "";
try
{
var createResult = await userManager.CreateAsync(user);
ret = createResult.Succeeded;
if (!ret)
{
foreach (var error in createResult.Errors)
{
if (!string.IsNullOrEmpty(errorMessage))
errorMessage += "\n";
errorMessage += error.Description;
}
HandleError(errorMessage);
}
}
catch (Exception ex)
{
HandleError(ex.Message);
}
return ret;
}
/// <summary>
/// Manage user delete
/// </summary>
/// <param name="userId">Use id</param>
/// <returns>Request result</returns>
public async Task<bool> RemoveAsync(Guid userId)
{
var ret = false;
var errorMessage = "";
try
{
var user = await userManager.FindByIdAsync(userId.ToString());
ret = user != null;
if (!ret)
HandleError("User not found!");
else
{
ret = false;
var deleteResult = await userManager.DeleteAsync(user);
ret = deleteResult.Succeeded;
if (!ret)
{
foreach (var error in deleteResult.Errors)
{
if (!string.IsNullOrEmpty(errorMessage))
errorMessage += "\n";
errorMessage += error.Description;
}
HandleError(errorMessage);
}
}
}
catch (Exception ex)
{
HandleError(ex.Message);
}
return ret;
}
#endregion
}
Create the authentication controller
Add a new Api controller named AuthenticationController.
[Route("[controller]/[action]")]
public class AuthenticationController : BaseController
{
#region Private members
/// <summary>
/// Authentication service
/// </summary>
private readonly AuthenticationService authenticationService;
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="configuration">Configuration settings</param>
/// <param name="signInManager">Authentication manager</param>
public AuthenticationController(IConfiguration configuration, SignInManager<Model.User> signInManager)
{
authenticationService = new AuthenticationService(configuration, signInManager);
}
#endregion
#region Methods
/// <summary>
/// Manage user authentication
/// </summary>
/// <param name="request">Authentication info</param>
/// <returns>Request result</returns>
/// <response code="200">Request completed successfully</response>
/// <response code="400">Request failed</response>
[HttpPost]
[ProducesResponseType(typeof(Model.LoginResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Model.LoginResponse), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Login(Model.LoginRequest request)
{
var result = await authenticationService.LoginAsync(request.UserName, request.Password);
var loginResponse = new Model.LoginResponse
{
Result = result.Result,
AccessToken = result.AccessToken,
ErrorMessage = authenticationService.ErrorMessage
};
if (!result.Result)
return BadRequest(loginResponse);
return Ok(loginResponse);
}
/// <summary>
/// Manage user logout
/// </summary>
/// <returns>Request result</returns>
/// <response code="200">Request completed successfully</response>
/// <response code="400">Request failed</response>
[Authorize]
[HttpPost]
[ProducesResponseType(typeof(Model.LogoutResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Model.LogoutResponse), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Logout()
{
var errorMessage = "";
var ret = await authenticationService.LogoutAsync();
if (!ret)
errorMessage = authenticationService.ErrorMessage;
var result = new Model.LogoutResponse
{
Result = ret,
ErrorMessage = errorMessage
};
if (!result.Result)
return BadRequest(result);
return Ok(result);
}
#endregion
}
Create the users controller
[Route("[controller]")]
public class UsersController : BaseController
{
#region Private members
/// <summary>
/// User service
/// </summary>
private UsersService usersService;
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="userManager">User manager</param>
public UsersController(UserManager<Model.User> userManager)
{
usersService = new UsersService(userManager);
}
#endregion
#region Methods
/// <summary>
/// Manage user insert
/// </summary>
/// <param name="user"></param>
/// <returns>Request result</returns>
/// <response code="200">Request completed successfully</response>
/// <response code="400">Request failed</response>
[HttpPost]
[ProducesResponseType(typeof(Model.UsersPostResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Model.UsersPostResponse), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Post(Model.User user)
{
if (UserRole == Model.UserRole.User)
user.UserRole = Model.UserRole.User;
var errorMessage = "";
var ret = await usersService.InsertAsync(user);
if (!ret)
errorMessage = usersService.ErrorMessage;
var result = new Model.UsersPostResponse
{
Result = ret,
ErrorMessage = errorMessage
};
if (!result.Result)
return BadRequest(result);
return Ok(result);
}
/// <summary>
/// Manage user delete
/// </summary>
/// <param name="userId">User id</param>
/// <returns>Request result</returns>
/// <response code="200">Request completed successfully</response>
/// <response code="400">Request failed</response>
[Authorize(Roles = "Administrator")]
[HttpDelete]
[ProducesResponseType(typeof(Model.UsersPostResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Model.UsersPostResponse), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Delete(Guid userId)
{
var errorMessage = "";
var ret = await usersService.RemoveAsync(userId);
if (!ret)
errorMessage = usersService.ErrorMessage;
var result = new Model.UsersDeleteResponse
{
Result = ret,
ErrorMessage = errorMessage
};
if (!result.Result)
return BadRequest(result);
return Ok(result);
}
#endregion
}
Customize ASP.NET Core Identity
To allow custom db and custom db schema we have to implement a class for the ASP.NET identity interfaces IUserStore and IPassworHasher.
Implement UserStore class:
public static partial class CustomIdentity
{
public class UserStore : IUserStore<Model.User>, IUserPasswordStore<Model.User>
{
#region Properties
/// <summary>
/// Provider name
/// </summary>
protected string ProviderName { get; private set; }
/// <summary>
/// Connection string
/// </summary>
protected string ConnectionString { get; private set; }
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public UserStore() : base()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="providerName">Provider name</param>
/// <param name="connectionString">Connection string</param>
public UserStore(string providerName, string connectionString)
{
ProviderName = providerName;
ConnectionString = connectionString;
}
#endregion
#region Methods
//
// Summary:
// Creates the specified user in the user store.
//
// Parameters:
// user:
// The user to create.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the Microsoft.AspNetCore.Identity.IdentityResult of the creation operation.
public Task<IdentityResult> CreateAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (!user.IsPasswordEncrypted)
{
user.PasswordSalt = Utils.GeneratePasswordSalt();
user.Password = Utils.GetPasswordHash(user.Password, user.PasswordSalt);
user.IsPasswordEncrypted = true;
}
var ret = false;
var errorMessage = "";
using (var db = DbLayer.CreateObject(ProviderName, ConnectionString))
{
ret = db.InsertUser(user);
if (!ret)
errorMessage = db.ErrorMessage;
}
if (ret)
return Task.FromResult(IdentityResult.Success);
return Task.FromResult(IdentityResult.Failed(new IdentityError { Description = errorMessage }));
}
//
// Summary:
// Deletes the specified user from the user store.
//
// Parameters:
// user:
// The user to delete.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the Microsoft.AspNetCore.Identity.IdentityResult of the update operation.
public Task<IdentityResult> DeleteAsync(Model.User user, CancellationToken cancellationToken)
{
//Not supported
return Task.FromResult(IdentityResult.Failed());
}
//
// Summary:
// Finds and returns a user, if any, who has the specified userId.
//
// Parameters:
// userId:
// The user ID to search for.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the user matching the specified userId if it exists.
public Task<Model.User> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (!Guid.TryParse(userId, out var userIdValue))
throw new ArgumentException("Not a valid Guid id", nameof(userId));
using (var db = DbLayer.CreateObject(ProviderName, ConnectionString))
{
var user = db.GetUser(userId);
return Task.FromResult(user);
}
}
//
// Summary:
// Finds and returns a user, if any, who has the specified normalized user name.
//
// Parameters:
// normalizedUserName:
// The normalized user name to search for.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the user matching the specified normalizedUserName if it exists.
public Task<Model.User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var db = DbLayer.CreateObject(ProviderName, ConnectionString))
{
var user = db.GetUser(normalizedUserName);
return Task.FromResult(user);
}
}
//
// Summary:
// Gets the normalized user name for the specified user.
//
// Parameters:
// user:
// The user whose normalized name should be retrieved.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the normalized user name for the specified user.
public Task<string> GetNormalizedUserNameAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.UserName);
}
//
// Summary:
// Gets the user identifier for the specified user.
//
// Parameters:
// user:
// The user whose identifier should be retrieved.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the identifier for the specified user.
public Task<string> GetUserIdAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.UserId.ToString());
}
//
// Summary:
// Gets the user name for the specified user.
//
// Parameters:
// user:
// The user whose name should be retrieved.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the name for the specified user.
public Task<string> GetUserNameAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
return Task.FromResult(user.UserName);
}
//
// Summary:
// Sets the given normalized name for the specified user.
//
// Parameters:
// user:
// The user whose name should be set.
//
// normalizedName:
// The normalized name to set.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation.
public Task SetNormalizedUserNameAsync(Model.User user, string normalizedName, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
return Task.FromResult<object>(null);
}
//
// Summary:
// Sets the given userName for the specified user.
//
// Parameters:
// user:
// The user whose name should be set.
//
// userName:
// The user name to set.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation.
public Task SetUserNameAsync(Model.User user, string userName, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
user.UserName = userName;
return Task.FromResult<object>(null);
}
//
// Summary:
// Updates the specified user in the user store.
//
// Parameters:
// user:
// The user to update.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, containing
// the Microsoft.AspNetCore.Identity.IdentityResult of the update operation.
public Task<IdentityResult> UpdateAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
//Not supported
return Task.FromResult(IdentityResult.Failed());
}
//
// Summary:
// Gets the password hash for the specified user.
//
// Parameters:
// user:
// The user whose password hash to retrieve.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, returning
// the password hash for the specified user.
public Task<string> GetPasswordHashAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(user.Password))
throw new ArgumentNullException(nameof(user.Password));
if (user.IsPasswordEncrypted)
return Task.FromResult(user.Password);
user.PasswordSalt = Utils.GeneratePasswordSalt();
user.Password = Utils.GetPasswordHash(user.Password, user.PasswordSalt);
user.IsPasswordEncrypted = true;
return Task.FromResult(user.Password);
}
//
// Summary:
// Gets a flag indicating whether the specified user has a password.
//
// Parameters:
// user:
// The user to return a flag for, indicating whether they have a password or not.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation, returning
// true if the specified user has a password otherwise false.
public Task<bool> HasPasswordAsync(Model.User user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
return Task.FromResult(!string.IsNullOrEmpty(user.Password));
}
//
// Summary:
// Sets the password hash for the specified user.
//
// Parameters:
// user:
// The user whose password hash to set.
//
// passwordHash:
// The password hash to set.
//
// cancellationToken:
// The System.Threading.CancellationToken used to propagate notifications that the
// operation should be canceled.
//
// Returns:
// The System.Threading.Tasks.Task that represents the asynchronous operation.
public Task SetPasswordHashAsync(Model.User user, string passwordHash, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
throw new ArgumentNullException(nameof(user));
user.Password = passwordHash;
user.IsPasswordEncrypted = true;
return Task.FromResult<object>(null);
}
public void Dispose()
{
}
#endregion
}
}
Implement PasswordHasher class:
public static partial class CustomIdentity
{
public class PasswordHasher : IPasswordHasher<Model.User>
{
//
// Summary:
// Returns a hashed representation of the supplied password for the specified user.
//
// Parameters:
// user:
// The user whose password is to be hashed.
//
// password:
// The password to hash.
//
// Returns:
// A hashed representation of the supplied password for the specified user.
public string HashPassword(Model.User user, string password)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(password))
throw new ArgumentNullException(nameof(password));
user.PasswordSalt = Utils.GeneratePasswordSalt();
user.Password = Utils.GetPasswordHash(password, user.PasswordSalt);
user.IsPasswordEncrypted = true;
return user.Password;
}
//
// Summary:
// Returns a Microsoft.AspNetCore.Identity.PasswordVerificationResult indicating
// the result of a password hash comparison.
//
// Parameters:
// user:
// The user whose password should be verified.
//
// hashedPassword:
// The hash value for a user's stored password.
//
// providedPassword:
// The password supplied for comparison.
//
// Returns:
// A Microsoft.AspNetCore.Identity.PasswordVerificationResult indicating the result
// of a password hash comparison.
//
// Remarks:
// Implementations of this method should be time consistent.
public PasswordVerificationResult VerifyHashedPassword(Model.User user, string hashedPassword, string providedPassword)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(hashedPassword))
throw new ArgumentNullException(nameof(hashedPassword));
if (string.IsNullOrEmpty(providedPassword))
throw new ArgumentNullException(nameof(providedPassword));
var password = Utils.GetPasswordHash(providedPassword, user.PasswordSalt);
if (password.Equals(hashedPassword))
return PasswordVerificationResult.Success;
return PasswordVerificationResult.Failed;
}
}
}
Update server Startup.cs to configure the authentication
public class Startup
{
#region Properties
/// <summary>
/// Configuration
/// </summary>
public IConfiguration Configuration { get; }
/// <summary>
/// Provider name
/// </summary>
public string ProviderName
{
get
{
return "System.Data.SqlClient";
}
}
/// <summary>
/// Connection string
/// </summary>
public string ConnectionString
{
get
{
return Configuration["ConnectionStrings:DefaultConnection"];
}
}
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="configuration"></param>
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
#endregion
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//ASP.NET Core Identity Authentication
var identityBuilder = services.AddIdentity<Model.User, Model.Role>(options => {
//Password validation criteria
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
identityBuilder.AddDefaultTokenProviders();
//UserStore management
services.AddTransient<IPasswordHasher<Model.User>, CustomIdentity.PasswordHasher>();
services.AddTransient<IUserStore<Model.User>, CustomIdentity.UserStore>(obj => new CustomIdentity.UserStore(ProviderName, ConnectionString));
services.AddTransient<IRoleStore<Model.Role>, CustomIdentity.RoleStore>(obj => new CustomIdentity.RoleStore(ProviderName, ConnectionString));
//JWT Bearer token authentication
services.AddAuthentication(options => {
//Set default JwtBearer authentication
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
});
//Set Default JwtBearer authorization
services.AddAuthorization(options => {
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
//Configure json serialization
services.AddControllersWithViews()
.AddJsonOptions(options => {
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.DictionaryKeyPolicy = null;
});
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
Implement client custom AuthenticationStateProvider
In the client side we have to implement a custom Authentication state provider, so we add a class who inherit AutenticatinStateProvider
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
#region Private members
/// <summary>
/// HttpService to manage http requests
/// </summary>
private readonly HttpService httpService;
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="httpService">HttpService to manage http requests</param>
public CustomAuthenticationStateProvider(HttpService httpService)
{
this.httpService = httpService;
}
#endregion
#region Public methods
/// <summary>
/// Return authentication info
/// </summary>
/// <returns>Authentication info</returns>
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var accessToken = await httpService.Authentication.GetAccessTokenAsync();
if (string.IsNullOrWhiteSpace(accessToken))
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(Utils.ParseClaimsFromJwt(accessToken), "jwt")));
}
/// <summary>
/// Manage user login
/// </summary>
/// <param name="loginRequest"></param>
/// <returns>Login response info</returns>
public async Task<Model.LoginResponse> LoginAsync(Model.LoginRequest loginRequest)
{
var result = await httpService.Authentication.LoginAsync(loginRequest);
if (result.Result)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, loginRequest.UserName) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
return result;
}
/// <summary>
/// Manage user logout
/// </summary>
/// <returns>Logout response info</returns>
public async Task<Model.LogoutResponse> LogoutAsync()
{
var result = await httpService.Authentication.LogoutAsync();
if (result.Result)
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
return result;
}
/// <summary>
/// Manage user registration
/// </summary>
/// <param name="user">User info</param>
/// <returns>Request result</returns>
public async Task<Model.UsersPostResponse> RegisterAsync(Model.User user)
{
return await httpService.Users.PostAsync(user);
}
#endregion
}
Implement client http service
Add a class to implement the http service
public partial class AuthenticationHttpService: HttpService
{
#region Private members
protected readonly ILocalStorageService m_localStorage;
#endregion
#region Properties
/// <summary>
/// Indicates whether to use LocalStorage to read / store access token
/// </summary>
public bool UseLocalStorageForAccessToken { get; set; }
#endregion
#region Costruttore
/// <summary>
/// Constructor
/// </summary>
/// <param name="httpClient"></param>
/// <param name="localStorage"></param>
public AuthenticationHttpService(HttpClient httpClient, ILocalStorageService localStorage) : base(httpClient)
{
m_localStorage = localStorage;
}
#endregion
#region Private methods
/// <summary>
/// Set access token in http header
/// </summary>
/// <param name="accessToken">Access token</param>
private async Task SetAccessTokenAsync(string accessToken)
{
if (!string.IsNullOrEmpty(accessToken))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
if (UseLocalStorageForAccessToken)
await m_localStorage.SetItemAsync("accessToken", accessToken);
}
else
{
httpClient.DefaultRequestHeaders.Authorization = null;
if (UseLocalStorageForAccessToken)
await m_localStorage.RemoveItemAsync("accessToken");
}
}
#endregion
#region Public methods
/// <summary>
/// Return access token
/// </summary>
/// <returns></returns>
public async Task<string> GetAccessTokenAsync()
{
//Read token from object
if (httpClient.DefaultRequestHeaders.Authorization != null)
{
if (!string.IsNullOrEmpty(httpClient.DefaultRequestHeaders.Authorization.Parameter))
return httpClient.DefaultRequestHeaders.Authorization.Parameter;
}
//Read token from LocalStorage
var accessToken = "";
if (UseLocalStorageForAccessToken)
accessToken = await m_localStorage.GetItemAsync<string>("accessToken");
return accessToken;
}
/// <summary>
/// Manage login
/// </summary>
/// <param name="loginRequest"></param>
/// <returns></returns>
public async Task<Model.LoginResponse> LoginAsync(Model.LoginRequest loginRequest)
{
Model.LoginResponse result = null;
try
{
var response = await httpClient.PostAsJsonAsync("authentication/login", loginRequest);
result = await CheckJsonResponseAsync<Model.LoginResponse>(response);
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
if (result != null && result.Result)
await SetAccessTokenAsync(result.AccessToken);
return result;
}
/// <summary>
/// Manage logout
/// </summary>
/// <returns>Request response</returns>
public async Task<Model.LogoutResponse> LogoutAsync()
{
Model.LogoutResponse result = null;
try
{
var response = await httpClient.PostAsync("authentication/logout", null);
result = await CheckJsonResponseAsync<Model.LogoutResponse>(response);
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
if (result != null && result.Result)
await SetAccessTokenAsync(null);
return result;
}
#endregion
}
public partial class UsersHttpService: HttpService
{
#region Costruttore
/// <summary>
/// Constructor
/// </summary>
/// <param name="httpClient"></param>
public UsersHttpService(HttpClient httpClient) : base(httpClient)
{
}
#endregion
#region Public methods
/// <summary>
/// Inser user
/// </summary>
/// <param name="user">User info</param>
/// <returns>Request response</returns>
public async Task<Model.UsersPostResponse> PostAsync(Model.User user)
{
Model.UsersPostResponse result = null;
try
{
var response = await httpClient.PostAsJsonAsync("users", user);
result = await CheckJsonResponseAsync<Model.UsersPostResponse>(response);
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
return result;
}
/// <summary>
/// Remove user
/// </summary>
/// <returns>Request response</returns>
public async Task<Model.UsersDeleteResponse> DeleteAsync(Guid userId)
{
Model.UsersDeleteResponse result = null;
try
{
var response = await httpClient.DeleteAsync($"users?userId={userId}");
result = await CheckJsonResponseAsync<Model.UsersDeleteResponse>(response);
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
return result;
}
#endregion
}
Update the razor pages
To implement authentication we have to implement some pages to manage authentication (Login.razor, LoginDisplay.razor, RedirectToLogin.razor, Register.razor)
Login.razor
@page "/login"
<div id="login">
<div class="container">
<!-- Title -->
<div class="row">
<div class="col-sm">
<h1>Login</h1>
</div>
</div>
<div class="row">
<EditForm EditContext="@EditContext" OnSubmit="@Authenticate">
<DataAnnotationsValidator />
<!-- User -->
<div class="form-group">
<label for="userName">User</label>
<InputText id="userName" class="form-control" @bind-Value="@UserName" />
<ValidationMessage For="@(() => UserName)" />
</div>
<!-- Password -->
<div class="form-group">
<label for="password">Password</label>
<InputText type="password" id="password" class="form-control" @bind-Value="@Password" />
<ValidationMessage For="@(() => Password)" />
</div>
<!-- Action -->
<button type="submit" class="btn btn-primary">Login</button>
</EditForm>
</div>
<!-- Error -->
<div class="row mt-1">
<label>@ErrorMessage</label>
</div>
</div>
</div>
Login.razor.cs
public partial class Login : ComponentBase
{
#region Services
/// <summary>
/// Manage page navigation
/// </summary>
[Inject]
private NavigationManager Navigation { get; set; }
/// <summary>
/// Manage authentication
/// </summary>
[Inject]
private CustomAuthenticationStateProvider AuthStateProvider { get; set; }
#endregion
#region Proprties
/// <summary>
/// Contesto di modifica del form
/// </summary>
private EditContext EditContext { get; set; }
/// <summary>
/// User name
/// </summary>
[Required]
public string UserName { get; set; }
/// <summary>
/// Password
/// </summary>
[Required]
public string Password { get; set; }
/// <summary>
/// Error message
/// </summary>
private string ErrorMessage { get; set; }
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public Login()
{
EditContext = new EditContext(this);
}
#endregion
#region Methods
/// <summary>
/// Manage user login
/// </summary>
private async void Authenticate()
{
//Data validation
if (!EditContext.Validate())
return;
var loginRequest = new Model.LoginRequest
{
UserName = UserName,
Password = Password
};
//Set return url from querystring param
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var returnUrl = "/";
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var param))
returnUrl = param.First();
//Login
var result = await AuthStateProvider.LoginAsync(loginRequest);
if (result.Result)
Navigation.NavigateTo(returnUrl);
else
{
ErrorMessage = result.ErrorMessage;
StateHasChanged();
}
}
#endregion
}
LoginDisplay.razor
@using Microsoft.AspNetCore.Components.Authorization
<AuthorizeView>
<Authorized>
<a href="/profile">Welcome @context.User.Identity.Name</a>
<button class="nav-link btn btn-link" @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<a href="/register">Signup</a>
<a href="/login">Login</a>
</NotAuthorized>
</AuthorizeView>
LoginDisplay.razor.cs
public partial class LoginDisplay : ComponentBase
{
#region Services
/// <summary>
/// Manage page navigation
/// </summary>
[Inject]
private NavigationManager NavigationManager { get; set; }
/// <summary>
/// Manage authentication
/// </summary>
[Inject]
private CustomAuthenticationStateProvider AuthStateProvider { get; set; }
#endregion
/// <summary>
/// Manage logout
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task Logout(MouseEventArgs args)
{
var result = await AuthStateProvider.LogoutAsync();
if (result.Result)
NavigationManager.NavigateTo("/");
}
}
RedirectToLogin.razor
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
protected override void OnInitialized()
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
protected override void OnInitialized()
{
Navigation.NavigateTo($"login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
}
}
Register.razor
@page "/register"
<div id="register">
<div class="container">
<!-- Title -->
<div class="row">
<div class="col-sm">
<h1>Register user</h1>
</div>
</div>
<div class="row">
<EditForm @ref="Form" Model="@User" OnSubmit="@RegisterUser">
<DataAnnotationsValidator />
<!-- User / Email -->
<div class="form-row">
<!-- User -->
<div class="col form-group">
<label for="userName">User</label>
<InputText id="userName" class="form-control" @bind-Value="User.UserName" />
<ValidationMessage For="@(() => User.UserName)" />
</div>
<!-- Email -->
<div class="col form-group">
<label for="email">Email</label>
<InputText id="email" class="form-control" @bind-Value="@User.Email" />
<ValidationMessage For="@(() => User.Email)" />
</div>
</div>
<!-- Password / PasswordConfirm -->
<div class="form-row">
<!-- Password -->
<div class="col form-group">
<label for="password">Password</label>
<InputText id="password" type="password" class="form-control" @bind-Value="@User.Password" />
<ValidationMessage For="@(() => User.Password)" />
</div>
<!-- PasswordConfirm -->
<div class="col form-group">
<label for="passwordConfirm">Password confirm</label>
<InputText id="passwordConfirm" type="password" class="form-control" @bind-Value="@User.PasswordConfirm" />
<ValidationMessage For="@(() => User.PasswordConfirm)" />
</div>
</div>
<!-- Name / Surname -->
<div class="form-row">
<!-- Name -->
<div class="col form-group">
<label for="name">Name</label>
<InputText id="name" class="form-control" @bind-Value="@User.Name" />
<ValidationMessage For="@(() => User.Name)" />
</div>
<!-- Surname -->
<div class="col form-group">
<label for="surname">Surname</label>
<InputText id="surname" class="form-control" @bind-Value="@User.Surname" />
<ValidationMessage For="@(() => User.Surname)" />
</div>
</div>
<!-- Action -->
<button type="submit" class="btn btn-primary">Register</button>
</EditForm>
</div>
<!-- Message -->
<div class="row">
<label>@Message</label>
</div>
</div>
</div>
Register.razor.cs
public partial class Register : ComponentBase
{
#region Services
/// <summary>
/// Manage authentication
/// </summary>
[Inject]
private CustomAuthenticationStateProvider AuthStateProvider { get; set; }
#endregion
#region Properties
public EditForm Form { get; set; }
/// <summary>
/// Contesto di modifica del form
/// </summary>
public EditContext EditContext { get; set; }
/// <summary>
/// User info
/// </summary>
public Model.User User { get; set; }
/// <summary>
/// Error message
/// </summary>
private string Message { get; set; }
#endregion
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public Register()
{
User = new Model.User();
}
#endregion
#region Methods
/// <summary>
/// Manage component initialization
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();
User = new Model.User();
}
/// <summary>
/// Manage user registration
/// </summary>
private async Task RegisterUser()
{
//Data validation
if (!Form.EditContext.Validate())
return;
var result = await AuthStateProvider.RegisterAsync(User);
if (result == null || !result.Result)
Message = $"Registration failed: {result?.ErrorMessage}";
else
Message = "Registration completed successfully!";
StateHasChanged();
}
#endregion
}
Conclusions
With some settings we can manage authentication process using ASP.NET Core Identity having full control over authentication process, data layer and user interface.
References
https://zogface.blog/2019/04/17/asp-net-core-identity-with-a-custom-data-store/
https://www.codewithmukesh.com/blog/authentication-in-blazor-webassembly/
9 Comments
AllTechGeeks · October 11, 2020 at 4:08 pm
Hi Claudio,
I am really appreciate on this article. I am trying from last 7 weeks and learning the Blazor but coming to authentication with JWT got lot of problems. Now I am using this code in my sample project
Thanks
Murali
Claudio Gamberini · October 12, 2020 at 4:37 pm
Thanks Murali
Alessio Iafrate · October 14, 2020 at 5:51 am
Hi, good articles bus some class like LoginResponse, RegisterResponse are missing. Can you add in the article or add a github repository with full code?
Thanks!!
Claudio Gamberini · October 14, 2020 at 6:04 pm
In the article i have omitted some class related to data model and db layer to keep the post shortly and more readable. Hope to add a GitHub project soon.
Pedro Rossi · February 18, 2021 at 12:06 pm
Can you add a github repository with full code?
Claudio Gamberini · June 28, 2021 at 10:02 pm
Hi, i have updated the post and published the project on GitHub: https://github.com/CodeDesignTips/CustomBlazorAuthentication
Sérgio · July 8, 2021 at 4:02 pm
Hi.
Thank you for all the work.
Just one question please. All of this works great, but i can’t add to the Token passed to the client roles and claims.
Is there something i’m missing in the code?
Thank you.
Robert · July 9, 2021 at 3:36 pm
Using your github repo as a starting point, what parts would need to be changed to adapt this to Blazor Server? I’m brand new to this, so you might have to target your answer to a five year old 🙂
Thank you for sharing!
Robert · July 11, 2021 at 6:33 pm
Hi Claudio,
Thanks for posting this to github. I am interested in adapting this for server side blazor, what parts will need to be changed? Can you give any guidance?
Thank you very much!