admin管理员组

文章数量:1410717

I have a simple Blazor Server application that is sitting behind an F5 gateway, which allows only authenticated requests to reach the Blazor App. The gateway passes the user information as custom HTTP request headers. I would like to integrate the authenticated user information with the authorization features available for the Blazor Server infrastructure, and I devised a quite simple approach, which seems to be working.

In the Program.cs startup code I register an inline middleware that initializes a collection of claims read from the request’s custom headers and the use the claims to implement a simple AuthenticationStateProvider-derived class:

  // Set of claims that are initialized in the inline middleware below from request headers and
  // used to create the user's identity through the CustomAuthenticationStateProvider
  List<Claim> claimsFromRequestHeaders = new();

  // Add the CustomAuthenticationStateProvider to the service collection with a delegate that closes
  // over the claimsFromRequestHeaders initialized by the inline middleware below.
  builder.Services.AddScoped<AuthenticationStateProvider>(implementationFactory: (serviceProvider) => 
  {
    return new CustomAuthenticationStateProvider(claimsFromRequestHeaders);
  });

  var app = builder.Build();

  // Inline middleware that initializes the claimsFromRequestHeaders collection from the first request.
  app.Use(async (context, next) =>
  {
    // Do not initialize more than once.
    if (claimsFromRequestHeaders.Count > 0)
    {
      await next();
      return;
    }

    // This would be initialized from request headers...
    if (context.Request.Headers.TryGetValue("X-Claim-UserId", out var userId))
    {
      claimsFromRequestHeaders.Add(new Claim(ClaimTypes.NameIdentifier, userId!));
    }
    if (context.Request.Headers.TryGetValue("X-Claim-UserName", out var userName))
    {
      claimsFromRequestHeaders.Add(new Claim(ClaimTypes.Name, userName!));
    }
    await next();
  });

The CustomAuthenticationStateProvider is very simple:

/// <summary>
/// Creates an <see cref="AuthenticationState"/> with claims provider in constructor.
/// </summary>
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
  private readonly IReadOnlyCollection<Claim> _claims;

  public CustomAuthenticationStateProvider(IReadOnlyCollection<Claim> claims)
  {
    this._claims = claims;
  }


  public override Task<AuthenticationState> GetAuthenticationStateAsync()
  {
    var identity = new ClaimsIdentity(this._claims, "Custom Authentication");
    var user = new ClaimsPrincipal(identity);
    return Task.FromResult(new AuthenticationState(user));
  }
}

Using this setup, I can now use the AuthorizeAttribute to guard access on a page-by-page basis and also use the CascadingAuthenticationState to access the user information.

Here is a sample implementation that just hardcodes the user claims instead of reading them from request headers: .

Please note that the application uses InteractiveServer rendering and that there are no AddAuthentication/AddAuthorization and UseAuthentication/UseAuthorization calls in the startup code. Yet, the application works as expected.

I am wondering whether the approach is correct, or I am missing something?

I am very thankful for your feedback!

I have a simple Blazor Server application that is sitting behind an F5 gateway, which allows only authenticated requests to reach the Blazor App. The gateway passes the user information as custom HTTP request headers. I would like to integrate the authenticated user information with the authorization features available for the Blazor Server infrastructure, and I devised a quite simple approach, which seems to be working.

In the Program.cs startup code I register an inline middleware that initializes a collection of claims read from the request’s custom headers and the use the claims to implement a simple AuthenticationStateProvider-derived class:

  // Set of claims that are initialized in the inline middleware below from request headers and
  // used to create the user's identity through the CustomAuthenticationStateProvider
  List<Claim> claimsFromRequestHeaders = new();

  // Add the CustomAuthenticationStateProvider to the service collection with a delegate that closes
  // over the claimsFromRequestHeaders initialized by the inline middleware below.
  builder.Services.AddScoped<AuthenticationStateProvider>(implementationFactory: (serviceProvider) => 
  {
    return new CustomAuthenticationStateProvider(claimsFromRequestHeaders);
  });

  var app = builder.Build();

  // Inline middleware that initializes the claimsFromRequestHeaders collection from the first request.
  app.Use(async (context, next) =>
  {
    // Do not initialize more than once.
    if (claimsFromRequestHeaders.Count > 0)
    {
      await next();
      return;
    }

    // This would be initialized from request headers...
    if (context.Request.Headers.TryGetValue("X-Claim-UserId", out var userId))
    {
      claimsFromRequestHeaders.Add(new Claim(ClaimTypes.NameIdentifier, userId!));
    }
    if (context.Request.Headers.TryGetValue("X-Claim-UserName", out var userName))
    {
      claimsFromRequestHeaders.Add(new Claim(ClaimTypes.Name, userName!));
    }
    await next();
  });

The CustomAuthenticationStateProvider is very simple:

/// <summary>
/// Creates an <see cref="AuthenticationState"/> with claims provider in constructor.
/// </summary>
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
  private readonly IReadOnlyCollection<Claim> _claims;

  public CustomAuthenticationStateProvider(IReadOnlyCollection<Claim> claims)
  {
    this._claims = claims;
  }


  public override Task<AuthenticationState> GetAuthenticationStateAsync()
  {
    var identity = new ClaimsIdentity(this._claims, "Custom Authentication");
    var user = new ClaimsPrincipal(identity);
    return Task.FromResult(new AuthenticationState(user));
  }
}

