admin管理员组

文章数量:1390399

I'm working on JWT authentication/authorization project, following this guide.

I have successfully wired up the JWT infrastructure and I'm able to login successfully and generate the required tokens.

Additionally, I'm using AddIdentityCore, so I have wired up the required service from the source code.

services.AddIdentityCore<User>()
    .AddRoles<UserRole>()
    .AddEntityFrameworkStores<NesnasWorkerEfContext>()
    //.AddClaimsPrincipalFactory<UsersClaimsPrincipalFactory>()
    .AddDefaultTokenProviders();

services.TryAddScoped<IUserValidator<User>, UserValidator<User>>();
services.TryAddScoped<IPasswordValidator<User>, PasswordValidator<User>>();
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<UserRole>, RoleValidator<UserRole>>();
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<User>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<User>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<User>, UserClaimsPrincipalFactory<User, UserRole>>();
services.TryAddScoped<IUserConfirmation<User>, DefaultUserConfirmation<User>>();
services.TryAddScoped<UserManager<User>>();
services.TryAddScoped<SignInManager<User>>();
services.TryAddScoped<RoleManager<UserRole>>();

The first issue comes into place when I move forward with the actions, for example Logout endpoint doesn't recognize that the user is authenticated and the ClaimsPrincipal is unauthenticated and shows IsAuthenticated = false

public static async Task<Results<Ok, UnauthorizedHttpResult>> Logout(
    SignInManager<User> signInManager,
    IJwtProvider jwtProvider,
    ClaimsPrincipal principal)
{
    if (!principal.Identity!.IsAuthenticated)
    {
        return TypedResults.Unauthorized();
    }

    await signInManager.SignOutAsync();

    await jwtProvider.RevokeToken(principal);

    return TypedResults.Ok();
}

However, I was able to workaround this issue by using

options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme

instead of

options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme

in the services.AddAuthentication section:

services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
        //options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; <-- This doesn't work
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = tokenValidationParameters;
    })
    .AddCookie(IdentityConstants.ApplicationScheme, o =>
    {
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
        };
    })
    .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
        };
    })
    .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    });

Now, after doing the above change, I noticed is that there are 2 identities when I inspect the ClaimsPrincipal, one that is being authenticated and the other is not. However, from my understanding, there should be a single identity that is pointing up to the authenticated user.

Furthermore, by investigating the principal, I found that the one that is authenticated have the default Claims, such as issuer LOCAL AUTHORITY. Although, mine is being setup in TokenValidationParameters is different and I've setup custom claims while generating the token, which is not showing up.

public async Task<Result<AccessTokenResponse>> GenerateToken(ClaimsPrincipal principal)
{
    var claims = new List<Claim>();

    var userId = principal.GetUserId();

    var user = await userRepository.GetByIdAsync(userId);

    claims.Add(new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
    claims.Add(new(JwtRegisteredClaimNames.Sub, userId.ToString()));
    claims.Add(new(JwtRegisteredClaimNames.Email, principal.GetUserEmail()));
    claims.Add(new(ApplicationClaimTypes.Id, userId.ToString()));
    claims.Add(new(ApplicationClaimTypes.TenantId, user!.TenantId.ToString()!));

    var handler = new JwtSecurityTokenHandler();

    var token = handler.CreateJwtSecurityToken(new SecurityTokenDescriptor()
    {
        Subject = new ClaimsIdentity(claims),
        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOptions.Secret)),
            SecurityAlgorithms.HmacSha256Signature),
        Issuer = _tokenOptions.ValidIssuer,
        Audience = _tokenOptions.ValidAudience,
        Expires = DateTime.UtcNow.AddSeconds(_tokenOptions.AccessTokenLifeTime),
    });

    var refreshToken = new UserRefreshToken(
        token.Id,
        DateTime.UtcNow,
        DateTime.UtcNow.AddDays(_tokenOptions.RefreshTokenLifeTime),
        userId);

    await refreshTokenRepository.CreateRefreshTokenAsync(refreshToken);
    await context.SaveChangesAsync();

    return Result.Success(new AccessTokenResponse
    {
        AccessToken = handler.WriteToken(token),
        RefreshToken = refreshToken.Token.ToString(),
        ExpiresIn = _tokenOptions.AccessTokenLifeTime * 60,
    });
}
var tokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidIssuer = configuration["Jwt:ValidIssuer"],
    ValidateAudience = true,
    ValidAudience = configuration["Jwt:ValidAudience"],
    ValidateLifetime = true,
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]!)),
    ValidateIssuerSigningKey = true,
    ClockSkew = TimeSpan.Zero
};

Lastly, I've tried to create my own UserClaimsPrincipalFactory and pass in the custom claims. However, this created 3 identities, the default one IsAuthenticated = true, another identity with the custom claims passed in the factory as IsAuthenticated = false and the last one with IsAuthenticated = false.

