Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save javiercn/85a6d4ce46a78239ccb427baad3a66da to your computer and use it in GitHub Desktop.
Save javiercn/85a6d4ce46a78239ccb427baad3a66da to your computer and use it in GitHub Desktop.
Authentication docs

Authentication for client-side blazor applications

Blazor webassembly applications should be treated as Single Page Applications for the purposes of security. When it comes to authenticating single page applications there are several approaches, but the most common and extended one is to use an approach based on oAuth like Open ID Connect.

Blazor webassembly supports authenticating and authorizing your applications using Open ID Connect via a the Microsoft.AspNetCore.Components.WebAssembly.Authentication library.

This library provides a set of primitives that allow Blazor webassembly applications to seamlessly authenticate against ASP.NET Core backends using Identity with the ApiAuthorization support that we built on top of Identity Server and to authenticate against third-party Identity Providers so long as they support Open ID Connect.

The authentication support on the webassembly side is built on top of the oidc-client.js library which we use to handle the underlying authentication protocol details.

There are other possible options for authenticating single page applications, like using SameSite cookies, but as part of our design process we have settled on oAuth and specifically on Open ID Connect as the best option for authenticating Blazor webassembly applications. The reasons we chose to use a token based protocol compared to cookie are both functional and security related:

  • Using a token based protocol offers a smaller attack surface area, as the tokens are not sent in all requests.
  • You don't have to protect your server endpoints against CSRF as the tokens need to be sent explicitly.
    • This allows you to host Blazor webassembly applications alongside MVC or Razor pages applications.
  • Tokens have more limited permissions than cookies, for example they can't be used to manage the user account or change their password unless you implement that functionality explicitly.
  • Tokens have a shorter lifetime (1h by default) which limits the attack window and can be revoked if necessary.
  • Tokens (in this case self-contained tokens JWTs) offer guarantees to the client and the server about the authentication process.
    • The client has the means to detect and validate that the tokens it receives are legitimate and were emitted as part a given authentication process.
    • For example, if a third paryt tried to switch a token in the middle of the authentication process, the client can detect that and avoid using the token.
  • Tokens and in this case oAuth and Open ID Connect don't rely on the user agent behaving correctly to ensure that the application is secure.
  • Token based protocols (in this case oAuth and Open ID Connect) allow for authenticating and authorizing hosted and standalone applications with the same set of security caracteristics.

Authenticating Blazor webassembly applications with Open ID Connect

The Microsoft.AspNetCore.Components.WebAssembly.Authentication offers several primitives to implement authentication and authorization using Open ID Connect. In broad terms, the way authentication works is as follows:

  • When an anonymous user clicks the login button or tries to access a page with the [Authorize] attribute applied to it, gets redirected to the authentication/login page.
  • In this page, the authentication library prepares the application to be redirected to the authorization endpoint. This endpoint is outside of the Blazor webassembly application, can even be hosted in a separate origin and is responsible for determining whether the user is authenticated or not and to issue one or more tokens in response.
    • If the user is not authenticated, the user gets redirected to the underlying authentication system being used, in the common case this is ASP.NET Core Identity.
    • If the user was already authenticated, the authorization endpoint generates the appropriate response and redirects the browser back to authentication/login-callback.
  • When the Blazor webassembly application starts and loads the authentication/login-callback endpoint, the authentication response is processed and as a result the user is authenticated.
  • If something goes wrong during this process, the user is sent to the authentication/login-failed page and an error is displayed.
  • If the authentication process completes successfully, the user will be authenticated and optionally sent back to the protected url he was trying to originally visit.

Authenticating hosted applications using Identity and the authentication and authorization support for single page applications in ASP.NET Core.

To create a new Blazor hosted application with authentication from within Visual Studio:

  • Open Visual Studio
  • Select File -> New Project -> Blazor Application
  • On the right top corner click on Change under the Authentication section
  • On the window select Individual User Accounts and press OK
  • Check the ASP.NET Core hosted checkbox on the bottom right corner.
  • Click on Create
  • After the project creation completes, you will see three projects

Overview of the ASP.NET Core server application

The following sections describe additions to the project when authentication support is included:

Startup class

The Startup class has the following additions:

  • Inside the Startup.ConfigureServices method:

    • Identity with the default UI:

      services.AddDbContext<ApplicationDbContext>(options =>
          options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
      
      services.AddDefaultIdentity<ApplicationUser>()
          .AddDefaultUI(UIFramework.Bootstrap4)
          .AddEntityFrameworkStores<ApplicationDbContext>();
    • IdentityServer with an additional AddApiAuthorization helper method that sets up some default ASP.NET Core conventions on top of IdentityServer:

      services.AddIdentityServer()
          .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
    • Authentication with an additional AddIdentityServerJwt helper method that configures the app to validate JWT tokens produced by IdentityServer:

      services.AddAuthentication()
          .AddIdentityServerJwt();
  • Inside the Startup.Configure method:

    • The authentication middleware that is responsible for validating the request credentials and setting the user on the request context:

      app.UseAuthentication();
    • The IdentityServer middleware that exposes the Open ID Connect endpoints:

      app.UseIdentityServer();

AddApiAuthorization

This helper method configures IdentityServer to use our supported configuration. IdentityServer is a powerful and extensible framework for handling app security concerns. At the same time, that exposes unnecessary complexity for the most common scenarios. Consequently, a set of conventions and configuration options is provided to you that are considered a good starting point. Once your authentication needs change, the full power of IdentityServer is still available to customize authentication to suit your needs.

AddIdentityServerJwt

This helper method configures a policy scheme for the app as the default authentication handler. The policy is configured to let Identity handle all requests routed to any subpath in the Identity URL space "/Identity". The JwtBearerHandler handles all other requests. Additionally, this method registers an <<ApplicationName>>API API resource with IdentityServer with a default scope of <<ApplicationName>>API and configures the JWT Bearer token middleware to validate tokens issued by IdentityServer for the app.

WeatherForecastController

In the Controllers\WeatherForecastController.cs file, notice the [Authorize] attribute applied to the class that indicates that the user needs to be authorized based on the default policy to access the resource. The default authorization policy happens to be configured to use the default authentication scheme, which is set up by AddIdentityServerJwt to the policy scheme that was mentioned above, making the JwtBearerHandler configured by such helper method the default handler for requests to the app.

ApplicationDbContext

In the Data\ApplicationDbContext.cs file, notice the same DbContext is used in Identity with the exception that it extends ApiAuthorizationDbContext (a more derived class from IdentityDbContext) to include the schema for IdentityServer.

To gain full control of the database schema, inherit from one of the available Identity DbContext classes and configure the context to include the Identity schema by calling builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value) on the OnModelCreating method.

