0

I have two ASP.NET Core applications:

  1. Blazor Server app with Identity authentication (working correctly)
  2. Web API that should share authentication cookies with the Blazor app

The API is not authenticating users - User.FindFirstValue(ClaimTypes.NameIdentifier) always returns null, even when the user is authenticated in the Blazor app.

Blazor Server Program.cs:

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies();

builder.Services.AddIdentityCore<User>(options => options.SignIn.RequireConfirmedAccount = false)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<UserdbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

var app = builder.Build();
app.MapAdditionalIdentityEndpoints();

Web API Program.cs:

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies();

builder.Services.AddIdentityCore<BlazorProject.Data.User>(options =>
{
    options.SignIn.RequireConfirmedAccount = false;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<UserdbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();

var app = builder.Build();

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

API controller (where authentication fails):

[HttpPost]
public async Task<IActionResult> AddUserDeliveryMethod(int methodId)
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); // Always null
    // ...
}

What I've tried:

  • Both apps use the same database and Identity configuration
  • Authentication works perfectly in Blazor app
  • Same cookie schemes configured in both apps

Question

Why isn't the Web API recognizing the authentication cookies from the Blazor Server app, and how can I make them share authentication state properly?

I want to maintain cookie-based authentication and avoid implementing JWT tokens as a solution.

2
  • How are your applications sharing cookies, exactly? Unless both sites are running on the same Origin then you won't be able to do this. Commented Aug 26 at 19:26
  • But both applications run on the same domain? And the browser has the same cookies. Commented Aug 26 at 21:36

1 Answer 1

0

I imagine that your scenario involves a Blazor Server application and a WebAPI that share business logic and functionality, but the Blazor app handles the UI while the WebAPI is dedicated to other external clients.

In my experience, the best way to authenticate the APIs is using a bearer token.

I suggest you modify the Program.cs file like this:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = configuration["Jwt:Issuer"],
            ValidAudience = configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)),
        };
    });

And add an authentication endpoint:

[HttpPost("login")]
public async Task<IActionResult> Login(string email, string password)
{
    var result = await _identityService.LoginAsync(email, password);
    return result.IsSuccess ? Ok(result.Value) : Unauthorized(result.Errors);
}

To perform the login, add a service with the following logic:

public async Task<LoginResponseDto> LoginAsync(string email, string password, CancellationToken cancellationToken = default!)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
        return LoginResponseDto.Failure("Invalid credentials");

    var result = await _signInManager.CheckPasswordSignInAsync(user, password, false);
    if (!result.Succeeded)
        return LoginResponseDto.Failure("Invalid credentials");

    var accessToken = _jwtTokenGenerator.GenerateAccessToken(user);
    var refreshToken = _jwtTokenGenerator.GenerateRefreshToken();
    return LoginResponseDto.Success(user.Id, accessToken, refreshToken);
}

And a token generator service with a function like this:

public string GenerateAccessToken(ApplicationUser user)
{
    var jwtSettings = _configuration.GetSection("Jwt");

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]!));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var claims = new List<Claim>
    {
        new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new(JwtRegisteredClaimNames.Email, user.Email!),
        new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };

    // Optional: Add roles
    // claims.Add(new Claim(ClaimTypes.Role, "Admin")); 

    var token = new JwtSecurityToken(
        issuer: jwtSettings["Issuer"],
        audience: jwtSettings["Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(int.Parse(jwtSettings["Duration"]!)),
        signingCredentials: creds);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

If you want more security, you can set a short duration for the access token and use a refresh token to renew it on the client side using the following endpoint:

[HttpPost("refresh")]
public async Task<IActionResult> Refresh(string accessToken, string refreshToken)
{
    var response = await _identityService.RefreshTokenAsync(accessToken, refreshToken);
    // Handle response...
}

public async Task<RefreshTokenResponseDto> RefreshTokenAsync(string accessToken, string refreshToken)
{
    var principal = _jwtTokenValidator.ValidateToken(accessToken, validateLifetime: false);
    if (principal == null)
    {
        return RefreshTokenResponseDto.Failure("Invalid access token");
    }
    
    var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    if (userId == null)
    {
        return RefreshTokenResponseDto.Failure("Invalid access token");
    }
    
    var user = await _userManager.FindByIdAsync(userId);
    if (user == null)
    {
        return RefreshTokenResponseDto.Failure("Invalid access token");
    }
    
    var newAccessToken = _jwtTokenGenerator.GenerateAccessToken(user);
    var newRefreshToken = _jwtTokenGenerator.GenerateRefreshToken();

    return RefreshTokenResponseDto.Success(Guid.Parse(userId), newAccessToken, newRefreshToken);
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.