.NET 7 Blazor MAUI – 要求使用 Azure 用户登录进行身份验证

huangapple go评论70阅读模式
英文:

.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&lt;AuthenticationState&gt; authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) =&gt; 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

    public override Task&lt;AuthenticationState&gt; GetAuthenticationStateAsync() =&gt;
        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&lt;AuthenticationStateProvider, ExternalAuthStateProvider&gt;();
builder.Services.AddSingleton&lt;AuthenticatedUser&gt;();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService&lt;AuthenticatedUser&gt;();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider&#39;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&amp;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:

英文:

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[] { &quot;User.Read&quot; };
    private readonly string _tenantId = &quot;[TENANT ID HERE]&quot;;
    private readonly string _clientId = &quot;[APP ID HERE]&quot;;

    public AuthenticationService()
    {
        authenticationClient = PublicClientApplicationBuilder.Create(_clientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, _tenantId) // Only allow accounts in the tenant to authenticate
            .WithRedirectUri($&quot;msal{_clientId}://auth&quot;)
            .Build();
    }

    public async Task&lt;AuthenticationResult?&gt; 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&lt;AuthenticationResult?&gt; 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&lt;AuthenticationState&gt; GetAuthenticationStateAsync() =&gt;
        Task.FromResult(new AuthenticationState(_currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        // Use BlazorWebView to update the auth state
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task&lt;AuthenticationState&gt; LogInAsyncCore()
        {
            _currentUser = await LoginWithExternalProviderAsync();

            return new AuthenticationState(_currentUser);
        }
    }

    private async Task&lt;ClaimsPrincipal&gt; LoginWithExternalProviderAsync()
    {
        var authResult = await _authenticationService.AcquireTokenInteractiveAsync();

        // Authentication failed, return the current logged out user state
        if (authResult == null) return _currentUser;

        List&lt;Claim&gt; claims;
        ClaimsPrincipal authenticatedUser;

        // For some reason AAD sets &quot;name&quot; 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 =&gt; c.Type == &quot;name&quot;)?.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&amp;pivots=maui
            authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(authResult.ClaimsPrincipal.Claims));
        }

        // Here&#39;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&lt;AuthenticationStateProvider, ExternalAuthStateProvider&gt;();
builder.Services.AddSingleton&lt;IAuthenticationService&gt;();

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:

huangapple
  • 本文由 发表于 2023年4月20日 01:08:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/76057152.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定