Using this setup, I can now use the AuthorizeAttribute to guard access on a page-by-page basis and also use the CascadingAuthenticationState to access the user information.

Here is a sample implementation that just hardcodes the user claims instead of reading them from request headers: https://github/PaloMraz/BlazorWebAppWithSimpleAuthentication.

Please note that the application uses InteractiveServer rendering and that there are no AddAuthentication/AddAuthorization and UseAuthentication/UseAuthorization calls in the startup code. Yet, the application works as expected.

I am wondering whether the approach is correct, or I am missing something?

I am very thankful for your feedback!

Share Improve this question edited Mar 12 at 8:52 Ruikai Feng 12.3k1 gold badge7 silver badges16 bronze badges asked Mar 11 at 10:37 Palo MrazPalo Mraz 9297 silver badges18 bronze badges 4
  • Basically the F5 Gateway is your authentication provider. All you're doing is taken the provided authentication data and plugging it into Blazor's Authorization system. I'm no security expert, but it follows the pattern of many other authentication mechanisms. – MrC aka Shaun Curtis Commented Mar 11 at 11:49
  • Are you using HTTP or HTTPS. HTTP is not encrypted so anything you send a hacker can access. – jdweng Commented Mar 11 at 12:23
  • @jdweng, as far as I know, the F5 Gateway handles TLS termination so this is a non-isssue, IMHO. – Palo Mraz Commented Mar 11 at 18:26
  • The reason I asked is you code is sending private info (usename, user id) and didn't want unencrypted data transmitted. You are right if you are using HTTPS this is a non issue. If gateway is doing the TLS than data is being sent unencrypted from your client to the gateway. Gateways only encrypt if they are using Port Forwarding to another Corporate Gateway. Unencrypted is only recommended when you are behind a firewall. – jdweng Commented Mar 11 at 18:39
Add a comment  | 

2 Answers 2

Reset to default 0

I don't think it's a good behavior

List<Claim> claimsFromRequestHeaders = new();

.......

app.Use(async (context, next) =>
  {
    // Do not initialize more than once.
    if (claimsFromRequestHeaders.Count > 0)
    {
      await next();
      return;
    }

    // This would be initialized from request headers...
    if (context.Request.Headers.TryGetValue("X-Claim-UserId", out var userId))
    {
      claimsFromRequestHeaders.Add(new Claim(ClaimTypes.NameIdentifier, userId!));
    }
    if (context.Request.Headers.TryGetValue("X-Claim-UserName", out var userName))
    {
      claimsFromRequestHeaders.Add(new Claim(ClaimTypes.Name, userName!));
    }
    await next();

  });

If there're many people visit your site, claimsFromRequestHeaders would contain many different username and userid and would never be disposed

I think you should configure the authentication middleware to read username& userid (from cookie , jwt token or something else ,it depends on you)when you register authentication service

@Ruikai Feng, you are absolutely right! I cannot believe I have used such a naive, faulty approach :-(. I have added a simple Microsoft.Playwright test to verify the erroneous behavior. After that, I have refactored the CustomAuthenticationStateProvider to Implement the IAuthenticationStateProvider directly and use HttpContext to obtain the custom request header:

public class CustomAuthenticationHandler : IAuthenticationHandler 
{
  public const string SchemeName = "CustomAuthenticationScheme"; 
  private const string UserNameHeaderName = "X-Claim-UserName";
  
  private HttpContext? _httpContext;
  
  public CustomAuthenticationHandler()
  {
  }
  
  public Task<AuthenticateResult> AuthenticateAsync()
  {
    if (this._httpContext is null)
    {
      return Task.FromResult(AuthenticateResult.Fail("No HttpContext"));
    }
    if (!this._httpContext.Request.Headers.TryGetValue(UserNameHeaderName, out var userName) || (userName.Count == 0))
    {
      return Task.FromResult(AuthenticateResult.Fail("No user name found in the request headers."));
    }
    return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(CreateClaimsPrincipal(userName.ToString()), SchemeName)));
  }
  // Code omitted for clarity
  public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
  {
    this._httpContext = context;
    return Task.CompletedTask;
  }
  
  private ClaimsPrincipal CreateClaimsPrincipal(string userName = "DEFAULT")
  {
    var claims = new[] { new Claim(ClaimTypes.Name, userName) };
    var identity = new ClaimsIdentity(claims, SchemeName);
    return new ClaimsPrincipal(identity);
  }
}

Register the provider with a custom scheme in the DI container:

// Add our custom authentication scheme and handler for request headers-based authentication..
  builder.Services.AddAuthentication(options =>
  {
    options.AddScheme<CustomAuthenticationHandler>(
      name: CustomAuthenticationHandler.SchemeName, 
      displayName: CustomAuthenticationHandler.SchemeName);
  });

I hope that this is finally the correct way to do the authentication based on trusted request headers. I would really like to hear your opinion.

本文标签: cBlazor Server application with custom authentication based on request headersStack Overflow