I am using Microsoft Blazor with Hosted WebAssembly model for some applications. This means that the solution has three projects, the client, the server and the shared.
I have already build one application with basic login providing username and password, which are validated into database and the user is capable to login. For this approach I use a CustomAuthStateProvider.
Under Client, in Program.cs
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
The CustomAuthStateProvider.cs
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly IJSRuntime _jSRuntime;
private readonly ILocalStorageService _iLocalStorage;
public CustomAuthStateProvider(HttpClient httpClient, IJSRuntime jSRuntime, ILocalStorageService iLocalStorage)
{
_httpClient = httpClient;
_jSRuntime = jSRuntime;
_iLocalStorage = iLocalStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
//CREATE AN EMPTY STATE
var state = new AuthenticationState(new ClaimsPrincipal());
try
{
AuthenticationData? authenticationData = null;
if (authenticationData == null)
{
//TRY WITH SESSION & COOKIE DATA
HttpResponseMessage httpResponseMessage = await _httpClient.GetAsync("/api/auth/validate");
if (httpResponseMessage.IsSuccessStatusCode)
{
HttpContent content = httpResponseMessage.Content;
AuthenticationData? authenticationData_ = await content.ReadFromJsonAsync<AuthenticationData>();
authenticationData = CheckAuthenticationData(authenticationData_);
//HELPER MESSAGE
if (authenticationData != null)
{ Console.WriteLine("user_is_authorized"); }
else
{ Console.WriteLine("user_not_authorized"); }
}
}
if (authenticationData != null)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Sid, authenticationData.Sid.ToString()),
new Claim(ClaimTypes.Name, authenticationData.Name.ToString()),
new Claim("preferred_username", authenticationData.Name.ToString()),
}, "Authentication");
Parameters.signuser = authenticationData.Name.ToString();
state = new AuthenticationState(new ClaimsPrincipal(identity));
}
else
{
//TRY WITH FORM DATA
state = new AuthenticationState(new ClaimsPrincipal());
}
}
catch (Exception e)
{
state = new AuthenticationState(new ClaimsPrincipal());
throw new Exception(e.Message);
}
finally
{
NotifyAuthenticationStateChanged(Task.FromResult(state));
}
return state;
}
private AuthenticationData? CheckAuthenticationData(AuthenticationData? authenticationData_)
{
AuthenticationData? authenticationData = null;
if (authenticationData_ != null)
{
if (
(!string.IsNullOrEmpty(authenticationData_.Sid)) &&
(!string.IsNullOrEmpty(authenticationData_.Name))
)
{ authenticationData = authenticationData_; }
}
return authenticationData;
}
}
And this works as expected with a Login and Logout razor components, but then I have created another application using Keycloack. For this approach I use AddOidcAuthentication:
Under Client, in Program.cs
var authHostUrl =
$"{authHost}/auth";
var authRealUrl =
$"{authHost}/auth/realms/{authReal}";
var authOpenIdUrl =
$"{authHost}/auth/realms/{authReal}/.well-known/openid-configuration";
var authTokenUrl =
$"{authHost}/auth/realms/{authReal}/protocol/openid-connect/token";
RegisterClient.RegisterHttpClient(builder, builder.Services);
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = authRealUrl;
options.ProviderOptions.MetadataUrl = authOpenIdUrl;
options.ProviderOptions.ClientId = authClient;
options.ProviderOptions.ResponseType = "id_token token";
options.UserOptions.NameClaim = "preferred_username";
options.UserOptions.RoleClaim = "roles";
options.UserOptions.ScopeClaim = "scope";
});
Of course there are some additional things which need to be configured into the Client like some js files and also into the Server side like Keycloak library but this also works great.
When trying to combine those two ways of authentication the application fails with strange errors like the following:
I figured out then when builder.Services.AddOidcAuthentication coexists with builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>() it is failing.
My intention is to be able to use both ways for login in to the application, any idea?