OidcConfigurationController

In the Controllers\OidcConfigurationController.cs file, notice the endpoint that's provisioned to serve the OIDC parameters that the client needs to use.

appsettings.json

In the appsettings.json file of the project root, there's a new IdentityServer section that describes the list of configured clients. In the following example, there's a single client. The client name corresponds to the app name and is mapped by convention to the OAuth ClientId parameter. The profile indicates the app type being configured. It's used internally to drive conventions that simplify the configuration process for the server. There are several profiles available, as explained in the Application profiles section.

"IdentityServer": {
  "Clients": {
    "BlazorApplicationWithAuthentication.Client": {
      "Profile": "IdentityServerSPA"
    }
  }
}

appsettings.Development.json

In the appsettings.Development.json file of the project root, there's an IdentityServer section that describes the key used to sign tokens. When deploying to production, a key needs to be provisioned and deployed alongside the app, as explained in the Deploy to production section.

"IdentityServer": {
  "Key": {
    "Type": "Development"
  }
}

Overview of the Blazor webassembly application

Project file

The application uses the Microsoft.AspNetCore.Components.WebAssembly.Authentication which contains the set of primitives that help the application authenticate users and get tokens to call protected APIs.

<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="..." />

Program.cs

The support for authenticating users is plugged into the service container by the extension method provided inside the Microsoft.AspNetCore.Components.WebAssembly.Authentication package. This method sets up all the services needed for the application to interact with the existing authorization system.

builder.Services.AddApiAuthorization();

By default, it loads the configuration for the application by convention from _configuration/<<client-id>>. The client ID used by convention is the application assembly name. This url can be changed to point to a separate endpoint by calling the overload with options.

Index.html

The index.html page includes a script that defines the AuthenticationService in JavaScript that is used to handle the low level details of the Open ID Connect protocol. The Blazor webassembly application will call methods defined on this script internally to perform the authentication operations.

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>

App.razor

The App.razor component is very similar to the one that can be found in server-side Blazor apps:

  • The CascadingAuthenticationState component takes care of exposing the AuthenticationState to the rest of the application.
  • The AuthorizeRouteView makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component takes care of redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin.razor

This component takes care of redirecting unauthorized users to the login page and making sure we preserve the current url they were trying to access so that they can return to that page once the authentication is successful.

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
    }
}

Login display

This component is added as part of the MainLayout.razor component and takes care of:

  • For authenticated users:
    • Displays the current user name.
    • Offers a link to the user profile page in ASP.NET Core Identity.
    • Offers a button to log out of the application.
  • For anonymous users:
    • Offers the option to register.
    • Offers the option to log in.
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        <a href="authentication/profile">Hello, @context.User.Identity.Name!</a>
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/register">Register</a>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication.razor

This page defines the routes needed for handling different authentication stages in the application. It uses the RemoteauthenticatorView component that comes in the Microsoft.AspNetCore.Components.WebAssembly.Authentication and that takes care of performing the appropriate actions at each stage.

Several authentication aspects can be customized through this component as detailed in PLACEHOLDER.

@page "/authentication/{action}"
@inject IAccessTokenProvider AuthenticationService
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

FetchData.razor

This page shows how to provision an access token and use that access token to call a protected resource API on the hosting asp.net core application.

The @attribute [Authorize] indicates the blazor webassembly authorization system that the user must be authorized in order to visit this area of the application. It is fundamental to understand that this doesn't prevent the API on the server from being called without proper credentials and that the server needs to also use [Authorize] on the appropriate endpoints to correctly protect them.

AuthenticationService.RequestAccessToken(); takes care of requesting an access token that can be added to the request to call the API. If the token is cached or the service is able to provision a new access token without user interaction, the token request will succeed; otherwise, it will fail.

In order to get the actual token to include in the request you need to check that the request succedeed by calling tokenResult.TryGetToken(out var token).

If the request was successful, the token variable will be populated with the access token. The value property in the token exposes the literal string to include in the Authorization request header.

If the request failed because the token could not be provisioned without user interaction, the token result will contain a redirect url. As described in PLACEHOLDER navigating to this url will take you to the login page and back to the current page after a successful authentication.

@page "/fetchdata"
...
@attribute [Authorize]
...
@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri(Navigation.BaseUri);

        var tokenResult = await AuthenticationService.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
            forecasts = await httpClient.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        else
        {
            Navigation.NavigateTo(tokenResult.RedirectUrl);
        }

    }
}

