From 86baaea135ef20bf3aeec9d408fe51b65963ff7e Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Tue, 3 May 2022 07:24:40 -0500 Subject: [PATCH 01/20] Blazor Hybrid security content --- aspnetcore/blazor/hybrid/security/index.md | 219 ++++++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index 177d3b12e8a4..2313b99a6944 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -5,7 +5,7 @@ description: Learn about Blazor Hybrid authentication and authorization scenario monikerRange: '>= aspnetcore-6.0' ms.author: riande ms.custom: mvc -ms.date: 04/07/2022 +ms.date: 05/03/2022 no-loc: [".NET MAUI", "Mac Catalyst", "Blazor Hybrid", Home, Privacy, Kestrel, appsettings.json, "ASP.NET Core Identity", cookie, Cookie, Blazor, "Blazor Server", "Blazor WebAssembly", "Identity", "Let's Encrypt", Razor, SignalR] uid: blazor/hybrid/security/index --- @@ -13,8 +13,225 @@ uid: blazor/hybrid/security/index This article describes ASP.NET Core's support for the configuration and management of security in Blazor Hybrid apps. +Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. + +Follow the external guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article. + [!INCLUDE[](~/blazor/includes/blazor-hybrid-preview-notice.md)] +## Integration with identity providers + +Authentication of native apps happens via an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/connect/). + +### .NET MAUI + +[Xamarin.Essentials: Web Authenticator](/xamarin/essentials/web-authenticator): The `WebAuthenticator` class allows the app to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app. + +### WPF + +WPF apps use the [Microsoft identity platform](/azure/active-directory/develop/) to integrate with Azure Active Directory (AAD) and AAD B2C. For guidance and examples, see the following resources: + +* [Sign-in a user with the Microsoft Identity Platform in a WPF Desktop application and call an ASP.NET Core Web API](/samples/azure-samples/active-directory-dotnet-native-aspnetcore-v2/1-desktop-app-calls-web-api/) +* [Add authentication to your Windows (WPF) app](/azure/developer/mobile-apps/azure-mobile-apps/quickstarts/wpf/authentication) +* [Tutorial: Sign in users and call Microsoft Graph in Windows Presentation Foundation (WPF) desktop app](/azure/active-directory/develop/tutorial-v2-windows-desktop) +* [Quickstart: Acquire a token and call Microsoft Graph API from a desktop application](/azure/active-directory/develop/desktop-app-quickstart?pivots=devlang-windows-desktop) +* [Quickstart: Set up sign in for a desktop app using Azure Active Directory B2C](/azure/active-directory-b2c/quickstart-native-app-desktop) +* [Configure authentication in a sample WPF desktop app by using Azure AD B2C](/azure/active-directory-b2c/configure-authentication-sample-wpf-desktop-app) + +### Windows Forms + + + +## Integrate authentication + +Integrating authentication must achieve the following goals for Razor components and services: + +* Use the abstractions in the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) package, such as . +* React to changes in the authentication context. +* Access credentials provisioned by the app from the identity provider, such as access tokens to perform authorized API calls. + +After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps: + +* Reference the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) package. +* Implement a custom . +* Register the custom authentication state provider in the dependency injection container. + +### Create a custom `AuthenticationStateProvider` + +The is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes. + +If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. Typically in this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `HybridAuthenticationStateProvider` is an example implementation of a custom for this authentication scenario. + +```csharp +public HybridAuthenticationStateProvider : AuthenticationStateProvider +{ + private readonly Task authenticationState; + + public HybridAuthenticationStateProvider(AuthenticatedUser user) => + authenticationState = Task.FromResult(new AuthenticationState(user.Principal)); + + public override Task GetAuthenticationStateAsync() => + authenticationState; +} + +public class AuthenticatedUser +{ + public ClaimsPrincipal Principal { get; set; } +} +``` + +To update the user while the Blazor app is running, call within the implementation, using ***either*** of the following approaches: + +* [Signal an authentication update from outside of the `BlazorWebView`](#signal-an-authentication-update-from-outside-of-the-blazorwebview-option-1)) +* [Handle authentication within the `BlazorWebView`](#handle-authentication-within-the-blazorwebview-option-2) + +### Signal an authentication update from outside of the `BlazorWebView` (Option 1) + +The following example uses a global service to signal an authentication update. We recommend that the service offer an event that the can subscribe to, where the event invokes . + +> [!NOTE] +> `AuthenticatedUser` in the following example is registered in the dependency injection container later in this article. + +```csharp +public HybridAuthenticationStateProvider : AuthenticationStateProvider +{ + private readonly AuthenticatedUser authenticatedUser; + private Task authenticationState; + + public HybridAuthenticationStateProvider(AuthenticatedUser user) + { + authenticationState = + Task.FromResult(new AuthenticationState(user.Principal)); + authenticatedUser = user; + + authenticatedUser.UserChanged += () => + { + authenticationState = + Task.FromResult(new AuthenticationState(user.Principal)); + NotifyAuthenticationStateChanged(authenticatedUser); + } + } + + public override Task GetAuthenticationStateAsync() => + authenticationState; +} + +public class AuthenticatedUser +{ + public ClaimsPrincipal Principal { get; set; } + public event Action UserChanged; +} +``` + +From anywhere in the app, we can resolve the `AuthenticatedUser` service after we have authenticated the user and set the principal property to the authenticated user before we start the Blazor application: + +```csharp +var authenticatedUser = services.GetRequiredService(); +authenticatedUser.Principal = currentUser; +``` + +> [!NOTE] +> Alternatively, set the user's principal on instead of setting it via a service, which avoids use of the dependency injection container. + +### Handle authentication within the `BlazorWebView` (Option 2) + +Add additional methods to the `HybridAuthenticationStateProvider` to trigger log in and log out and update the user: + +```csharp +public HybridAuthenticationStateProvider : AuthenticationStateProvider +{ + private Task currentUser = + Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); + + public override Task GetAuthenticationStateAsync() => + currentUser; + + public Task LoginAsync() + { + var loginTask = CreateLoginTask; + + NotifyAuthenticationStateChanged(loginTask); + + return loginTask; + + Task CreateLoginTask() + { + var newUser = await LoginWithIdentityProviderAsync(); + currentUser = new AuthenticationState(newUser); + + return currentUser; + } + } +} +``` + +In the preceding example: + +* The call to `CreateLoginTask` triggers the login process. +* The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login process. +* Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete. +* The `LoginWithIdentityProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see the identity provider's documentation. + +The following `LoginComponent` component demonstrates how to log in a user. In a typical app, the `LoginComponent` component is only shown in a parent component if the user isn't logged into the app. + +`Shared/LoginComponent.razor`: + +```razor +@inject AuthenticationStateProvider AuthenticationStateProvider + + + +@code +{ + public async Task Login() + { + await ((HybridAuthenticationStateProvider)AuthenticationStateProvider) + .LoginAsync(); + } +} +``` + +The implementation for logout is similar. + +### Service registrations + +Add the Blazor abstractions to the DI container: + +```csharp +builder.Services.AddAuthorizationCore(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +``` + +## Accessing other authentication information + +Blazor doesn't define an abstraction to deal with other credentials, such as access tokens to use for HTTP requests to web APIs. We recommend following the identity provider's guidance to manage the user's credentials with the primitives that the identity provider's SDK provides. + +It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive is registered with the DI container, consume the SDK's primitive within the app. + +The Blazor framework isn't aware of a user's authentication credentials and doesn't interact with credentials in any way, so the app's code is free to follow whatever approach you deem most convenient. However, follow the general security guidance in the next section, [Other authentication security considerations](#other-authentication-security-considerations), when implementing authentication code in an app. + +## Other authentication security considerations + +The authentication process is external to Blazor, and we recommend that developers access the identity provider's guidance for additional security guidance. + +When implementing authentication: + +* Avoid authentication in the context of the :::no-loc text="Web View":::. For example, avoid using a JavaScript OAuth library to perform the authentication flow. In a single-page app, authentication tokens aren't hidden in JavaScript and can be easily discovered by malicious users and used for nefarious purposes. Native apps don't suffer this risk because native apps are only able to obtain tokens outside of the browser context, which means that rogue third-party scripts can't steal the tokens and compromise the app. +* Avoid implementing the authentication workflow yourself. In most cases, platform libraries securely handle the authentication workflow, specifically using the system's browser instead of using a custom :::no-loc text="Web View"::: that can be hijacked. +* Avoid using the platform's :::no-loc text="Web View"::: control to perform authentication. Instead, rely on the system's browser when possible. +* Avoid passing the tokens to the document context (JavaScript). In some situations, a JavaScript library within the document is required to perform an authorized call to an external service. Instead of making the token available to JavaScript via JS interop: + * Provide a fake token to the library and within the :::no-loc text="Web View":::. + * Intercept the outgoing network request in code. + * Replace the fake token with the real token and confirm that the destination of the request is valid. + ## Untrusted and unencoded content Avoid allowing an app render untrusted and unencoded content from a database or other resource, such as user-provided comments, in its rendered UI. Permitting untrusted, unencoded content to render can cause malicious code to execute. From 4ad7872c7620f6b9e6e05eeb897c0f04b2d54e6c Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 4 May 2022 10:57:29 -0500 Subject: [PATCH 02/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 44 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index 2313b99a6944..9e34cd018b60 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -130,6 +130,14 @@ public class AuthenticatedUser } ``` +Add the Blazor abstractions to the DI container: + +```csharp +builder.Services.AddAuthorizationCore(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +``` + From anywhere in the app, we can resolve the `AuthenticatedUser` service after we have authenticated the user and set the principal property to the authenticated user before we start the Blazor application: ```csharp @@ -138,7 +146,30 @@ authenticatedUser.Principal = currentUser; ``` > [!NOTE] -> Alternatively, set the user's principal on instead of setting it via a service, which avoids use of the dependency injection container. +> Alternatively, set the user's principal on instead of setting it via a service, which avoids use of the dependency injection container: +> +> ```csharp +> public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider +> { +> public override Task GetAuthenticationStateAsync() => +> Task.FromResult( +> new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? +> new ClaimsPrincipal(new ClaimsIdentity()))); +> } +> ``` +> +> In `MainWindow`'s constructor (`MainWindow.xaml.cs`): +> +> ```csharp +> services.AddScoped(); +> BlazorView.Services = services.BuildServiceProvider(); +> +> BlazorView.RootComponents.Add(new Microsoft.AspNetCore.Components.WebView.Wpf.RootComponent() +> { +> ComponentType = typeof(Main), +> Selector = "#app" +> }); +> ### Handle authentication within the `BlazorWebView` (Option 2) @@ -169,6 +200,14 @@ public HybridAuthenticationStateProvider : AuthenticationStateProvider return currentUser; } } + + private Task LoginWithExternalProviderAsync() + { + /* + Add developer OpenID/MSAL code to authenticate the user + */ + return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); + } } ``` @@ -200,14 +239,11 @@ The following `LoginComponent` component demonstrates how to log in a user. In a The implementation for logout is similar. -### Service registrations - Add the Blazor abstractions to the DI container: ```csharp builder.Services.AddAuthorizationCore(); builder.Services.AddScoped(); -builder.Services.AddSingleton(); ``` ## Accessing other authentication information From 0185388f096ad7e449b9a60590339f2cd0134693 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 4 May 2022 11:34:36 -0500 Subject: [PATCH 03/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index 9e34cd018b60..c65b482bbb85 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -194,7 +194,7 @@ public HybridAuthenticationStateProvider : AuthenticationStateProvider Task CreateLoginTask() { - var newUser = await LoginWithIdentityProviderAsync(); + var newUser = await LoginWithExternalProviderAsync(); currentUser = new AuthenticationState(newUser); return currentUser; @@ -216,7 +216,7 @@ In the preceding example: * The call to `CreateLoginTask` triggers the login process. * The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login process. * Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete. -* The `LoginWithIdentityProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see the identity provider's documentation. +* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see the identity provider's documentation. The following `LoginComponent` component demonstrates how to log in a user. In a typical app, the `LoginComponent` component is only shown in a parent component if the user isn't logged into the app. From 9d943c50616a732080464a0c9514e79b233f2288 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 4 May 2022 12:14:42 -0500 Subject: [PATCH 04/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index c65b482bbb85..b5a4c0001502 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -169,7 +169,7 @@ authenticatedUser.Principal = currentUser; > ComponentType = typeof(Main), > Selector = "#app" > }); -> +> ``` ### Handle authentication within the `BlazorWebView` (Option 2) From 98b39ac043bd08f5354fd68f2b9809a2b222d5a0 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 5 May 2022 05:05:32 -0500 Subject: [PATCH 05/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 55 ++++++++++++++++------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index b5a4c0001502..c14f93d8667c 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -133,9 +133,9 @@ public class AuthenticatedUser Add the Blazor abstractions to the DI container: ```csharp -builder.Services.AddAuthorizationCore(); -builder.Services.AddScoped(); -builder.Services.AddSingleton(); +services.AddAuthorizationCore(); +services.AddScoped(); +services.AddSingleton(); ``` From anywhere in the app, we can resolve the `AuthenticatedUser` service after we have authenticated the user and set the principal property to the authenticated user before we start the Blazor application: @@ -163,12 +163,6 @@ authenticatedUser.Principal = currentUser; > ```csharp > services.AddScoped(); > BlazorView.Services = services.BuildServiceProvider(); -> -> BlazorView.RootComponents.Add(new Microsoft.AspNetCore.Components.WebView.Wpf.RootComponent() -> { -> ComponentType = typeof(Main), -> Selector = "#app" -> }); > ``` ### Handle authentication within the `BlazorWebView` (Option 2) @@ -204,16 +198,32 @@ public HybridAuthenticationStateProvider : AuthenticationStateProvider private Task LoginWithExternalProviderAsync() { /* - Add developer OpenID/MSAL code to authenticate the user + Add developer OpenID/MSAL code to authenticate the user. + + Return a new ClaimsPrincipal for a new ClaimsIdentity. */ return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); } + + public Task LogoutAsync() + { + var logoutTask = CreateLogoutTask; + + NotifyAuthenticationStateChanged(logoutTask); + + return logoutTask; + + Task CreateLogoutTask() + { + return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); + } + } } ``` In the preceding example: -* The call to `CreateLoginTask` triggers the login process. +* The call to `CreateLoginTask` triggers the login process, and `CreateLogoutTask` triggers the logout process. * The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login process. * Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete. * The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see the identity provider's documentation. @@ -237,13 +247,30 @@ The following `LoginComponent` component demonstrates how to log in a user. In a } ``` -The implementation for logout is similar. +The following `LogoutComponent` component demonstrates how to log out a user. In a typical app, the `LogoutComponent` component is only shown in a parent component if the user is logged into the app. + +`Shared/LogoutComponent.razor`: + +```razor +@inject AuthenticationStateProvider AuthenticationStateProvider + + + +@code +{ + public async Task Logout() + { + await ((HybridAuthenticationStateProvider)AuthenticationStateProvider) + .LogoutAsync(); + } +} +``` Add the Blazor abstractions to the DI container: ```csharp -builder.Services.AddAuthorizationCore(); -builder.Services.AddScoped(); +services.AddAuthorizationCore(); +services.AddScoped(); ``` ## Accessing other authentication information From 1ceacd424af44179fb44d2178052bb8481ae8a57 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 5 May 2022 06:32:21 -0500 Subject: [PATCH 06/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 138 ++++++++++++--------- 1 file changed, 78 insertions(+), 60 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index c14f93d8667c..c7f4ebf22d76 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -67,14 +67,18 @@ After authentication is added to a .NET MAUI, WPF, or Windows Forms app and user The is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes. -If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. Typically in this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `HybridAuthenticationStateProvider` is an example implementation of a custom for this authentication scenario. +If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. Typically in this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthenticationStateProvider` is an example implementation of a custom for this authentication scenario. ```csharp -public HybridAuthenticationStateProvider : AuthenticationStateProvider +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Authorization; + +public class ExternalAuthenticationStateProvider : AuthenticationStateProvider { private readonly Task authenticationState; - public HybridAuthenticationStateProvider(AuthenticatedUser user) => + public ExternalAuthenticationStateProvider(AuthenticatedUser user) => authenticationState = Task.FromResult(new AuthenticationState(user.Principal)); public override Task GetAuthenticationStateAsync() => @@ -83,10 +87,18 @@ public HybridAuthenticationStateProvider : AuthenticationStateProvider public class AuthenticatedUser { - public ClaimsPrincipal Principal { get; set; } + public ClaimsPrincipal Principal { get; set; } = new(); } ``` +Add the Blazor abstractions to the DI container: + +```csharp +services.AddAuthorizationCore(); +services.AddScoped(); +services.AddSingleton(); +``` + To update the user while the Blazor app is running, call within the implementation, using ***either*** of the following approaches: * [Signal an authentication update from outside of the `BlazorWebView`](#signal-an-authentication-update-from-outside-of-the-blazorwebview-option-1)) @@ -100,33 +112,48 @@ The following example uses a global service to signal an authentication update. > `AuthenticatedUser` in the following example is registered in the dependency injection container later in this article. ```csharp -public HybridAuthenticationStateProvider : AuthenticationStateProvider +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Authorization; + +public class ExternalAuthenticationStateProvider : AuthenticationStateProvider { - private readonly AuthenticatedUser authenticatedUser; - private Task authenticationState; + private AuthenticationState currentUser; - public HybridAuthenticationStateProvider(AuthenticatedUser user) + public ExternalAuthenticationStateProvider(ExternalAuthService service) { - authenticationState = - Task.FromResult(new AuthenticationState(user.Principal)); - authenticatedUser = user; + currentUser = new AuthenticationState(service.CurrentUser); - authenticatedUser.UserChanged += () => + service.UserChanged += (newUser) => { - authenticationState = - Task.FromResult(new AuthenticationState(user.Principal)); - NotifyAuthenticationStateChanged(authenticatedUser); - } + currentUser = new AuthenticationState(newUser); + NotifyAuthenticationStateChanged(Task.FromResult(currentUser)); + }; } - public override Task GetAuthenticationStateAsync() => - authenticationState; + public override Task GetAuthenticationStateAsync() => + Task.FromResult(currentUser); } -public class AuthenticatedUser +public class ExternalAuthService { - public ClaimsPrincipal Principal { get; set; } - public event Action UserChanged; + public event Action? UserChanged; + private ClaimsPrincipal? currentUser; + + public ClaimsPrincipal CurrentUser + { + get { return currentUser ?? new(); } + set + { + currentUser = value; + + if (UserChanged is not null) + { + UserChanged(currentUser); + } + } + } } ``` @@ -134,15 +161,15 @@ Add the Blazor abstractions to the DI container: ```csharp services.AddAuthorizationCore(); -services.AddScoped(); -services.AddSingleton(); +services.AddScoped(); +services.AddSingleton(); ``` -From anywhere in the app, we can resolve the `AuthenticatedUser` service after we have authenticated the user and set the principal property to the authenticated user before we start the Blazor application: +From anywhere in the app, we can resolve the `ExternalAuthService` service after we have authenticated the user and set the principal property to the authenticated user before we start the Blazor application: ```csharp -var authenticatedUser = services.GetRequiredService(); -authenticatedUser.Principal = currentUser; +var authenticatedUser = services.GetRequiredService(); +ExternalAuthService.Principal = currentUser; ``` > [!NOTE] @@ -167,64 +194,55 @@ authenticatedUser.Principal = currentUser; ### Handle authentication within the `BlazorWebView` (Option 2) -Add additional methods to the `HybridAuthenticationStateProvider` to trigger log in and log out and update the user: +Add additional methods to the `ExternalAuthenticationStateProvider` to trigger log in and log out and update the user: ```csharp -public HybridAuthenticationStateProvider : AuthenticationStateProvider +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Authorization; + +public class ExternalAuthenticationStateProvider : AuthenticationStateProvider { - private Task currentUser = - Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); + private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity()); public override Task GetAuthenticationStateAsync() => - currentUser; + Task.FromResult(new AuthenticationState(currentUser)); - public Task LoginAsync() + public Task LogInAsync() { - var loginTask = CreateLoginTask; - + var loginTask = LogInAsyncCore(); NotifyAuthenticationStateChanged(loginTask); return loginTask; - Task CreateLoginTask() + async Task LogInAsyncCore() { - var newUser = await LoginWithExternalProviderAsync(); - currentUser = new AuthenticationState(newUser); + var user = await LoginWithExternalProviderAsync(); + currentUser = user; - return currentUser; + return new AuthenticationState(currentUser); } } private Task LoginWithExternalProviderAsync() { - /* - Add developer OpenID/MSAL code to authenticate the user. - - Return a new ClaimsPrincipal for a new ClaimsIdentity. - */ + // Here is where you write your OpenID/MSAL code to authenticate the user. return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); } - public Task LogoutAsync() + public void Logout() { - var logoutTask = CreateLogoutTask; - - NotifyAuthenticationStateChanged(logoutTask); - - return logoutTask; - - Task CreateLogoutTask() - { - return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); - } + currentUser = new ClaimsPrincipal(new ClaimsIdentity()); + NotifyAuthenticationStateChanged( + Task.FromResult(new AuthenticationState(currentUser))); } } ``` In the preceding example: -* The call to `CreateLoginTask` triggers the login process, and `CreateLogoutTask` triggers the logout process. -* The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login process. +* The call to `LogInAsyncCore` triggers the login process. +* The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process. * Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete. * The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see the identity provider's documentation. @@ -241,7 +259,7 @@ The following `LoginComponent` component demonstrates how to log in a user. In a { public async Task Login() { - await ((HybridAuthenticationStateProvider)AuthenticationStateProvider) + await ((ExternalAuthenticationStateProvider)AuthenticationStateProvider) .LoginAsync(); } } @@ -260,8 +278,8 @@ The following `LogoutComponent` component demonstrates how to log out a user. In { public async Task Logout() { - await ((HybridAuthenticationStateProvider)AuthenticationStateProvider) - .LogoutAsync(); + await ((ExternalAuthenticationStateProvider)AuthenticationStateProvider) + .Logout(); } } ``` @@ -270,7 +288,7 @@ Add the Blazor abstractions to the DI container: ```csharp services.AddAuthorizationCore(); -services.AddScoped(); +services.AddScoped(); ``` ## Accessing other authentication information From f2dbb1b079f7974104a3ec61eb4ee22da64f4beb Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 5 May 2022 06:36:36 -0500 Subject: [PATCH 07/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index c7f4ebf22d76..5f571f7696a5 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -226,7 +226,13 @@ public class ExternalAuthenticationStateProvider : AuthenticationStateProvider private Task LoginWithExternalProviderAsync() { - // Here is where you write your OpenID/MSAL code to authenticate the user. + /* + Provide OpenID/MSAL code to authenticate the user. See your identity + provider's documentation for details. + + Return a new ClaimsPrincipal based on a new ClaimsIdentity. + */ + return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); } @@ -244,7 +250,7 @@ In the preceding example: * The call to `LogInAsyncCore` triggers the login process. * The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process. * Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete. -* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see the identity provider's documentation. +* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. The following `LoginComponent` component demonstrates how to log in a user. In a typical app, the `LoginComponent` component is only shown in a parent component if the user isn't logged into the app. From 5f12466aa4acaa4b8c4e9d3ada377ce61c656f09 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 11 May 2022 11:40:56 -0500 Subject: [PATCH 08/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 378 ++++++++++++++++----- aspnetcore/zone-pivot-groups.yml | 10 + 2 files changed, 310 insertions(+), 78 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index bddc7a3988a2..ca63b6a9de3a 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -5,29 +5,43 @@ description: Learn about Blazor Hybrid authentication and authorization scenario monikerRange: '>= aspnetcore-6.0' ms.author: riande ms.custom: mvc -ms.date: 05/05/2022 +ms.date: 05/11/2022 no-loc: [".NET MAUI", "Mac Catalyst", "Blazor Hybrid", Home, Privacy, Kestrel, appsettings.json, "ASP.NET Core Identity", cookie, Cookie, Blazor, "Blazor Server", "Blazor WebAssembly", "Identity", "Let's Encrypt", Razor, SignalR] uid: blazor/hybrid/security/index +zone_pivot_groups: blazor-hybrid-frameworks --- # ASP.NET Core Blazor Hybrid authentication and authorization This article describes ASP.NET Core's support for the configuration and management of security in Blazor Hybrid apps. -Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. +[!INCLUDE[](~/blazor/includes/blazor-hybrid-preview-notice.md)] -Follow the external guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article. +Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Follow the external guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article. -[!INCLUDE[](~/blazor/includes/blazor-hybrid-preview-notice.md)] +Integrating authentication must achieve the following goals for Razor components and services: + +* Use the abstractions in the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) package, such as . +* React to changes in the authentication context. +* Access credentials provisioned by the app from the identity provider, such as access tokens to perform authorized API calls. + +After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps: + +* Reference the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) package. + + [!INCLUDE[](~/includes/package-reference.md)] + +* Implement a custom . +* Register the custom authentication state provider in the dependency injection container. -## Integration with identity providers +Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/connect/) -Authentication of native apps happens via an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/connect/). +:::zone pivot="maui" -### .NET MAUI +.NET MAUI apps use [Xamarin.Essentials: Web Authenticator](/xamarin/essentials/web-authenticator): The `WebAuthenticator` class allows the app to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app. -[Xamarin.Essentials: Web Authenticator](/xamarin/essentials/web-authenticator): The `WebAuthenticator` class allows the app to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app. +:::zone-end -### WPF +:::zone pivot="wpf" WPF apps use the [Microsoft identity platform](/azure/active-directory/develop/) to integrate with Azure Active Directory (AAD) and AAD B2C. For guidance and examples, see the following resources: @@ -38,78 +52,207 @@ WPF apps use the [Microsoft identity platform](/azure/active-directory/develop/) * [Quickstart: Set up sign in for a desktop app using Azure Active Directory B2C](/azure/active-directory-b2c/quickstart-native-app-desktop) * [Configure authentication in a sample WPF desktop app by using Azure AD B2C](/azure/active-directory-b2c/configure-authentication-sample-wpf-desktop-app) -### Windows Forms +:::zone-end - - -## Integrate authentication - -Integrating authentication must achieve the following goals for Razor components and services: - -* Use the abstractions in the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) package, such as . -* React to changes in the authentication context. -* Access credentials provisioned by the app from the identity provider, such as access tokens to perform authorized API calls. +:::zone-end -After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps: +## Create a custom `AuthenticationStateProvider` without user change updates -* Reference the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) package. -* Implement a custom . -* Register the custom authentication state provider in the dependency injection container. +The is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes. -### Create a custom `AuthenticationStateProvider` +If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. Typically in this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthStateProvider` is an example implementation of a custom for this authentication scenario. -The is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes. +> [!NOTE] +> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to include your app's namespace when you implement the example in a production app. -If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. Typically in this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthenticationStateProvider` is an example implementation of a custom for this authentication scenario. +`ExternalAuthStateProvider.cs`: ```csharp using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Authorization; -public class ExternalAuthenticationStateProvider : AuthenticationStateProvider +public class ExternalAuthStateProvider : AuthenticationStateProvider { private readonly Task authenticationState; - public ExternalAuthenticationStateProvider(AuthenticatedUser user) => + public ExternalAuthStateProvider(AuthenticatedUser user) => authenticationState = Task.FromResult(new AuthenticationState(user.Principal)); public override Task GetAuthenticationStateAsync() => authenticationState; } -public class AuthenticatedUser +public class AuthenticatedUser { public ClaimsPrincipal Principal { get; set; } = new(); } ``` -Add the Blazor abstractions to the DI container: +:::zone pivot="maui" + +In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, the following steps: + +* Add required namespaces. +* Add the authorization services and Blazor abstractions to the service collection. +* Build the service collection. +* Resolve the `AuthenticatedUser` service and set the authenticated user's claims principal. See your identity provider's documentation for details. +* Return the built host. + +Add namespaces for and : + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +using System.Security.Claims; +``` + +Remove the following line of code that returns a built `Microsoft.Maui.Hosting.MauiApp`: + +```diff +- return builder.Build(); +``` + +Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. + +```csharp +builder.Services.AddAuthorizationCore(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +var host = builder.Build(); + +var authenticatedUser = host.Services.GetRequiredService(); + +/* +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; +``` + +:::zone-end + +:::zone pivot="wpf" + +In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), the following steps: + +* Add required namespaces. +* Add the authorization services and Blazor abstractions to the service collection. +* Build the service collection and add the built service collection as a resource to the app's `ResourceDictionary`. +* Resolve the `AuthenticatedUser` service and set the authenticated user's claims principal. See your identity provider's documentation for details. +* Return the built host. + +Add namespaces for and : + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +using System.Security.Claims; +``` + +Remove the following line of code that adds the built service collection as a resource to the app's `ResourceDictionary`: + +```diff +- Resources.Add("services", serviceCollection.BuildServiceProvider()); +``` + +Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. + +```csharp +serviceCollection.AddAuthorizationCore(); +serviceCollection.AddScoped(); +serviceCollection.AddSingleton(); +var services = serviceCollection.BuildServiceProvider(); +Resources.Add("services", services); + +var authenticatedUser = services.GetRequiredService(); + +/* +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; +``` + +:::zone-end + +:::zone pivot="winforms" + +In the `Form1`'s constructor (`Form1.cs`), the following steps: + +* Add required namespaces. +* Add the authorization services and Blazor abstractions to the service collection. +* Build the service collection and add the built service collection to the app's service provider. +* Resolve the `AuthenticatedUser` service and set the authenticated user's claims principal. See your identity provider's documentation for details. +* Return the built host. + +Add namespaces for and : + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +using System.Security.Claims; +``` + +Remove the following line of code that sets the built service collection to the app's service provider: + +```diff +- blazorWebView1.Services = services.BuildServiceProvider(); +``` + +Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. ```csharp services.AddAuthorizationCore(); -services.AddScoped(); +services.AddScoped(); services.AddSingleton(); +var serviceCollection = services.BuildServiceProvider(); +blazorWebView1.Services = serviceCollection; + +var authenticatedUser = serviceCollection.GetRequiredService(); + +/* +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; ``` -To update the user while the Blazor app is running, call within the implementation, using ***either*** of the following approaches: +:::zone-end + +## Create a custom `AuthenticationStateProvider` with user change updates + +To update the user while the Blazor app is running, call within the implementation using ***either*** of the following approaches: * [Signal an authentication update from outside of the `BlazorWebView`](#signal-an-authentication-update-from-outside-of-the-blazorwebview-option-1)) * [Handle authentication within the `BlazorWebView`](#handle-authentication-within-the-blazorwebview-option-2) ### Signal an authentication update from outside of the `BlazorWebView` (Option 1) -The following example uses a global service to signal an authentication update. We recommend that the service offer an event that the can subscribe to, where the event invokes . +A custom can use a global service to signal an authentication update. We recommend that the service offer an event that the can subscribe to, where the event invokes . > [!NOTE] -> `AuthenticatedUser` in the following example is registered in the dependency injection container later in this article. +> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to include your app's namespace when you implement the example in a production app. + +`ExternalAuthStateProvider.cs`: ```csharp using System; @@ -117,11 +260,11 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Authorization; -public class ExternalAuthenticationStateProvider : AuthenticationStateProvider +public class ExternalAuthStateProvider : AuthenticationStateProvider { private AuthenticationState currentUser; - public ExternalAuthenticationStateProvider(ExternalAuthService service) + public ExternalAuthStateProvider(ExternalAuthService service) { currentUser = new AuthenticationState(service.CurrentUser); @@ -157,51 +300,103 @@ public class ExternalAuthService } ``` -Add the Blazor abstractions to the DI container: +:::zone pivot="maui" + +In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, add a namespace for : + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +``` + +Add the authorization services and Blazor abstractions to the service collection in the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`: + +```csharp +builder.Services.AddAuthorizationCore(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +``` + +:::zone-end + +:::zone pivot="wpf" + +In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add a namespace for : + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +``` + +Add the authorization services and the Blazor abstractions to the service collection in the `MainWindow`'s constructor (`MainWindow.xaml.cs`): + +```csharp +serviceCollection.AddAuthorizationCore(); +serviceCollection.AddScoped(); +serviceCollection.AddSingleton(); +``` + +:::zone-end + +:::zone pivot="winforms" + +In the `Form1`'s constructor (`Form1.cs`), add a namespace for : + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +``` + +Add the authorization services and Blazor abstractions to the service collection in `Form1`'s constructor (`Form1.cs`): ```csharp services.AddAuthorizationCore(); -services.AddScoped(); +services.AddScoped(); services.AddSingleton(); ``` -From anywhere in the app, we can resolve the `ExternalAuthService` service after we have authenticated the user and set the principal property to the authenticated user before we start the Blazor application: +:::zone-end + +Wherever the app authenticates a user, resolve the `ExternalAuthService` service: + +```csharp +var authService = host.Services.GetRequiredService(); +``` + +Execute your custom OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. The authenticated user (`authenticatedUser` in the following example) is a new based on a new . + +Set the current user to the authenticated user: ```csharp -var authenticatedUser = services.GetRequiredService(); -ExternalAuthService.Principal = currentUser; +authService.CurrentUser = authenticatedUser; ``` -> [!NOTE] -> Alternatively, set the user's principal on instead of setting it via a service, which avoids use of the dependency injection container: -> -> ```csharp -> public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider -> { -> public override Task GetAuthenticationStateAsync() => -> Task.FromResult( -> new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? -> new ClaimsPrincipal(new ClaimsIdentity()))); -> } -> ``` -> -> In `MainWindow`'s constructor (`MainWindow.xaml.cs`): -> -> ```csharp -> services.AddScoped(); -> BlazorView.Services = services.BuildServiceProvider(); -> ``` +An alternative to the preceding approach is to set the user's principal on instead of setting it via a service, which avoids use of the dependency injection container: + +```csharp +public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider +{ + public override Task GetAuthenticationStateAsync() => + Task.FromResult( + new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? + new ClaimsPrincipal(new ClaimsIdentity()))); +} +``` + +Using the alternative approach, only authorization services (`.AddAuthorizationCore()`) and `CurrentThreadUserAuthenticationStateProvider` (`.AddScoped()`) are added to the service collection. ### Handle authentication within the `BlazorWebView` (Option 2) -Add additional methods to the `ExternalAuthenticationStateProvider` to trigger log in and log out and update the user: +A custom can include additional methods to trigger log in and log out and update the user. + +> [!NOTE] +> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to include your app's namespace when you implement the example in a production app. + +`ExternalAuthStateProvider.cs`: ```csharp using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Authorization; -public class ExternalAuthenticationStateProvider : AuthenticationStateProvider +public class ExternalAuthStateProvider : AuthenticationStateProvider { private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity()); @@ -232,8 +427,9 @@ public class ExternalAuthenticationStateProvider : AuthenticationStateProvider Return a new ClaimsPrincipal based on a new ClaimsIdentity. */ + var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity()); - return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity())); + return Task.FromResult(authenticatedUser); } public void Logout() @@ -250,7 +446,40 @@ In the preceding example: * The call to `LogInAsyncCore` triggers the login process. * The call to notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process. * Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete. -* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. +* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. The authenticated user (`authenticatedUser`) is a new based on a new . + +:::zone pivot="maui" + +In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, add the authorization services and the Blazor abstraction to the service collection in the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`: + +```csharp +builder.Services.AddAuthorizationCore(); +builder.Services.AddScoped(); +``` + +:::zone-end + +:::zone pivot="wpf" + +In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add the authorization services and the Blazor abstraction to the service collection in the `MainWindow`'s constructor (`MainWindow.xaml.cs`): + +```csharp +serviceCollection.AddAuthorizationCore(); +serviceCollection.AddScoped(); +``` + +:::zone-end + +:::zone pivot="winforms" + +In the `Form1`'s constructor (`Form1.cs`), add the authorization services and the Blazor abstraction to the service collection in `Form1`'s constructor (`Form1.cs`): + +```csharp +services.AddAuthorizationCore(); +services.AddScoped(); +``` + +:::zone-end The following `LoginComponent` component demonstrates how to log in a user. In a typical app, the `LoginComponent` component is only shown in a parent component if the user isn't logged into the app. @@ -265,7 +494,7 @@ The following `LoginComponent` component demonstrates how to log in a user. In a { public async Task Login() { - await ((ExternalAuthenticationStateProvider)AuthenticationStateProvider) + await ((ExternalAuthStateProvider)AuthenticationStateProvider) .LoginAsync(); } } @@ -284,24 +513,17 @@ The following `LogoutComponent` component demonstrates how to log out a user. In { public async Task Logout() { - await ((ExternalAuthenticationStateProvider)AuthenticationStateProvider) + await ((ExternalAuthStateProvider)AuthenticationStateProvider) .Logout(); } } ``` -Add the Blazor abstractions to the DI container: - -```csharp -services.AddAuthorizationCore(); -services.AddScoped(); -``` - ## Accessing other authentication information Blazor doesn't define an abstraction to deal with other credentials, such as access tokens to use for HTTP requests to web APIs. We recommend following the identity provider's guidance to manage the user's credentials with the primitives that the identity provider's SDK provides. -It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive is registered with the DI container, consume the SDK's primitive within the app. +It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive are added to the service container, consume the SDK's primitive within the app. The Blazor framework isn't aware of a user's authentication credentials and doesn't interact with credentials in any way, so the app's code is free to follow whatever approach you deem most convenient. However, follow the general security guidance in the next section, [Other authentication security considerations](#other-authentication-security-considerations), when implementing authentication code in an app. diff --git a/aspnetcore/zone-pivot-groups.yml b/aspnetcore/zone-pivot-groups.yml index a598186e4a45..3b610db3bb16 100644 --- a/aspnetcore/zone-pivot-groups.yml +++ b/aspnetcore/zone-pivot-groups.yml @@ -30,3 +30,13 @@ groups: title: macOS - id: windows title: Windows +- id: blazor-hybrid-frameworks + title: Framework + prompt: Target framework + pivots: + - id: maui + title: .NET MAUI + - id: wpf + title: WPF + - id: winforms + title: Windows Forms From bc6846a7a7493e878bdc6ecddefb08acbf2c3731 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 12 May 2022 05:19:11 -0500 Subject: [PATCH 09/20] Updates --- aspnetcore/blazor/hybrid/security/index.md | 61 ++++++++++------------ 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/aspnetcore/blazor/hybrid/security/index.md b/aspnetcore/blazor/hybrid/security/index.md index ca63b6a9de3a..105f322c0ef2 100644 --- a/aspnetcore/blazor/hybrid/security/index.md +++ b/aspnetcore/blazor/hybrid/security/index.md @@ -5,7 +5,7 @@ description: Learn about Blazor Hybrid authentication and authorization scenario monikerRange: '>= aspnetcore-6.0' ms.author: riande ms.custom: mvc -ms.date: 05/11/2022 +ms.date: 05/12/2022 no-loc: [".NET MAUI", "Mac Catalyst", "Blazor Hybrid", Home, Privacy, Kestrel, appsettings.json, "ASP.NET Core Identity", cookie, Cookie, Blazor, "Blazor Server", "Blazor WebAssembly", "Identity", "Let's Encrypt", Razor, SignalR] uid: blazor/hybrid/security/index zone_pivot_groups: blazor-hybrid-frameworks @@ -16,7 +16,7 @@ This article describes ASP.NET Core's support for the configuration and manageme [!INCLUDE[](~/blazor/includes/blazor-hybrid-preview-notice.md)] -Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Follow the external guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article. +Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/connect/). Follow the guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article. Integrating authentication must achieve the following goals for Razor components and services: @@ -30,11 +30,9 @@ After authentication is added to a .NET MAUI, WPF, or Windows Forms app and user [!INCLUDE[](~/includes/package-reference.md)] -* Implement a custom . +* Implement a custom , which is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes. * Register the custom authentication state provider in the dependency injection container. -Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/connect/) - :::zone pivot="maui" .NET MAUI apps use [Xamarin.Essentials: Web Authenticator](/xamarin/essentials/web-authenticator): The `WebAuthenticator` class allows the app to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app. @@ -65,12 +63,10 @@ Windows Forms apps use ... ## Create a custom `AuthenticationStateProvider` without user change updates -The is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes. - -If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. Typically in this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthStateProvider` is an example implementation of a custom for this authentication scenario. +If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. In this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthStateProvider` is an example implementation of a custom for this authentication scenario. > [!NOTE] -> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to include your app's namespace when you implement the example in a production app. +> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app. `ExternalAuthStateProvider.cs`: @@ -98,15 +94,15 @@ public class AuthenticatedUser :::zone pivot="maui" -In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, the following steps: +The following steps describe how to: * Add required namespaces. * Add the authorization services and Blazor abstractions to the service collection. * Build the service collection. -* Resolve the `AuthenticatedUser` service and set the authenticated user's claims principal. See your identity provider's documentation for details. +* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details. * Return the built host. -Add namespaces for and : +In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, add namespaces for and : ```csharp using Microsoft.AspNetCore.Components.Authorization; @@ -146,15 +142,15 @@ return host; :::zone pivot="wpf" -In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), the following steps: +The following steps describe how to: * Add required namespaces. * Add the authorization services and Blazor abstractions to the service collection. * Build the service collection and add the built service collection as a resource to the app's `ResourceDictionary`. -* Resolve the `AuthenticatedUser` service and set the authenticated user's claims principal. See your identity provider's documentation for details. +* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details. * Return the built host. -Add namespaces for and : +In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add namespaces for and : ```csharp using Microsoft.AspNetCore.Components.Authorization; @@ -193,15 +189,14 @@ authenticatedUser.Principal = user; :::zone pivot="winforms" -In the `Form1`'s constructor (`Form1.cs`), the following steps: +The following steps describe how to: * Add required namespaces. * Add the authorization services and Blazor abstractions to the service collection. * Build the service collection and add the built service collection to the app's service provider. -* Resolve the `AuthenticatedUser` service and set the authenticated user's claims principal. See your identity provider's documentation for details. -* Return the built host. +* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details. -Add namespaces for and : +In the `Form1`'s constructor (`Form1.cs`), add namespaces for and : ```csharp using Microsoft.AspNetCore.Components.Authorization; @@ -250,7 +245,7 @@ To update the user while the Blazor app is running, call can use a global service to signal an authentication update. We recommend that the service offer an event that the can subscribe to, where the event invokes . > [!NOTE] -> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to include your app's namespace when you implement the example in a production app. +> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app. `ExternalAuthStateProvider.cs`: @@ -308,7 +303,7 @@ In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, add a namespace fo using Microsoft.AspNetCore.Components.Authorization; ``` -Add the authorization services and Blazor abstractions to the service collection in the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`: +Add the authorization services and Blazor abstractions to the service collection: ```csharp builder.Services.AddAuthorizationCore(); @@ -326,7 +321,7 @@ In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add a namespace for can include additional methods to trigger log in and log out and update the user. > [!NOTE] -> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to include your app's namespace when you implement the example in a production app. +> The following custom doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app. `ExternalAuthStateProvider.cs`: @@ -450,7 +445,7 @@ In the preceding example: :::zone pivot="maui" -In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, add the authorization services and the Blazor abstraction to the service collection in the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`: +In the `MauiProgram.CreateMauiApp` method of `MainWindow.cs`, add the authorization services and the Blazor abstraction to the service collection: ```csharp builder.Services.AddAuthorizationCore(); @@ -461,7 +456,7 @@ builder.Services.AddScoped