#c# #jwt
Вопрос:
Я создал микросервис с использованием .Net 5, в котором есть некоторые конечные точки, которые можно вызвать только с помощью jwtBearertoken.
Методы ConfigureServices
и Configure
в StartUp
классе выглядят следующим образом:
public void ConfigureServices(IServiceCollection services)
{
ConfigureDatabaseServices(services);
ConfigureMyProjectClasses(services);
services.AddVersioning();
services.AddControllers();
services.AddAuthentication(_configuration);
// Add framework services.
var mvcBuilder = services
.AddMvc()
.AddControllersAsServices();
ConfigureJsonSerializer(mvcBuilder);
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment webEnv,
ILoggerFactory loggerFactory,
IHostApplicationLifetime applicationLifetime)
{
_logger = loggerFactory.CreateLogger("Startup");
try
{
app.Use(async (context, next) =>
{
var correlationId = Guid.NewGuid();
System.Diagnostics.Trace.CorrelationManager.ActivityId = correlationId;
context.Response.Headers.Add("X-Correlation-ID", correlationId.ToString());
await next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
applicationLifetime.ApplicationStopped.Register(() =>
{
LogManager.Shutdown();
});
}
catch (Exception e)
{
_logger.LogError(e.Message);
throw;
}
}
Расширения аутентификации:
public static class AuthenticationExtensions
{
public static void AddAuthentication(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Authority = configuration["Authorization:Authority"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
}
Я использую сервер авторизации для микросервиса для проверки токена.
После добавления [Authorize]
атрибута над контроллерами возвращается почтальон 401 Unauthorized
, и интеграционные тесты, которые я создал перед добавлением аутентификации, также возвращаются Unauthorized
, как и ожидалось. Теперь я пытаюсь понять, как я могу изменить свои интеграционные тесты, добавив JwtBearerToken и издеваясь над ответом сервера авторизации, чтобы мои тесты прошли снова. Как я могу этого достичь?
Комментарии:
1. Вы не должны издеваться над
Authorize
атрибутом, даже если бы это было возможно. (Я не уверен, возможно это или нет) цель интеграционных тестов-проверить фактический запрос/ответ. Я думаю, вам следует сгенерировать токен для ваших тестов.2. @AliReza Я не собираюсь издеваться над
Authorize
атрибутом, я пытаюсь издеваться над добавлением jwtbearertoken и издеваться над ответом сервера авторизации3. Я знаю, что имел в виду, когда у вас есть авторизация в вашем конвейере, поведение вашего приложения может быть другим. лучше включить токен в свои запросы. но, в конце концов, если вам действительно нужно поиздеваться над авторизацией. вы должны начать издеваться над всем ДИ
Ответ №1:
Мой ответ не интегрирован на 100%, потому что мы добавим дополнительную схему аутентификации. TL;DR: Вы не проверяете, работает ли ваша аутентификация, но работаете с ней.
Было бы лучше использовать НАСТОЯЩИЙ токен, но, возможно, это решение-хорошая золотая середина.
Вы можете создать другую схему авторизации, например DevBearer
, где вы можете указать учетную запись, например, если вы отправите заголовок авторизации DevBearer Customer-John
, приложение распознает вас как клиента Джона.
Я использую этот подход во время разработки, потому что очень просто быстро тестировать разных пользователей. Мой код выглядит примерно так:
Startup.Auth.cs
private void ConfigureAuthentication(IServiceCollection services)
{
services.AddHttpContextAccessor();
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "Audience";
options.Authority = "Authority";
});
#if DEBUG
if (Environment.IsDevelopment())
{
AllowDevelopmentAuthAccounts(services);
return;
}
#endif
// This is custom and you might need change it to your needs.
services.AddAuthorization();
}
#if DEBUG
// If this is true, you can use the Official JWT bearer login flow AND Development Auth Account (DevBearer) flow for easier testing.
private static void AllowDevelopmentAuthAccounts(IServiceCollection services)
{
services.AddAuthentication("DevBearer").AddScheme<DevelopmentAuthenticationSchemeOptions, DevelopmentAuthenticationHandler>("DevBearer", null);
// This is custom and you might need change it to your needs.
services.AddAuthorization();
}
#endif
Подсказка о Пользовательских Политиках
// Because my Policies/Auth situation is different than yours, I will only post a hint that you might want to use.
// I want to allow calls from the REAL flow AND DevBearer flow during development so I can easily call my API using the DevBearer flow, or still connect it to the real IDentityServer and front-end for REAL calls.
var policyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser();
// The #IF adds an extra "security" check so we don't accidentally activate the development auth flow on production
#if DEBUG
if (_allowDevelopmentAuthAccountCalls)
{
policyBuilder.AddAuthenticationSchemes("DevBearer").RequireAuthenticatedUser();
}
#endif
return policyBuilder;
Обработчик авторизации
#if DEBUG
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace NAMESPACE
{
public class DevelopmentAuthenticationHandler : AuthenticationHandler<DevelopmentAuthenticationSchemeOptions>
{
public DevelopmentAuthenticationHandler(
IOptionsMonitor<DevelopmentAuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.Request.Headers.TryGetValue("Authorization", out var authorizationHeader))
{
return AuthenticateResult.Fail("Unauthorized");
}
var auth = AuthenticationHeaderValue.Parse(authorizationHeader);
if (auth.Scheme == "Bearer")
{
// If Bearer is used, it means the user wants to use the REAL authentication method and not the development accounts.
return AuthenticateResult.Fail("Bearer requests should use the real JWT validation scheme");
}
// Dumb workaround for NSwag/Swagger: I can't find a way to make it automatically pass "DevBearer" in the auth header.
// Having to type DevBearer everytime is annoying. So if it is missing, we just pretend it's there.
// This means you can either pass "ACCOUNT_NAME" in the Authorization header OR "DevBearer ACCOUNT_NAME".
if (auth.Parameter == null)
{
auth = new AuthenticationHeaderValue("DevBearer", auth.Scheme);
}
IEnumerable<Claim> claims;
try
{
var user = auth.Parameter;
claims = GetClaimsForUser(user);
}
catch (ArgumentException e)
{
return AuthenticateResult.Fail(e);
}
var identity = new ClaimsIdentity(claims, "DevBearer");
var principal = new ClaimsPrincipal(identity);
// Add extra claims if you want to
await Options.OnTokenValidated(Context, principal);
var ticket = new AuthenticationTicket(principal, "DevBearer");
return AuthenticateResult.Success(ticket);
}
private static IEnumerable<Claim> GetClaimsForUser(string? user)
{
switch (user?.ToLowerInvariant())
{
// These all depend on your needs.
case "Customer-John":
{
yield return new("ID_CLAIM_NAME", Guid.Parse("JOHN_GUID_THAT_EXISTS_IN_YOUR_DATABASE").ToString(), ClaimValueTypes.String);
yield return new("ROLE_CLAIM_NAME", "Customer", ClaimValueTypes.String);
break;
}
default:
{
throw new ArgumentException("Can't set specific account for local development because the user is not recognized", nameof(user));
}
}
}
}
public class DevelopmentAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public Func<HttpContext, ClaimsPrincipal, Task> OnTokenValidated { get; set; } = (context, principal) => { return Task.CompletedTask; };
}
}
#endif
С помощью чего-то подобного вы могли бы выполнить вызов API с заголовком авторизации, например DevBearer Customer-John
, и он добавил бы идентификатор и утверждение роли в контекст, что позволило бы выполнить аутентификацию 🙂
Комментарии:
1. Вы правы в том, что я не хочу проверять, работает ли аутентификация, и хочу обойти это. Таким образом, я могу просто добавить токен на предъявителя в
Authorization
заголовок запроса?2. Вот так?
Client.DefaultRequestHeaders.Add("Authorization", "DevBearer Customer-John)
?3. Да, именно так это можно было бы назвать :). Если у вас возникнут какие-либо проблемы с этим подходом, дайте мне знать. Если это работает и вас устраивает такой подход, пожалуйста, примите это как ответ. Если это сработает, я все равно спрошу вас, почему вы хотите пропустить часть аутентификации в своем интеграционном тесте. Интеграционный тест должен проверять совместную работу нескольких частей вашей системы. Почему вы не хотите, чтобы ваша система аутентификации была включена?
4. Я не обязательно хочу пропускать часть аутентификации, я хочу иметь возможность издеваться над ней и проверять ситуацию, когда она проходит аутентификацию. У меня есть специальное
TestServer
приложение с аутентификацией и настройкой клиента для этой конкретной цели.