Authenticating standalone applications using Microsoft.AspNetCore.Components.WebAssembly.Authentication

To create a new Blazor hosted application with authentication from within Visual Studio:

  • Open Visual Studio
  • Select File -> New Project -> Blazor Application
  • On the right top corner click on Change under the Authentication section
  • On the window select Individual User Accounts and press OK
  • Click on Create
  • After the project creation completes, you will see three projects

Overview of the Blazor Webassembly application

Project file

The application uses the Microsoft.AspNetCore.Components.WebAssembly.Authentication which contains the set of primitives that help the application authenticate users and get tokens to call protected APIs.

<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="..." />

Program.cs

The support for authenticating users is plugged into the service container by the extension method provided inside the Microsoft.AspNetCore.Components.WebAssembly.Authentication package. This method sets up all the services needed for the application to interact with the existing authorization system.

builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.Authority = "<<authority>>";
    options.ProviderOptions.ClientId = "<<client-id>>";
});

Authentication support for standalone applications is offered using Open ID Connect, the AddOidcAuthentication method accepts a callback to configure the parameters needed to authenticate an application using Open ID Connect.

The values needed for configuring the application can be obtained from your Identity Provider (Microsoft, Google, or other Open ID Connect compliant identity provider) when you register your app with them.

Index.html

The index.html page includes a script that defines the AuthenticationService in JavaScript that is used to handle the low level details of the Open ID Connect protocol. The Blazor webassembly application will call methods defined on this script internally to perform the authentication operations.

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>

App.razor

The App.razor component is very similar to the one that can be found in server-side Blazor apps:

  • The CascadingAuthenticationState component takes care of exposing the AuthenticationState to the rest of the application.
  • The AuthorizeRouteView makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component takes care of redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin.razor

This component takes care of redirecting unauthorized users to the login page and making sure we preserve the current url they were trying to access so that they can return to that page once the authentication is successful.

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
    }
}

Login display

This component is added as part of the MainLayout.razor component and takes care of:

  • For authenticated users:
    • Displays the current user name.
    • Offers a link to the user profile page in ASP.NET Core Identity.
    • Offers a button to log out of the application.
  • For anonymous users:
    • Offers the option to register.
    • Offers the option to log in.
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        <a href="authentication/profile">Hello, @context.User.Identity.Name!</a>
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/register">Register</a>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication.razor

This page defines the routes needed for handling different authentication stages in the application. It uses the RemoteauthenticatorView component that comes in the Microsoft.AspNetCore.Components.WebAssembly.Authentication and that takes care of performing the appropriate actions at each stage.

Several authentication aspects can be customized through this component as detailed in PLACEHOLDER.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

Authenticating Blazor webassembly hosted applications using Azure Active Directory with Microsoft.Authentication.WebAssembly.Msal

To create a Blazor webassembly hosted application that uses Azure Active Directory for authentication follow these steps:

  • Create two applications in Azure Active Directory following the steps in PLACEHOLDER
  • Capture the following data:
    • Application ID (Client ID) for the single page application
    • Tenant ID for the Azure Active Directory tenant.
    • App ID URI for the Application with exposed APIs (Your server API application)
    • The default scope that you want to initially request, you can add more scopes or change the scopes in the Program.cs if you need to later.
    • API Application ID (Client ID) for the server API.
  • From the command line, run the following command:
    dotnet new blazorwasm --no-restore -ho -au SingleOrg --client-id "<<single-page-client-id>>" --tenant-id "<<tenant-id>>" --app-id-uri "<<app-id-uri>>" --default-scope "<<default-scope>>" --api-client-id "<<server-api-client-id>>"

Overview of the ASP.NET Core server application

Project file

The support for authenticating and authorizing calls to ASP.NET Core Web APIs is provided by the Microsoft.AspNetCore.Authentication.AzureAD.UI

<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="3.1.0" />

Startup.cs

The AddAuthentication method sets up authentication services within the app and configures the JWT Bearer handler to be the default authentication method. The AddAzureADBearer method sets up the specific parameters in the JWT Bearer handler required to validate tokens emitted by the Azure Active Directory.

  services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
      .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));

UseAuthentication and UseAuthorization ensure that the application tries to parse and validate tokens on incoming requests and that any request trying to access a protected resource without proper credentials fails.

  app.UseAuthentication();
  app.UseAuthorization();

appsettings.json

Contains the options to configure the JWT bearer handler used to validate access tokens.

{
  ...
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<<tenant-id>>",
    "ClientId": "<<api-client-id>>",
  },
  ...
}

WeatherForecastController.cs

This controller exposes a protected API with the [Authorize] attribute applied to the controller. It is important to understand that the [Authorize] attribute in this API controller is the only thing that protect this API from unauthorized access and that the [Authorize] attribute used in the Blazor webassembly application only serves as a hint to the application that the user needs to be authorized for the application to work correctly.

    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
          ...
        }
    }

Overview of the Blazor Webassembly application

Project file

The application uses the Microsoft.Authentication.WebAssembly.Msal which contains the set of primitives that help the application authenticate users and get tokens to call protected APIs in Azure Active Directory.

<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="..." />

Program.cs

The support for authenticating users is plugged into the service container by the extension method provided inside the Microsoft.Authentication.WebAssembly.Msal package. This method sets up all the services needed for the application to interact with the existing authorization system.

