Angular 17 и dot net Core 8 – добавление Jwt при обновлении в аудиторию

В настоящее время я создаю простое веб-приложение, которое создаст Jwt с токеном обновления (хранящимся в БД). Никаких проблем здесь нет, но когда генерируется токен обновления, он продолжает добавляться к аудитории внутри Jwt, и я не знаю, почему. Создание и обновление обрабатываются ядром dot net core 8 с использованием Angular 17 в качестве внешнего интерфейса.

Вот что я получаю после нескольких обновлений токена:

"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "[Removed]",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "[Removed]",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "[Removed]",
  "exp": 1718882692,
  "iss": "https://localhost:4200",
  "aud": [
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200",
    "https://localhost:4200"
  ]
}

Я использовал это руководство по адаптации моего кода для создания токена обновления. В коде есть незначительные изменения, поскольку пользовательские данные, хранящиеся в Jwt, представляют собой значение идентификатора пользователя Entra, а не значение идентификатора пользователя БД.

Я хочу убедиться, что эмитент и аудитория проверены, поэтому я обязательно включил их, и они определены в appsettings.json (я только что использовал URL-адрес адреса, на котором они работают, либо локальный хост, либо окончательный веб-домен).

Вот применимый код, показывающий добавление аудитории в Jwt, а также код обновления и связанные модели.

Программа.cs

builder.Services.AddTransient<ITokenService, TokenService>();

var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var jwtSettings = builder.Configuration.GetSection("JwtSettingsCommon");
var environment = builder.Configuration.GetSection("Production");
if (env == "Development")
{
    environment = builder.Configuration.GetSection("Development");
}
var JwtSecret = jwtSettings["SigningKey"];

if (JwtSecret != null)
{
    builder.Services.AddAuthentication(opt =>
    {
        opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = environment["URI"],
            ValidAudience = environment["URI"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret))
        };
    });
}

Пользовательдатаконтроллер.cs

[HttpPost("refresh")]
public async Task<IActionResult> Refresh(JwtToken tokenApiModel)
{
    if (tokenApiModel is null)
        return BadRequest(new { status = "error", message = "Invalid client request (JwtToken)" });
    string accessToken = tokenApiModel.Token;
    string refreshToken = tokenApiModel.RefreshToken;
    var principal = _tokenService.GetPrincipalFromExpiredToken(accessToken);
    var token = new JwtSecurityToken(accessToken);
    var userId = token.Claims.First(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
    var userQuery = from users in _dbContext.Set<UserData>()
                            join refreshTokens in _dbContext.Set<JwtRefreshTokens>()
                                on users.EntraUserID equals userId
                            select refreshTokens;
    var user = userQuery.FirstOrDefault();
    if (user is null || user.RefreshToken != refreshToken)
        return BadRequest(new { status = "error", message = "Invalid client request (checking Jwt refresh token in DB" });
    else if (user.RefreshTokenExpiryTime <= DateTime.Now)                
        return Ok(new { status = "error", message = "Refresh token expired" });
    var newAccessToken = _tokenService.GenerateAccessToken(principal.Claims);
    var newRefreshToken = _tokenService.GenerateRefreshToken();
    user.RefreshToken = newRefreshToken;
    user.RefreshTokenExpiryTime = DateTime.Now.AddDays(1);
    _dbContext.Update(user);
    await _dbContext.SaveChangesAsync();
    return Ok(new JwtToken()
    {
        Token = newAccessToken,
        RefreshToken = newRefreshToken
    }); 
}

auth.service.ts

async tryRefreshingTokens(token: string): Promise<boolean> {
  const refreshToken: string = localStorage.getItem("refreshToken");
  if (!token || !refreshToken) {
    console.info('No token or refreshToken')
    return false;
  }

  const credentials = JSON.stringify({ token: token, refreshToken: refreshToken });
  let isRefreshSuccess: boolean;
  const refreshRes = await new Promise<JwtToken>((resolve, reject) => {
    this.http.post<JwtToken>(`${environment.apiUrl}/userdata/refresh`, credentials, {
      headers: new HttpHeaders({
        "Content-Type": "application/json"
      })
    }).subscribe({
      next: (res: JwtToken) => resolve(res),
      error: (_) => { reject; isRefreshSuccess = false; }
    });
  });
  var validToken = this.jwtHelper.isTokenExpired();
  if (validToken) {      
    localStorage.setItem("jwt", refreshRes.token);
    localStorage.setItem("refreshToken", refreshRes.refreshToken);
    isRefreshSuccess = true;
  } else {
    isRefreshSuccess = false;
  }
  return isRefreshSuccess;
}

auth.guard.ts

export const canActivate: CanActivateFn = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
) => {
  const jwtHelper = inject(JwtHelperService);
  const authService =  inject(AuthService);
  const router = inject(Router)
  const token = localStorage.getItem("jwt");
  if (token && !jwtHelper.isTokenExpired(token)) {    
    return true;
  }
  const isRefreshSuccess = authService.tryRefreshingTokens(token);
  if (!isRefreshSuccess) {
    router.navigate(['/unauthorised']);
  }
  return isRefreshSuccess;
}