public class UsersClaimsPrincipalFactory(
    UserManager<User> userManager,
    RoleManager<UserRole> roleManager,
    IOptions<IdentityOptions> optionsAccessor)
    : UserClaimsPrincipalFactory<User, UserRole>(userManager, roleManager, optionsAccessor)
{
    public override async Task<ClaimsPrincipal> CreateAsync(User user)
    {
        var principal = await base.CreateAsync(user);
        var claims = new List<Claim>();
        var userId = principal.GetUserId();

        claims.Add(new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
        claims.Add(new(JwtRegisteredClaimNames.Sub, userId.ToString()));
        claims.Add(new(JwtRegisteredClaimNames.Email, principal.GetUserEmail()));
        claims.Add(new(ApplicationClaimTypes.Id, userId.ToString()));
        claims.Add(new(ApplicationClaimTypes.TenantId, user!.TenantId.ToString()!));

        principal.AddIdentity(new ClaimsIdentity(claims));

        return principal;
    }
    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user)
    {
        ClaimsIdentity claims = await base.GenerateClaimsAsync(user);
        return claims;
    }
}

Here is my login endpoint:

public static async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult>> Login(
    HttpContext context,
    LoginRequest request,
    SignInManager<User> signInManager,
    UserManager<User> userManager,
    IJwtProvider jwtProvider)
{
    var user = await userManager.FindByEmailAsync(request.Email);

    if (user is null)
    {
        return TypedResults.Unauthorized();
    }

    var result = await signInManager.PasswordSignInAsync(
        user.UserName!,
        request.Password,
        isPersistent: false,
        lockoutOnFailure: false);

    if (!result.Succeeded)
    {
        return TypedResults.Unauthorized();
    }

    var tokenResult = await jwtProvider.GenerateToken(context.User);

    if (tokenResult.IsFailure)
    {
        return TypedResults.Unauthorized();
    }

    return TypedResults.Ok(tokenResult.Value);
}

I'd appreciate your support in resolving the issue of having multiple identities and IsAuthenticated = false.

I'm working on JWT authentication/authorization project, following this guide.

I have successfully wired up the JWT infrastructure and I'm able to login successfully and generate the required tokens.

Additionally, I'm using AddIdentityCore, so I have wired up the required service from the source code.

services.AddIdentityCore<User>()
    .AddRoles<UserRole>()
    .AddEntityFrameworkStores<NesnasWorkerEfContext>()
    //.AddClaimsPrincipalFactory<UsersClaimsPrincipalFactory>()
    .AddDefaultTokenProviders();

services.TryAddScoped<IUserValidator<User>, UserValidator<User>>();
services.TryAddScoped<IPasswordValidator<User>, PasswordValidator<User>>();
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<UserRole>, RoleValidator<UserRole>>();
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<User>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<User>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<User>, UserClaimsPrincipalFactory<User, UserRole>>();
services.TryAddScoped<IUserConfirmation<User>, DefaultUserConfirmation<User>>();
services.TryAddScoped<UserManager<User>>();
services.TryAddScoped<SignInManager<User>>();
services.TryAddScoped<RoleManager<UserRole>>();

The first issue comes into place when I move forward with the actions, for example Logout endpoint doesn't recognize that the user is authenticated and the ClaimsPrincipal is unauthenticated and shows IsAuthenticated = false

public static async Task<Results<Ok, UnauthorizedHttpResult>> Logout(
    SignInManager<User> signInManager,
    IJwtProvider jwtProvider,
    ClaimsPrincipal principal)
{
    if (!principal.Identity!.IsAuthenticated)
    {
        return TypedResults.Unauthorized();
    }

    await signInManager.SignOutAsync();

    await jwtProvider.RevokeToken(principal);

    return TypedResults.Ok();
}

However, I was able to workaround this issue by using

options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme

instead of

options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme

in the services.AddAuthentication section:

services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
        //options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; <-- This doesn't work
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = tokenValidationParameters;
    })
    .AddCookie(IdentityConstants.ApplicationScheme, o =>
    {
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
        };
    })
    .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
        };
    })
    .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    });

Now, after doing the above change, I noticed is that there are 2 identities when I inspect the ClaimsPrincipal, one that is being authenticated and the other is not. However, from my understanding, there should be a single identity that is pointing up to the authenticated user.

Furthermore, by investigating the principal, I found that the one that is authenticated have the default Claims, such as issuer LOCAL AUTHORITY. Although, mine is being setup in TokenValidationParameters is different and I've setup custom claims while generating the token, which is not showing up.