builder.Services.AddMsalAuthentication(options =>
{
    var authentication = options.ProviderOptions.Authentication;
    authentication.Authority = "https://login.microsoftonline.com/<<tenant-id>>";
    authentication.ClientId = "<<client-id>>";
    options.ProviderOptions.DefaultAccessTokenScopes.Add("api://<app-id-uri>>/<<default-scope>>");
});

Authentication support for standalone applications is offered using Open ID Connect, the AddMsalAuthentication method accepts a callback to configure the parameters needed to authenticate the application with Azure Active Directory.

The authority is formed by appending the tenant id to the Azure Active Directory base url https://login.microsoftonline.com

The client id is the application id defined for the single page application when registering the app.

The default access token scopes represent the list of access token scopes that will be included by default in the sign in request and that will be used to provision an access token inmediately after. All scopes must belong to the same application as per Azure Active Directory rules.

Index.html

The index.html page includes a script that defines the AuthenticationService in JavaScript that is used to handle the low level details of the Open ID Connect protocol. The Blazor webassembly application will call methods defined on this script internally to perform the authentication operations.

<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

App.razor

The App.razor component is very similar to the one that can be found in server-side Blazor apps:

  • The CascadingAuthenticationState component takes care of exposing the AuthenticationState to the rest of the application.
  • The AuthorizeRouteView makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component takes care of redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin.razor

This component takes care of redirecting unauthorized users to the login page and making sure we preserve the current url they were trying to access so that they can return to that page once the authentication is successful.

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
    }
}

Login display

This component is added as part of the MainLayout.razor component and takes care of:

  • For authenticated users:
    • Displays the current user name.
    • Offers a button to log out of the application.
  • For anonymous users:
    • Offers the option to log in.

The template only supports authenticating users in Azure Active Directory. Other tasks like editing the user profile or registering new users need to be handled following your Azure Active Directory tenant policies.

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication.razor

This page defines the routes needed for handling different authentication stages in the application. It uses the RemoteauthenticatorView component that comes in the Microsoft.AspNetCore.Components.WebAssembly.Authentication and that takes care of performing the appropriate actions at each stage.

Several authentication aspects can be customized through this component as detailed in PLACEHOLDER.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

FetchData.razor

This page shows how to provision an access token and use that access token to call a protected resource API on the hosting asp.net core application.

The @attribute [Authorize] indicates the blazor webassembly authorization system that the user must be authorized in order to visit this area of the application. It is fundamental to understand that this doesn't prevent the API on the server from being called without proper credentials and that the server needs to also use [Authorize] on the appropriate endpoints to correctly protect them.

AuthenticationService.RequestAccessToken(); takes care of requesting an access token that can be added to the request to call the API. If the token is cached or the service is able to provision a new access token without user interaction, the token request will succeed; otherwise, it will fail.

In order to get the actual token to include in the request you need to check that the request succedeed by calling tokenResult.TryGetToken(out var token).

If the request was successful, the token variable will be populated with the access token. The value property in the token exposes the literal string to include in the Authorization request header.

If the request failed because the token could not be provisioned without user interaction, the token result will contain a redirect url. As described in PLACEHOLDER navigating to this url will take you to the login page and back to the current page after a successful authentication.

@page "/fetchdata"
...
@attribute [Authorize]
...
@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri(Navigation.BaseUri);

        var tokenResult = await AuthenticationService.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
            forecasts = await httpClient.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        else
        {
            Navigation.NavigateTo(tokenResult.RedirectUrl);
        }

    }
}

Authenticating Blazor webassembly standalone applications using Azure Active Directory with Microsoft.Authentication.WebAssembly.Msal

To create a Blazor webassembly hosted application that uses Azure Active Directory for authentication follow these steps:

  • Create two applications in Azure Active Directory following the steps in PLACEHOLDER
  • Capture the following data:
    • Application ID (Client ID) for the single page application
    • Tenant ID for the Azure Active Directory tenant.
  • From the command line, run the following command:
    dotnet new blazorwasm -au SingleOrg --client-id "<<single-page-client-id>>" --tenant-id "<<tenant-id>>" 

Overview of the Blazor Webassembly application

Project file

The application uses the Microsoft.Authentication.WebAssembly.Msal which contains the set of primitives that help the application authenticate users and get tokens to call protected APIs in Azure Active Directory.

<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="..." />

Program.cs

The support for authenticating users is plugged into the service container by the extension method provided inside the Microsoft.Authentication.WebAssembly.Msal package. This method sets up all the services needed for the application to interact with the existing authorization system.

builder.Services.AddMsalAuthentication(options =>
{
    var authentication = options.ProviderOptions.Authentication;
    authentication.Authority = "https://login.microsoftonline.com/<<tenant-id>>";
    authentication.ClientId = "<<client-id>>";
});

Authentication support for standalone applications is offered using Open ID Connect, the AddMsalAuthentication method accepts a callback to configure the parameters needed to authenticate the application with Azure Active Directory.

The authority is formed by appending the tenant id to the Azure Active Directory base url https://login.microsoftonline.com

The client id is the application id defined for the single page application when registering the app.

When creating standalone applications we don't configure the application to request an access token to talk to an API as we don't know what API you plan to talk to. The code in Program.cs can be changed as described below to provision a token as part of the sign in flow:

options.ProviderOptions.DefaultAccessTokenScopes.Add("<<insert-your-scope-here>>");

Index.html

The index.html page includes a script that defines the AuthenticationService in JavaScript that is used to handle the low level details of the Open ID Connect protocol. The Blazor webassembly application will call methods defined on this script internally to perform the authentication operations.

<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

App.razor