ТокенService.cs

public class TokenService(IConfiguration configuration) : ITokenService
{
    private readonly IConfiguration _configuration = configuration;
    public string GenerateAccessToken(IEnumerable<Claim> claims)
    {
        var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        var jwtSettings = _configuration.GetSection("JwtSettingsCommon");
        var environment = _configuration.GetSection("Production");
        if (env == "Development")
        {
            environment = _configuration.GetSection("Development");
        }
        var JwtSecret = jwtSettings["SigningKey"];
        var Issuer = environment["URI"];
        var Audience = environment["URI"];
        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
        var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

        int expiry;
        try
        {
            expiry = Int32.Parse(jwtSettings["Expires"]);
        }
        catch (FormatException)
        {
            expiry = 10;
        }
        var tokenOptions = new JwtSecurityToken(
            issuer: Issuer,
            audience: Audience,                
            claims: claims,
            expires: DateTime.Now.AddMinutes(expiry),
            signingCredentials: signinCredentials
        );
        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
        return tokenString;
    }
    public string GenerateRefreshToken()
    {
        var randomNumber = new byte[32];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }
    }

    public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
        var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        var jwtSettings = _configuration.GetSection("JwtSettingsCommon");
        var environment = _configuration.GetSection("Production");
        if (env == "Development")
        {
            environment = _configuration.GetSection("Development");
        }
        var JwtSecret = jwtSettings["SigningKey"];
        var Issuer = environment["URI"];
        var Audience = environment["URI"];
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret)),
            ValidateLifetime = false,
            ValidIssuer = Issuer,
            ValidAudience = Audience
        };
        var tokenHandler = new JwtSecurityTokenHandler();
        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
        var jwtSecurityToken = securityToken as JwtSecurityToken;
        if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");
        return principal;
    }
}

ITokenService.cs

public interface ITokenService
{
    string GenerateAccessToken(IEnumerable<Claim> claims);
    string GenerateRefreshToken();
    ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
}

JwtToken.cs

 public class JwtToken
 {
     public string Token { get; set; } = string.Empty;
     public string RefreshToken { get; set; } = string.Empty;
 }

Метод GenerateAccessToken в вашем TokenService.cs может создавать новые JWT на основе существующих утверждений, что приводит к накоплению.

Jason Pan 21.06.2024 10:40

Вам следует изменить этот метод, чтобы аудитория не добавлялась повторно. Ставьте лайк var newClaims = claims.Where(claim => claim.Type != JwtRegisteredClaimNames.Aud).ToList(); newClaims.Add(new Claim(JwtRegisteredClaimNames.Aud, Audience)); и используйте его в JwtSecurityToken(claims: newClaims,)

Jason Pan 21.06.2024 10:41

Абсолютно идеально, спасибо, при проверке теперь к претензиям больше аудиторий не добавляется. Я просто не мог увидеть или понять, почему добавлялись только «Аудитории», а не другие поля. Мне пришлось удалить аудиторию из new JwtSecurityToken(), тогда все заработало без дублирования.

Jason Williams 21.06.2024 13:05

Привет, Джейсон, не против, если я обобщу свои комментарии в виде ответа ниже, это поможет другим, кто столкнется с той же проблемой в будущем, спасибо.

Jason Pan 21.06.2024 16:10

Пожалуйста, не стесняйтесь — я добавлю свои комментарии и покажу обновленный код. Спасибо за вашу неоценимую помощь!

Jason Williams 21.06.2024 18:00

Привет Джейсон, спасибо за понимание.

Jason Pan 24.06.2024 17:29
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
149
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Измените метод GenerateAccessToken, как показано ниже, чтобы аудитория не добавлялась повторно.

public string GenerateAccessToken(IEnumerable<Claim> claims)
{
    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    var jwtSettings = _configuration.GetSection("JwtSettingsCommon");
    var environment = _configuration.GetSection("Production");
    if (env == "Development")
    {
        environment = _configuration.GetSection("Development");
    }
    var JwtSecret = jwtSettings["SigningKey"];
    var Issuer = environment["URI"];
    var Audience = environment["URI"];
    var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
    var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

    int expiry;
    try
    {
        expiry = Int32.Parse(jwtSettings["Expires"]);
    }
    catch (FormatException)
    {
        expiry = 10;
    }
   // Suggestion: Remove duplicate audiences if any
   var distinctAudiences = claims.Where(c => c.Type == JwtRegisteredClaimNames.Aud).Select(c => c.Value).Distinct().ToList();
   var filteredClaims = claims.Where(c => c.Type != JwtRegisteredClaimNames.Aud).ToList();
   filteredClaims.Add(new Claim(JwtRegisteredClaimNames.Aud, Audience));

    var tokenOptions = new JwtSecurityToken(
        issuer: Issuer,
        audience: filteredClaims,                
        claims: claims,
        expires: DateTime.Now.AddMinutes(expiry),
        signingCredentials: signinCredentials
    );
    var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
    return tokenString;
}

Другие вопросы по теме