Добавление аутентификации на предъявителя JWT в клиент, созданный OpenAPI3 NSwag

#swagger #authorization #openapi #blazor-webassembly #nswag

#развязность #авторизация #openapi #blazor-веб-сборка #нсваг

Вопрос:

У меня есть приложение Blazor WebAssm, которое я хочу защитить с помощью базовых токенов на предъявителя JWT, это внутренне используемое приложение, поэтому фактическая аутентификация будет осуществляться с помощью проверки на соответствие внутреннему AD. Мой уровень WebAPI использует встроенный Swagger для создания документа OpenAPI 3, который затем использует мой клиент для создания класса C # для его использования.

Итак, у меня есть клиент OpenAPI, и я могу вызвать его из своего приложения blazor, но я не могу понять, как добавить безопасность ни на стороне API, ни на стороне Blazor.

Я использую .NET 5 как на уровне WebAPI, так и на клиенте Blazor.

Ответ №1:

Это заняло много времени, чтобы разобраться, и я уверен, что есть лучшие способы. Но вот шаги, которые я сделал, чтобы добавить это:

Во-первых, при запуске WebAPI вам необходимо правильно настроить Swagger:

 services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "My Cool Tool",
                Version = "v1",
                Description = "My Cool API",
            });

            //add jwt authentication definition to the OpenAPI doc.
            var securityScheme = new OpenApiSecurityScheme
            {
                Name = "JWT Authentication",
                Description = "Enter JWT Bearer Token Only",
                In = ParameterLocation.Header,
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                BearerFormat = "JWT",
                Reference = new OpenApiReference
                {
                    Id = JwtBearerDefaults.AuthenticationScheme,
                    Type = ReferenceType.SecurityScheme
                }
            };

            c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);

            //to indicate the entire API is secured, add this.  NOTE it does NOT secure it, just indicates it is.
            //c.AddSecurityRequirement(new OpenApiSecurityRequirement
            //{ 
            //    {securityScheme, new string[]{ } }
            //});

            //this filter does add if an API is secured based upon the Authorize attribute
            c.OperationFilter<SecurityRequirementsOperationFilter>();                
        });
 

Обратите внимание, что последние несколько строк являются ключевыми, в AddSecurityDefinition конце помещается фрагмент с подробным описанием схемы, которую вы используете. Если вы раскомментируете эти строки, то схема безопасности добавляется к каждой операции (маловероятно, что вы хотите!), Поэтому последняя строка просто добавляет ее к этим операциям / методам с [Authorize] тегом.

А это SecurityRequirementsOperationFilter заключается в следующем

 public class SecurityRequirementsOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        //if the controller method has the authorize attribute, get the roles
        var requiredRoles = context.MethodInfo
            .GetCustomAttributes(true)
            .OfType<AuthorizeAttribute>()
            .Select(attr => attr.Roles)
            .Distinct();

        if (requiredRoles.Any())
        {
            //add the fact that this operation could return the 401/403 HTTP status codes
            operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
            operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });

            //add the fact that this operation is secured by bearer auth
            var bearerScheme = new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Id = JwtBearerDefaults.AuthenticationScheme,
                    Type = ReferenceType.SecurityScheme
                }
            };

            operation.Security = new List<OpenApiSecurityRequirement>
            {
                new OpenApiSecurityRequirement
                {
                    { bearerScheme, requiredRoles.ToList() }
                }
            };
        }
    }
}
 

Вы можете пропустить часть ролей и просто заменить ToList на пустой массив строк.

Наконец, у нас есть еще несколько стандартных настроек авторизации, которые нужно выполнить при запуске:

         //get token management settings
        var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>();
        services.AddSingleton(token);

        //add authentication
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(options =>
        {
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = token.Issuer,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
                ValidAudience = token.Audience,
                ValidateAudience = false
            };
        });
        //add the authentication user service
        services.AddScoped<IUserService, UserService>();
 

TokenManagement это poco, в котором хранятся некоторые элементы конфигурации для токенов.
UserService это класс IsValidUser , в котором у вас есть, куда вы добавляете свой фактический чек.

Наконец, вам нужен какой-то метод входа в систему:

     [AllowAnonymous]
    [HttpPost("login")]
    [ProducesResponseType(typeof(LoginResult), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public ActionResult Login([FromBody] LoginRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest("Invalid Request");
        }

        if (!_userService.IsValidUser(request.UserName, request.Password))
        {
            return BadRequest("Invalid Request");
        }

        var claims = new[]
        {
            new Claim(ClaimTypes.Name, request.UserName),
            new Claim(ClaimTypes.Role, _userService.GetUserRole(request.UserName))
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenManagement.Secret));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var jwtToken = new JwtSecurityToken(
            _tokenManagement.Issuer,
            _tokenManagement.Audience,
            claims,
            expires: DateTime.Now.AddMinutes(_tokenManagement.AccessExpiration),
            signingCredentials: credentials);

        var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);

        _logger.LogInformation($"User [{request.UserName}] logged in the system.");

        return Ok(new LoginResult
        {
            UserName = request.UserName,
            JwtToken = token
        });
    }
 

На этом этапе вы можете украсить свои методы api [Authorize] атрибутом, и последующие вызовы должны быть заблокированы без заголовка Bearer в запросе.

На стороне клиента NSwag сгенерирует partial класс для клиента, который я расширил и предоставил тело одному из предоставленных PrepareRequest методов, поскольку это вызывается непосредственно перед HttpClient выполнением Send .

     partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
    {
        _logger.LogInformation($"Prepare Request: {request.RequestUri.AbsoluteUri}");
        var user = _storage.GetItem<LoginResult>("user");

        if (user != null)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", user.JwtToken);
            
            _logger.LogDebug($"Headers: {request.Headers.ToString()}");
        }
    }
 

Ключевым моментом здесь является то, что этот метод должен быть синхронным, поскольку сгенерированный клиент этого не await делает.
Поэтому в моем случае я использовал Blazored.LocalStorage пакет, чтобы получить эти методы синхронизации. Обратите внимание, что, поскольку мне был нужен LocalStorageService , мне также пришлось добавить свой собственный конструктор для клиента в файл частичного класса, который вызывал тот, который генерирует NSwag.

Теперь, когда это сделано, вы можете добавить страницу входа в систему для вызова этого более раннего метода входа, сохранить токен, и теперь, когда вы вызываете API, будет добавлен ваш токен-носитель.