The App.razor component is very similar to the one that can be found in server-side Blazor apps:

  • The CascadingAuthenticationState component takes care of exposing the AuthenticationState to the rest of the application.
  • The AuthorizeRouteView makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component takes care of redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin.razor

This component takes care of redirecting unauthorized users to the login page and making sure we preserve the current url they were trying to access so that they can return to that page once the authentication is successful.

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
    }
}

Login display

This component is added as part of the MainLayout.razor component and takes care of:

  • For authenticated users:
    • Displays the current user name.
    • Offers a button to log out of the application.
  • For anonymous users:
    • Offers the option to log in.

The template only supports authenticating users in Azure Active Directory. Other tasks like editing the user profile or registering new users need to be handled following your Azure Active Directory tenant policies.

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication.razor

This page defines the routes needed for handling different authentication stages in the application. It uses the RemoteauthenticatorView component that comes in the Microsoft.AspNetCore.Components.WebAssembly.Authentication and that takes care of performing the appropriate actions at each stage.

Several authentication aspects can be customized through this component as detailed in PLACEHOLDER.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

Authenticating Blazor webassembly hosted applications using Azure Active Directory B2C with Microsoft.Authentication.WebAssembly.Msal

To create a Blazor webassembly hosted application that uses Azure Active Directory B2C for authentication follow these steps:

  • Create two applications in Azure Active Directory B2C following the steps in PLACEHOLDER
  • Capture the following data:
    • Base url for the B2C tenant, typically something like https://<<tenant>>.b2clogin.com
    • Sign-up/sign-in policy to use for login and registering users. See here for details.
    • Application ID (Client ID) for the single page application
    • Domain for the Azure Active Directory B2C tenant, typically something like <>.onmicrosoft.com.
    • App ID URI for the Application with exposed APIs (Your server API application). Only capture the value, that you configured not the https://.../ prefix included. The template will compute that.
    • The default scope that you want to initially request, you can add more scopes or change the scopes in the Program.cs if you need to later.
    • API Application ID (Client ID) for the server API.
  • From the command line, run the following command:
    dotnet new blazorwasm -ho -au IndividualB2C --aad-b2c-instance "https://<<instance-name>>.b2clogin.com/" -ssp "<<sign-up-sign-in-policy>>" --client-id "<<single-page-application-application-id>>" --domain "<<instance-name>>.onmicrosoft.com" --default-scope "<<default-scope>>" --app-id-uri "<app-id-uri>>" --api-client-id "<<api-application-id>>" 

Overview of the ASP.NET Core server application

Project file

The support for authenticating and authorizing calls to ASP.NET Core Web APIs is provided by the Microsoft.AspNetCore.Authentication.AzureADB2C.UI

<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="3.1.0" />

Startup.cs

The AddAuthentication method sets up authentication services within the app and configures the JWT Bearer handler to be the default authentication method. The AddAzureADB2CBearer method sets up the specific parameters in the JWT Bearer handler required to validate tokens emitted by the Azure Active Directory.

services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
    .AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));

UseAuthentication and UseAuthorization ensure that the application tries to parse and validate tokens on incoming requests and that any request trying to access a protected resource without proper credentials fails.

  app.UseAuthentication();
  app.UseAuthorization();

appsettings.json

Contains the options to configure the JWT bearer handler used to validate access tokens.

{
  ...
    "AzureAdB2C": {
    "Instance": "https://<<aad-b2c-instance>>.b2clogin.com/",
    "ClientId": "<<api-client-id>>",
    "Domain": "<<domain>>",
    "SignUpSignInPolicyId": "<<sign-up-sign-in-policy>>"
  },
  ...
}

WeatherForecastController.cs

This controller exposes a protected API with the [Authorize] attribute applied to the controller. It is important to understand that the [Authorize] attribute in this API controller is the only thing that protect this API from unauthorized access and that the [Authorize] attribute used in the Blazor webassembly application only serves as a hint to the application that the user needs to be authorized for the application to work correctly.

    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
          ...
        }
    }

Overview of the Blazor Webassembly application

Project file

The application uses the Microsoft.Authentication.WebAssembly.Msal which contains the set of primitives that help the application authenticate users and get tokens to call protected APIs in Azure Active Directory.

<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="..." />

Program.cs

The support for authenticating users is plugged into the service container by the extension method provided inside the Microsoft.Authentication.WebAssembly.Msal package. This method sets up all the services needed for the application to interact with the existing authorization system.

builder.Services.AddMsalAuthentication(options =>
{
    var authentication = options.ProviderOptions.Authentication;
    authentication.Authority = "https://<<aad-b2c-instance>>.b2clogin.com/<<domain>>/<<policy>>";
    authentication.ClientId = "<<single-page-application-client-id>>";
    authentication.ValidateAuthority = false;

    options.ProviderOptions.DefaultAccessTokenScopes.Add("https://<<domain>>/<<api-id-uri>>/<<scope>>");
});

The AddMsalAuthentication method accepts a callback to configure the parameters needed to authenticate the application with Azure Active Directory.

The authority is formed by appending the AAD B2C instance url, the domain and the sign-up/sign-in policy for the Azure Active Directory B2C tenant.

The client id is the application id defined for the single page application when registering the app.

The default access token scopes represent the list of access token scopes that will be included by default in the sign in request and that will be used to provision an access token inmediately after. All scopes must belong to the same application as per Azure Active Directory rules.

Index.html

The index.html page includes a script that defines the AuthenticationService in JavaScript that is used to handle the low level details of the Open ID Connect protocol. The Blazor webassembly application will call methods defined on this script internally to perform the authentication operations.

<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

App.razor

