英文:
.NET 7 Blazor MAUI - Require Authentication with Azure User Logins
问题
I am developing a Blazor MAUI application, and I am trying to require users in my organization to login with their Microsoft work account in order to access the application. I have not been able to find much documentation about authentication within Blazor MAUI, and have been struggling to get a solution working.
Currently, I have been following Microsoft's docs about Blazor Hybrid authentication: ASP.NET Core Blazor Hybrid authentication and authorization. I have added the following class ExternalAuthStateProvider.cs
:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
public class ExternalAuthStateProvider : AuthenticationStateProvider
{
private readonly Task<AuthenticationState> authenticationState;
public ExternalAuthStateProvider(AuthenticatedUser user) =>
authenticationState = Task.FromResult(new AuthenticationState(user.Principal));
public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
authenticationState;
}
public class AuthenticatedUser
{
public ClaimsPrincipal Principal { get; set; } = new();
}
And have replaced the return builder.Build();
in MauiProgram.cs
with:
builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();
var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();
/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's
documentation for details.
The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());
authenticatedUser.Principal = user;
return host;
From here, I am not sure what code to add in MauiProgram.cs
to connect to my Azure app registration. I have followed multiple guides for configuring my Azure app registration, but have not seen any C# code similar to the code above, so I am confused on how to implement the connection here.
~ Aside from this method, the only other documentation I have found for Blazor MAUI is this video: MSAL Auth in MAUI Blazor. However, I'm wondering what other options there are for social logins without being forced to use Azure AD B2C? I am working on a handful of other projects where I do not have access to Azure AD B2C, and do not want to be forced to use the service.
英文:
I am developing a Blazor MAUI application, and I am trying to require users in my organization to login with their Microsoft work account in order to access the application. I have not been able to find much documentation about authentication within Blazor MAUI, and have been struggling to get a solution working.
Currently, I have been following Microsoft's docs about Blazor Hybrid authentication: ASP.NET Core Blazor Hybrid authentication and authorization. I have added the following class ExternalAuthStateProvider.cs
:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
public class ExternalAuthStateProvider : AuthenticationStateProvider
{
private readonly Task<AuthenticationState> authenticationState;
public ExternalAuthStateProvider(AuthenticatedUser user) =>
authenticationState = Task.FromResult(new AuthenticationState(user.Principal));
public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
authenticationState;
}
public class AuthenticatedUser
{
public ClaimsPrincipal Principal { get; set; } = new();
}
And have replaced the return builder.Build();
in MauiProgram.cs
with:
builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();
var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();
/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's
documentation for details.
The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());
authenticatedUser.Principal = user;
return host;
From here, I am not sure what code to add in MauiProgram.cs
to connect to my Azure app registration. I have followed multiple guides for configuring my Azure app registration, but have not seen any C# code similar to the code above, so I am confused on how to implement the connection here.
~ Aside from this method, the only other documentation I have found for Blazor MAUI is this video: MSAL Auth in MAUI Blazor. However, I'm wondering what other options there are for social logins without being forced to use Azure AD B2C? I am working on a handful of other projects where I do not have access to Azure AD B2C, and do not want to be forced to use the service.
答案1
得分: 2
I also found Microsoft docs to be lacking on the subject when it comes to integrating MSAL with Blazor Hybrid. I spent a few days digging through various resources online to come up with something that worked.
Here's a wrapper that includes the MSAL.NET code to authenticate the user. You'll have to create IAuthenticationService
which I left out.
public class AuthenticationService : IAuthenticationService
{
// I recommend storing this in appsettings.json and grabbing it from IConfiguration instead
private readonly IPublicClientApplication authenticationClient;
private readonly string[] _scopes = new[] { "User.Read" };
private readonly string _tenantId = "[TENANT ID HERE]";
private readonly string _clientId = "[APP ID HERE]";
public AuthenticationService()
{
authenticationClient = PublicClientApplicationBuilder.Create(_clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, _tenantId) // Only allow accounts in the tenant to authenticate
.WithRedirectUri($"msal{_clientId}://auth")
.Build();
}
public async Task<AuthenticationResult?> AcquireTokenSilentAsync()
{
var accounts = await authenticationClient.GetAccountsAsync();
AuthenticationResult? result;
try
{
result = await authenticationClient.AcquireTokenSilent(_scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Acquiring silently failed; need to acquire the token interactively
result = await AcquireTokenInteractiveAsync();
}
return result;
}
public async Task<AuthenticationResult?> AcquireTokenInteractiveAsync()
{
if (authenticationClient == null)
return null;
AuthenticationResult result;
try
{
result = await authenticationClient.AcquireTokenInteractive(_scopes)
.WithTenantId(_tenantId)
.ExecuteAsync()
.ConfigureAwait(false);
return result;
}
catch (MsalClientException)
{
return null;
}
}
}
Now your state provider can inject AuthenticationService
to get the token.
There are a few weird quirks in here for ClaimsPrincipal
that I could only get working with a hack so the user context would actually recognize the change in authentication state.... definitely some room for improvement.
public class ExternalAuthStateProvider : AuthenticationStateProvider
{
private readonly IAuthenticationService _authenticationService;
private ClaimsPrincipal _currentUser;
public ExternalAuthStateProvider(IAuthenticationService authenticationService)
{
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
_authenticationService = authenticationService;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
Task.FromResult(new AuthenticationState(_currentUser));
public Task LogInAsync()
{
var loginTask = LogInAsyncCore();
// Use BlazorWebView to update the auth state
NotifyAuthenticationStateChanged(loginTask);
return loginTask;
async Task<AuthenticationState> LogInAsyncCore()
{
_currentUser = await LoginWithExternalProviderAsync();
return new AuthenticationState(_currentUser);
}
}
private async Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
{
var authResult = await _authenticationService.AcquireTokenInteractiveAsync();
// Authentication failed, return the current logged out user state
if (authResult == null) return _currentUser;
List<Claim> claims;
ClaimsPrincipal authenticatedUser;
// For some reason AAD sets "name" as the claim type for the user name and not ClaimsType.Name...
// This is a workaround since context.User.Identity only recognizes ClaimsType.Name
var name = authResult.ClaimsPrincipal.FindFirst(c => c.Type == "name")?.Value;
if (name != null)
{
claims = claims = authResult.ClaimsPrincipal.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Name, name));
authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims));
}
else
{
// Another interesting quirk, we MUST recreate the ClaimsPrincipal instead of using authResult.ClaimsPrincipal directly
// according to https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/security/?view=aspnetcore-6.0&pivots=maui
authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(authResult.ClaimsPrincipal.Claims));
}
// Here's the access token
var token = authResult.AccessToken;
return await Task.FromResult(authenticatedUser);
}
public void Logout()
{
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_currentUser)));
}
}
Finally, we can add these services to MauiProgram.cs
:
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<IAuthenticationService>();
As for integrating the authentication service with Blazor, see the answer from javiercn in this, as well as my post here.
Hopefully this leaves you with a good start if you choose to pick up where you left off with your project.
Resources:
- ASP.NET Core Blazor Hybrid authentication and authorization
- ASP.NET Core Blazor authentication and authorization
- github.com/carlfranklin/MsalAuthInMauiBlazor
- Desktop app that calls web APIs: Acquire a token interactively
- Get a token from the token cache using MSAL.NET
英文:
I also found Microsoft docs to be lacking on the subject when it comes to integrating MSAL with Blazor Hybrid. I spent a few days digging through various resources online to come up with something that worked.
Here's a wrapper that includes the MSAL.NET code to authenticate the user. You'll have to create IAuthenticationService
which I left out.
public class AuthenticationService : IAuthenticationService
{
// I recommend storing this in appsettings.json and grabbing it from IConfiguration instead
private readonly IPublicClientApplication authenticationClient;
private readonly string[] _scopes = new[] { "User.Read" };
private readonly string _tenantId = "[TENANT ID HERE]";
private readonly string _clientId = "[APP ID HERE]";
public AuthenticationService()
{
authenticationClient = PublicClientApplicationBuilder.Create(_clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, _tenantId) // Only allow accounts in the tenant to authenticate
.WithRedirectUri($"msal{_clientId}://auth")
.Build();
}
public async Task<AuthenticationResult?> AcquireTokenSilentAsync()
{
var accounts = await authenticationClient.GetAccountsAsync();
AuthenticationResult? result;
try
{
result = await authenticationClient.AcquireTokenSilent(_scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Acquiring silently failed; need to acquire the token interactively
result = await AcquireTokenInteractiveAsync();
}
return result;
}
public async Task<AuthenticationResult?> AcquireTokenInteractiveAsync()
{
if (authenticationClient == null)
return null;
AuthenticationResult result;
try
{
result = await authenticationClient.AcquireTokenInteractive(_scopes)
.WithTenantId(_tenantId)
.ExecuteAsync()
.ConfigureAwait(false);
return result;
}
catch (MsalClientException)
{
return null;
}
}
}
Now your state provider can inject AuthenticationService
to get the token.
There are a few weird quirks in here for ClaimsPrincipal
that I could only get working with a hack so the user context would actually recognize the change in authentication state.... definitely some room for improvement.
public class ExternalAuthStateProvider : AuthenticationStateProvider
{
private readonly IAuthenticationService _authenticationService;
private ClaimsPrincipal _currentUser;
public ExternalAuthStateProvider(IAuthenticationService authenticationService)
{
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
_authenticationService = authenticationService;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
Task.FromResult(new AuthenticationState(_currentUser));
public Task LogInAsync()
{
var loginTask = LogInAsyncCore();
// Use BlazorWebView to update the auth state
NotifyAuthenticationStateChanged(loginTask);
return loginTask;
async Task<AuthenticationState> LogInAsyncCore()
{
_currentUser = await LoginWithExternalProviderAsync();
return new AuthenticationState(_currentUser);
}
}
private async Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
{
var authResult = await _authenticationService.AcquireTokenInteractiveAsync();
// Authentication failed, return the current logged out user state
if (authResult == null) return _currentUser;
List<Claim> claims;
ClaimsPrincipal authenticatedUser;
// For some reason AAD sets "name" as the claim type for the user name and not ClaimsType.Name...
// This is a workaround since context.User.Identity only recognizes ClaimsType.Name
var name = authResult.ClaimsPrincipal.FindFirst(c => c.Type == "name")?.Value;
if (name != null)
{
claims = claims = authResult.ClaimsPrincipal.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Name, name));
authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims));
}
else
{
// Another interesting quirk, we MUST recreate the ClaimsPrincipal instead of using authResult.ClaimsPrincipal directly
// according to https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/security/?view=aspnetcore-6.0&pivots=maui
authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(authResult.ClaimsPrincipal.Claims));
}
// Here's the access token
var token = authResult.AccessToken;
return await Task.FromResult(authenticatedUser);
}
public void Logout()
{
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_currentUser)));
}
}
Finally, we can add these services to MauiProgram.cs
:
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<IAuthenticationService>();
As for integrating the authentication service with Blazor, see the answer from javiercn in this, as well as my post here.
Hopefully this leaves you with a good start if you choose to pick up where you left off with your project.
Resources:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论