public async Task<Result<AccessTokenResponse>> GenerateToken(ClaimsPrincipal principal)
{
    var claims = new List<Claim>();

    var userId = principal.GetUserId();

    var user = await userRepository.GetByIdAsync(userId);

    claims.Add(new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
    claims.Add(new(JwtRegisteredClaimNames.Sub, userId.ToString()));
    claims.Add(new(JwtRegisteredClaimNames.Email, principal.GetUserEmail()));
    claims.Add(new(ApplicationClaimTypes.Id, userId.ToString()));
    claims.Add(new(ApplicationClaimTypes.TenantId, user!.TenantId.ToString()!));

    var handler = new JwtSecurityTokenHandler();

    var token = handler.CreateJwtSecurityToken(new SecurityTokenDescriptor()
    {
        Subject = new ClaimsIdentity(claims),
        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenOptions.Secret)),
            SecurityAlgorithms.HmacSha256Signature),
        Issuer = _tokenOptions.ValidIssuer,
        Audience = _tokenOptions.ValidAudience,
        Expires = DateTime.UtcNow.AddSeconds(_tokenOptions.AccessTokenLifeTime),
    });

    var refreshToken = new UserRefreshToken(
        token.Id,
        DateTime.UtcNow,
        DateTime.UtcNow.AddDays(_tokenOptions.RefreshTokenLifeTime),
        userId);

    await refreshTokenRepository.CreateRefreshTokenAsync(refreshToken);
    await context.SaveChangesAsync();

    return Result.Success(new AccessTokenResponse
    {
        AccessToken = handler.WriteToken(token),
        RefreshToken = refreshToken.Token.ToString(),
        ExpiresIn = _tokenOptions.AccessTokenLifeTime * 60,
    });
}
var tokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidIssuer = configuration["Jwt:ValidIssuer"],
    ValidateAudience = true,
    ValidAudience = configuration["Jwt:ValidAudience"],
    ValidateLifetime = true,
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]!)),
    ValidateIssuerSigningKey = true,
    ClockSkew = TimeSpan.Zero
};

Lastly, I've tried to create my own UserClaimsPrincipalFactory and pass in the custom claims. However, this created 3 identities, the default one IsAuthenticated = true, another identity with the custom claims passed in the factory as IsAuthenticated = false and the last one with IsAuthenticated = false.

public class UsersClaimsPrincipalFactory(
    UserManager<User> userManager,
    RoleManager<UserRole> roleManager,
    IOptions<IdentityOptions> optionsAccessor)
    : UserClaimsPrincipalFactory<User, UserRole>(userManager, roleManager, optionsAccessor)
{
    public override async Task<ClaimsPrincipal> CreateAsync(User user)
    {
        var principal = await base.CreateAsync(user);
        var claims = new List<Claim>();
        var userId = principal.GetUserId();

        claims.Add(new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
        claims.Add(new(JwtRegisteredClaimNames.Sub, userId.ToString()));
        claims.Add(new(JwtRegisteredClaimNames.Email, principal.GetUserEmail()));
        claims.Add(new(ApplicationClaimTypes.Id, userId.ToString()));
        claims.Add(new(ApplicationClaimTypes.TenantId, user!.TenantId.ToString()!));

        principal.AddIdentity(new ClaimsIdentity(claims));

        return principal;
    }
    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user)
    {
        ClaimsIdentity claims = await base.GenerateClaimsAsync(user);
        return claims;
    }
}

Here is my login endpoint:

public static async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult>> Login(
    HttpContext context,
    LoginRequest request,
    SignInManager<User> signInManager,
    UserManager<User> userManager,
    IJwtProvider jwtProvider)
{
    var user = await userManager.FindByEmailAsync(request.Email);

    if (user is null)
    {
        return TypedResults.Unauthorized();
    }

    var result = await signInManager.PasswordSignInAsync(
        user.UserName!,
        request.Password,
        isPersistent: false,
        lockoutOnFailure: false);

    if (!result.Succeeded)
    {
        return TypedResults.Unauthorized();
    }

    var tokenResult = await jwtProvider.GenerateToken(context.User);

    if (tokenResult.IsFailure)
    {
        return TypedResults.Unauthorized();
    }

    return TypedResults.Ok(tokenResult.Value);
}

I'd appreciate your support in resolving the issue of having multiple identities and IsAuthenticated = false.

Share Improve this question edited Mar 14 at 7:14 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Mar 14 at 6:18 Nasser XNasser X 3054 silver badges11 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

For 8 or higher versions, you could protect your webapi project with Identity framework follow the official document

Register Identity Api service and Authorization service:

builder.Services.AddIdentityApiEndpoints<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddAuthorization();

Map Identity Endpoints route;

app.MapIdentityApi<IdentityUser>();

本文标签: cMultiple identities in ClaimsPrincipal with IsAuthenticatedfalse in JWTStack Overflow