The App.razor component is very similar to the one that can be found in server-side Blazor apps:

  • The CascadingAuthenticationState component takes care of exposing the AuthenticationState to the rest of the application.
  • The AuthorizeRouteView makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component takes care of redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin.razor

This component takes care of redirecting unauthorized users to the login page and making sure we preserve the current url they were trying to access so that they can return to that page once the authentication is successful.

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
    }
}

Login display

This component is added as part of the MainLayout.razor component and takes care of:

  • For authenticated users:
    • Displays the current user name.
    • Offers a button to log out of the application.
  • For anonymous users:
    • Offers the option to log in, users can choose to register from within the login screen.

The template only supports authenticating users in Azure Active Directory B2C. Other tasks like editing the user profile need to be handled following your Azure Active Directory B2C tenant policies.

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication.razor

This page defines the routes needed for handling different authentication stages in the application. It uses the RemoteauthenticatorView component that comes in the Microsoft.AspNetCore.Components.WebAssembly.Authentication and that takes care of performing the appropriate actions at each stage.

Several authentication aspects can be customized through this component as detailed in PLACEHOLDER.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

FetchData.razor

This page shows how to provision an access token and use that access token to call a protected resource API on the hosting asp.net core application.

The @attribute [Authorize] indicates the blazor webassembly authorization system that the user must be authorized in order to visit this area of the application. It is fundamental to understand that this doesn't prevent the API on the server from being called without proper credentials and that the server needs to also use [Authorize] on the appropriate endpoints to correctly protect them.

AuthenticationService.RequestAccessToken(); takes care of requesting an access token that can be added to the request to call the API. If the token is cached or the service is able to provision a new access token without user interaction, the token request will succeed; otherwise, it will fail.

In order to get the actual token to include in the request you need to check that the request succedeed by calling tokenResult.TryGetToken(out var token).

If the request was successful, the token variable will be populated with the access token. The value property in the token exposes the literal string to include in the Authorization request header.

If the request failed because the token could not be provisioned without user interaction, the token result will contain a redirect url. As described in PLACEHOLDER navigating to this url will take you to the login page and back to the current page after a successful authentication.

@page "/fetchdata"
...
@attribute [Authorize]
...
@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri(Navigation.BaseUri);

        var tokenResult = await AuthenticationService.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
            forecasts = await httpClient.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        else
        {
            Navigation.NavigateTo(tokenResult.RedirectUrl);
        }

    }
}

Authenticating Blazor webassembly standalone applications using Azure Active Directory with Microsoft.Authentication.WebAssembly.Msal

To create a Blazor webassembly standalone application that uses Azure Active Directory B2C for authentication follow these steps:

  • Create an application in Azure Active Directory B2C following the steps in PLACEHOLDER
  • Capture the following data:
    • Base url for the B2C tenant, typically something like https://<<tenant>>.b2clogin.com
    • Sign-up/sign-in policy to use for login and registering users. See here for details.
    • Application ID (Client ID) for the single page application
    • Domain for the Azure Active Directory B2C tenant, typically something like <>.onmicrosoft.com.
  • From the command line, run the following command:
    dotnet new blazorwasm -ho -au IndividualB2C --aad-b2c-instance "https://<<instance-name>>.b2clogin.com/" -ssp "<<sign-up-sign-in-policy>>" --client-id "<<single-page-application-application-id>>" --domain "<<instance-name>>.onmicrosoft.com" 

Overview of the Blazor Webassembly application

Project file

The application uses the Microsoft.Authentication.WebAssembly.Msal which contains the set of primitives that help the application authenticate users and get tokens to call protected APIs in Azure Active Directory.

<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="..." />

Program.cs

The support for authenticating users is plugged into the service container by the extension method provided inside the Microsoft.Authentication.WebAssembly.Msal package. This method sets up all the services needed for the application to interact with the existing authorization system.

builder.Services.AddMsalAuthentication(options =>
{
    var authentication = options.ProviderOptions.Authentication;
    authentication.Authority = "https://<<aad-b2c-instance>>.b2clogin.com/<<domain>>/<<policy>>";
    authentication.ClientId = "<<single-page-application-client-id>>";
    authentication.ValidateAuthority = false;

    options.ProviderOptions.DefaultAccessTokenScopes.Add("https://<<domain>>/<<api-id-uri>>/<<scope>>");
});

The AddMsalAuthentication method accepts a callback to configure the parameters needed to authenticate the application with Azure Active Directory.

The authority is formed by appending the AAD B2C instance url, the domain and the sign-up/sign-in policy for the Azure Active Directory B2C tenant.

The client id is the application id defined for the single page application when registering the app.

When creating standalone applications we don't configure the application to request an access token to talk to an API as we don't know what API you plan to talk to. The code in Program.cs can be changed as described below to provision a token as part of the sign in flow:

options.ProviderOptions.DefaultAccessTokenScopes.Add("<<insert-your-scope-here>>");

Index.html

The index.html page includes a script that defines the AuthenticationService in JavaScript that is used to handle the low level details of the Open ID Connect protocol. The Blazor webassembly application will call methods defined on this script internally to perform the authentication operations.

<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>

App.razor

The App.razor component is very similar to the one that can be found in server-side Blazor apps:

  • The CascadingAuthenticationState component takes care of exposing the AuthenticationState to the rest of the application.
  • The AuthorizeRouteView makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component takes care of redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin.razor

This component takes care of redirecting unauthorized users to the login page and making sure we preserve the current url they were trying to access so that they can return to that page once the authentication is successful.

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
    }
}

