skip to Main Content

By default, Blazor provides a demo app for Identity. What I want is I have an API project that returns jwt token. Now, I want to create a Blazor App using "InteractiveAuto", which means pages can return from the server for the first request and then use web assembly.

2

Answers


  1. Creating an authentication flow in ASP.NET Core 8.0 Blazor with an external API and JWT token involves several steps. Below is a simplified outline of the process. Keep in mind that this is a high-level overview, and you might need to adapt it based on your specific requirements.

    1. Create a Blazor WebAssembly App:

      dotnet new blazorwasm -n YourBlazorApp
      cd YourBlazorApp
      
    2. Install Required NuGet Packages:

      dotnet add package Microsoft.AspNetCore.Components.Web.Extensions
      dotnet add package Microsoft.AspNetCore.Components.Web.Extensions.Server
      dotnet add package Microsoft.AspNetCore.Components.Web.Extensions.Http
      
    3. Configure Authentication Services in Program.cs:

      builder.Services.AddAuthorizationCore();
      builder.Services.AddHttpClient();
      
    4. Configure Authentication in Startup.cs:

      services.AddAuthentication(options =>
      {
          options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
      }).AddJwtBearer(options =>
      {
          options.Authority = "YourExternalApiBaseUrl";
          options.Audience = "YourApiAudience";
      });
      
    5. Add Authentication to Blazor App in App.razor:

      <CascadingAuthenticationState>
          <Router AppAssembly="@typeof(Program).Assembly">
              <Found Context="routeData">
                  <!-- Your routes here -->
              </Found>
              <NotFound>
                  <!-- Not found page -->
              </NotFound>
          </Router>
      </CascadingAuthenticationState>
      
    6. Use Authentication in Blazor Components:

      @inject AuthenticationStateProvider AuthenticationStateProvider
      
      @code {
          private async Task LogIn()
          {
              var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
              var user = authState.User;
      
              // Access user information
          }
      
    7. Invoke External API and Handle JWT Tokens:

      @inject HttpClient HttpClient
      
      @code {
          private async Task CallExternalApi()
          {
              var result = await HttpClient.GetStringAsync("YourExternalApiEndpoint");
      
              // Process the result
          }
      
    8. Secure External API with JWT Authentication:
      Ensure your external API is configured to accept JWT tokens for authentication. The details depend on the API technology you’re using.

    Remember to replace placeholders like "YourExternalApiBaseUrl," "YourApiAudience," and others with your actual values. This is a simplified guide, and you might need to adjust it based on your specific requirements and external API details. Additionally, always consider security best practices when working with authentication and external APIs.

    Login or Signup to reply.
  2. This seemingly simple requirement is actually fairly complex. I have not yet found a definitive example posted of the "official" way to do it, but I’ll provide some details on an implementation that I have developed. I’m using OIDC with Azure Entra for the initial signon and then JWT to access the Api, but hopefully you’ll be able to adapt some of these ideas to your case.

    The primary issue is that we need to handle two authentication cases. The first load is using Blazor Server (SSR) and the second load uses WASM. If you remember back to the old .Net 7 templates, there were two very different flows to implement. The SSR route used IHtttpContextAccessor to access the underlying context. The WASM route used the MSAL library.

    I assume that you already have a method of signing in and a component that looks something like the following:

    ServerProject.Program.cs (Psuedo code only)

    builder.Services.AddAuthentication()
       .AddCookies()
       .AddOpenIdConnect()
       .AddJwtBearer()
    
    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
    builder.Services.AddHttpContextAccessor();
    builder.Services.AddCascadingAuthenticationState();
    

    ClientProject.Program.cs (Psuedo code only)

    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
    builder.Services.AddHttpContextAccessor();
    builder.Services.AddCascadingAuthenticationState();
    

    AuthorisedComponent.razor

    @page "/SigningIn"
    @rendermode InteractiveAuto
    
    <AuthorizeView>
        <Authorized>
        </Authorized>
        <NotAuthorized>
        </NotAuthorized>
    </AuthorizeView>
    

    I have implemented a Custom Authentication State provider to enable the different flows and it looks something like this:

    CustomAuthStateProvider.cs

    public class CustomAuthStateProvider :AuthenticationStateProvider
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly NavigationManager _navMan
    
        public CustomAuthStateProvider (IHttpConextAccessor httpContextAccessor, NavigationManager navMan)
        { 
            _httpContextAccessor = httpContextAccessor;
            _navMan = navMan;
        }
    
        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            HttpContext? context = _httpContextAccessor.HttpContext;
            ClaimsPrincipal? claimsPrincipal = null;
    
            if(context != null)  //ie we have a context, so it must be Blazor Server 
            {
                 //This is fairly straightforward as we can just retrieve the Claims Principal from the context (and modify it if you like)
                  claimsPrincipal = context.User;
    
                 //The biggest downside I have found with this flow is that I can't seem to access Local or Session browser storage
                 //I keep getting the Javascript Interop error that you mention.
                 //After a bit of research, I think that the issue is that this flow is being called during the OnInitializedAsync lifecycle method.
            }
            else   //ie Blazor WASM  (this will normally be called a few seconds after the Server flow is completed(
            {
                //This is a bit more complicated, and relies on a little trick to retrieve the HttpContext from the server using a call to a controller.
    
                HttpClient? client = _clientFactory.CreateClient();
                client.BaseAddress = new Uri(_navMan.BaseUri);
                string? uri = "api/AppAuth/GetClaimsPrincipal_WASM";
    
                HttpResponseMessage? response = await client.PostAsync(uri, null);
                string? responseString = await response.Content.ReadAsStringAsync();
    
                AppClaimsPrincipalDTO = JsonSerializer.Deserialize<AppClaimsPrincipalDTO>(responseString);
    
                claimsPrincipal = AppClaimsPrincipalDTO?.ToClaimsPrincipal() ?? new();
                //Note: I have found it is possible to access browser storage here without Javascript Interop error if you need to.
            }
    
            return new AuthenticationState(claimsPrincipal);
        }  
    

    It is possible to retrieve the httpcontext if a request is made to a controller in your backend.
    Credit to: Damien Boden – Backend for Front End Auth for this idea.

    AppAuthController.cs

    public class AppAuthController : Controller
    {
    
        [AllowAnonomous]
        [HttpPost("api/AppAuth/GetClaimsPrincipal_WASM")
        public async Task<ActionResult<ClaimsPrincipalDTO>> GetClaimsPrincipal_WASM()
        {
              if (!User?.Identity?.IsAuthenticated ?? true)
            {
                var ret = AppClaimsPrincipalDTO.Anonymous();
                return Ok(ret);
            }
            else
            {         
               AppClaimsPrincipalDTO ret = new ();
               //get claims from User
               {
                   {
                       ret.IsAuthenticated = User.Identity.IsAuthenticated;
                       var claims = User.Claims.ToList();
    
                       foreach (Claim? claim in claims)
                       {
                           var claimDto = new AppClaimDTO();
                           claimDto.Type = claim.Type;
                           claimDto.Value = claim.Value;
                           claimDto.ValueType = claim.ValueType;
                           claimDto.Issuer = claim.Issuer;
                           ret.AppClaims.Add(claimDto);
                       }
    
                       foreach (AppClaimDTO? claimDTO in customClaims)
                       {
                           ret.AppClaims.Add(claimDTO);
                       }
                   }
               }
    
               return Ok(ret);     
            }
        }
    
    } 
    
    
    public class AppClaimsPrincipalDTO
    {
        public static AppClaimsPrincipalDTO Anonymous() => new();
        public bool IsAuthenticated { get; set; } = false;
        public string NameClaimType { get; set; } = string.Empty;
        public string RoleClaimType { get; set; } = string.Empty;
    
    
    
        /// <summary>
        /// Custom AppClaims that are to be added to the ClaimsPrincipal in AuthenticationStateProvider and OnTokenValidation for Bearer_Server
        /// </summary>
        public IList<AppClaimDTO> AppClaims { get; set; } = new List<AppClaimDTO>();
    
        /// <summary>
        /// Take  a base Claims Principal and adds the AppClaims to it
        /// </summary>
        public ClaimsPrincipal ToClaimsPrincipal()
        {
            //create anonymous identity
            ClaimsIdentity newIdentity = new();
    
            if (AppClaims.Any())
            {
                newIdentity = new ClaimsIdentity(nameof(AppClaimsPrincipalDTO), NameClaimType, RoleClaimType);
    
                var claims = new List<Claim>();
    
                foreach (AppClaimDTO claimDTO in AppClaims)
                {
                    claims.Add(claimDTO.ToClaim());
                }
                newIdentity.AddClaims(claims);
            }
    
            var ret = new ClaimsPrincipal(newIdentity);
            return ret;
         }
    }
    
    public class AppClaimDTO
    {
        public string Type { get; set; } = string.Empty;
        public string Value { get; set; } = string.Empty;
        public string ValueType { get; set; } = string.Empty;
        public string Issuer { get; set; } = string.Empty;
    
        public Claim ToClaim()
        {
            return new Claim(Type, Value, ValueType, Issuer);
        }
    }
    

    There are quite a few different concepts here and I’ve distilled a fairly large chunk of code into the essential elements and removed error handling etc., but hopefully you can get the general idea.

    I know this use case is a bit different to yours, but hopefully you can work out where to inject your JWT tokens to call the API in this flow. You only provided limited detail, so I’ve had to make a lot of assumptions about your your implementation.

    I have got this "working", but there seems to be a fair bit of redundant computation since we need to do auth for both Server and WASM. I hope that an official template is issued soon so that we can see how the makers intended it to be used.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search