Login display

This component is added as part of the MainLayout.razor component and takes care of:

  • For authenticated users:
    • Displays the current user name.
    • Offers a button to log out of the application.
  • For anonymous users:
    • Offers the option to log in, users can choose to register from within the login screen.

The template only supports authenticating users in Azure Active Directory B2C. Other tasks like editing the user profile need to be handled following your Azure Active Directory B2C tenant policies.

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code{
    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication.razor

This page defines the routes needed for handling different authentication stages in the application. It uses the RemoteauthenticatorView component that comes in the Microsoft.AspNetCore.Components.WebAssembly.Authentication and that takes care of performing the appropriate actions at each stage.

Several authentication aspects can be customized through this component as detailed in PLACEHOLDER.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}

Authenticating standalone applications with microsoft accounts

To create a Blazor webassembly standalone application that uses Microsoft accounts for authentication follow these steps:

  • Register an application following the steps in PLACEHOLDER

  • Capture the following data:

    • Application ID (Client ID) for the single page application
  • From the command line, run the following command:

    dotnet new blazorwasm -au SingleOrg --client-id "<<single-page-client-id>>" --tenant-id "common" 

    After this, you should be able to log-in using a Microsoft account and request access tokens for Microsoft APIs in the same way it is done in standalone Blazor applications provided that you have configured your app correctly. For more details see PLACEHOLDER

Handling token request errors

When a single page application authenticates a user using Open ID Connect, the authentication state is kept locally within the single page application and in the Identity Provider in the form of a session cookie that is set as a result of the user introducing their credentials.

The tokens that the Identity Provider emits for the user typically are valid for short periods of time (about 1h normally) so the client application needs to regularly fetch new tokens. Otherwise, you would be logged-out after the tokens granted expired. In the majority of cases Open ID Connect clients are able to provision new tokens without requiring the user to authenticate again thanks to the authentication state or "session" that is kept within the Identity Provider.

There are however, some cases in which the client can't get a token without user interaction, for example, when for some reason the user explitly logged out from the identity provider. (For example if you visited https://login.microsoftonline.com and logged out). In those scenarios your application will not know inmediately that the user logged out, and any token that it migh have received might no longer be valid or it won't be able to provision a new one without user interaction once the current one expires.

This is not something specific to token based authentication, but it is part of the nature of single page applications. A single page application using cookies would also fail to call a server API if the authentication cookie got removed.

For this reason, when we are performing API calls to protected resources we need to be aware of the fact that provision a new access token to call the API might require the user authenticating again and that, even if we have a token that seems to be valid, the call to the server might fail because the token got revoked by the user.

When we request a token there are two possible outcomes:

  • The request succeeds and we have a valid token.
  • The request fails and we need to authenticate again to get a new token.

When a token request fails, we need to decide whether we want to save any current state before we perform a redirection. We can do several things with increasing levels of complexity:

  • We can store the current page state in session storage and during OnInitializeAsync check if it is there to restore before we continue when we return to the current page after a successful authentication.
  • We can add a query string parameter and use that as a way to signal the application that it needs to re-hidrate the previously saved state.
  • We can add a query string parameter with a unique identifier to store things in session storage without risking collisions with other items.

The example below shows how you can preserve the state before you redirect to the login page and how you recover the previous state afterwards using the second option.

<EditForm Model="User" @onsubmit="OnSaveAsync">
    <label>User
        <InputText @bind-Value="User.Name" />
    </label>
    <label>Last name
        <InputText @bind-Value="User.LastName" />
    </label>
</EditForm>

@code {

    public class Profile
    {
        public string Name { get; set; }
        public string LastName { get; set; }
    }

    public Profile User { get; set; } = new Profile();

    protected async override Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;
        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            User = await JS.InvokeAsync<Profile>("sessionStorage.getState", "resumeSavingProfile");
        }
    }

    public async Task OnSaveAsync()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await AuthenticationService.RequestAccessToken(new AccessTokenRequestOptions
        {
            ReturnUrl = resumeUri
        });

        if (tokenResult.TryGetToken(out var token))
        {
            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
            await httpClient.PostJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setState", "resumeSavingProfile", User);
            Navigation.NavigateTo(tokenResult.RedirectUrl);
        }
    }
}

Saving application state before an authentication operation

During an authentication operation there are cases where you want to save the application state before the browser gets redirected to the Identity provider. This can be the case when you are using something like a state container and you want to restore the state after the authentication succeeds. In those scenarios, you can use a custom authentication state object to preserve your app specific state or a reference to it and restore that state once the authentication operation completes successfully:

@page "/authentication/{action}"
@inject JSRuntime JS
@inject StateContainer State
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorViewCore Action="@Action" AuthenticationState="AuthenticationState" OnLoginSucceded="RestoreState" OnLogoutSucceded="RestoreState" />

@code{

    public class ApplicationAuthenticationState : RemoteAuthenticationState
    {
        public string Id { get; set; }
    }

    protected async override Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();
            await JS.InvokeVoidAsync("sessionStorage.setKey", AuthenticationState.Id, State.Store());
        }
    }

    public async Task RestoreState(ApplicationAuthenticationState state)
    {
        var stored = await JS.InvokeAsync<string>("sessionStorage.getKey", state.Id);
        State.FromStore(stored);
    }

    public ApplicationAuthenticationState AuthenticationState { get; set; } = new ApplicationAuthenticationState();

    [Parameter] public string Action { get; set; }
}

Requesting additional access tokens

Most applications only require an access token to talk to the protected resources that they work with, but in some scenarios a given application might need more than one token to talk to two or more resources. In these scenarios, the IAccessTokenProvider.RequestToken method provides an overload that allows you to provision a token with a given set of scopes.

For example:

var tokenResult = await AuthenticationService.RequestAccessToken(new AccessTokenRequestOptions
{
    Scopes = new[] { "https://graph.microsoft.com/Mail.Send", "https://graph.microsoft.com/User.Read" }
});

Customizing application routes

By default, the Microsoft.AspNetCore.Components.WebAssembly.Authentication package uses these paths for representing different authentication states.

  • authentication/login is used for triggering a sign-in operation.
  • authentication/login-callback is used for handling the result of any sign-in operation.
  • authentication/login-failed is used for displaying error messages when the sign-in operation fails for some reason.
  • authentication/logout is used for triggering a sign-out operation.
  • authentication/logout-callback is used for handling the result of a sign-out operation.
  • authentication/logout-failed is used for displaying error messages when the sign-out operation fails for some reason.
  • authentication/logged-out is used to indicate that the user has successfully logout.
  • authentication/profile is used to trigger an operation to edit the user profile.
  • authentication/register is used to trigger an operation to register a new user.

All these paths are configurable in RemoteAuthenticationOptions<TProviderOptions>.AuthenticationPaths if you want to change the paths your application uses for any of the operations described above, you need to change the path in the options as well as making sure that you have a route that handles that path. For example, you can change all the paths to be prefixed by security instead as shown below:

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string Action { get; set; }
}
builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});

If you plan to have completely different paths, you can do so as described above and simply render the RemoteAuthenticatorView with an explicit action parameter. For example:

@page "/register"
<RemoteAuthenticatorView Action="@RemoteAuthenticationActions.Register" />

This also means you can break the UI into different pages if you choose to do so.

Customizing the authentication user interface

RemoteAuthenticatorView includes a default set of UI pieces for each authentication state. You can customize each state by passing in your own RenderFragment. For example, to customize the text that gets displayed during the initial login proccess you can change the RemoteAuthenticatorView as follows:

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter] public string Action { get; set; }
}

The remote authenticator view has one fragment that can be used per authentication route:

  • authentication/login -> LoggingIn.
  • authentication/login-callback -> CompletingLogIn.
  • authentication/login-failed -> LogInFailed.
  • authentication/logout -> LoggingOut.
  • authentication/logout-callback -> CompletingLogOut.
  • authentication/logout-failed -> LogOutFailed.
  • authentication/logged-out -> LogOutSucceeded.
  • authentication/profile -> UserProfile.
  • authentication/register -> Registering.
@SteveSandersonMS
Copy link

This looks really comprehensive, and excellent! Thoughts:

Tokens and in this case oAuth and Open ID Connect don't rely on the user agent behaving correctly to ensure that the application is secure.

Odd claim. We rely on the user agent not doing wildly incorrect things like allowing XSS by letting another tab invoke JS in your tab, or giving unencrypted MitM access to a 3rd party at the network level, or just keylogging the user and exfiltrating the data. 😈

RedirectToLogin.razor

Why does this have @using Microsoft.AspNetCore.Components.WebAssembly.Authentication? Seems unused.

await SignOutManager.SetSignOutState();

Didn't notice if this was explained anywheres.

Create two applications in Azure Active Directory following the steps in PLACEHOLDER

Why two applications? Can that be explained here? Not sure what the PLACEHOLDER will say.

dotnet new blazorwasm --no-restore -ho -au SingleOrg --client-id "<>" --tenant-id "<>" --app-id-uri "<>" --default-scope "<>" --api-client-id "<>"

Shouldn't have --no-restore. Also preferably use full name for params, because things like -ho on its own is really hard to understand.

Same in other dotnet new examples throughout the doc.

public async Task OnSaveAsync()

This flow looks quite hard work. I'm not saying we should change anything about it right now, but if people give feedback that it's too tricky to do this, we could consider doing something like automatically providing a FlowId (GUID) on the TokenResult so people don't have to mess with the redirect URI - they can just use that FlowId as a key into their state store before issuing the NavigateTo call.

Then when they come back later, they could read the FlowId value using some extension method on NavigationManager (e.g., Navigation.GetAuthenticationFlowId()) which parses it out of querystring or hash.

Or we could go further still and provide some actual storage for the state ourselves, so all the developer has to do is supply a JSON-serializable object. Not certain of the API design but there are many possibilities. Not suggesting we attempt this right now!

Saving application state before an authentication operation

Why go through the process of defining a ApplicationAuthenticationState type? Couldn't you more easily just have a field called AuthenticationStateGuid that gets rendered into the RemoteAuthenticatorViewCore's AuthenticationState parameter?

General

In lots of places, things are phrased as uses the {packagename} or from the {packagename}, etc, which will be confusing unless amended to uses the {packagename} package. The docs writers might not know what to correct this to, since it could be "class" or "namespace" or something else.

Should we document "how to add auth to an existing application"? I know this is very nontrivial, considering all the combinations.

@Andrzej-W
Copy link

Every time I write a web application I use {culture} segment in my urls. All urls are also translated, for example:

  • en-us/login
  • pl-pl/zaloguj

Will it be supported?

@andoband
Copy link

There are other possible options for authenticating single page applications, like using SameSite cookies, but as part of our design process we have settled on oAuth and specifically on Open ID Connect as the best option for authenticating Blazor webassembly applications.

Still it'd be nice to have some guidance on using SameSite cookies, which seems easier to get up and running and which integrates with how many ASP.Net sites already handle auth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment