Я создал простой AspNetCore 2.2
API, который использует IdentityServer 4
для обработки OAuth. Он работает нормально, но теперь я хотел бы добавить интеграционные тесты и недавно обнаружил это. Я использовал его для создания нескольких тестов, и все они работали нормально, пока у меня не было атрибута [Authorize]
на моих контроллерах, но очевидно, что этот атрибут должен быть там.
Я наткнулся на этот вопрос о стеке и из ответов, данных там, я попытался собрать тест, но я все еще получаю ответ Unauthorized
, когда пытаюсь запустить тесты.
Пожалуйста, обрати внимание: Я действительно не знаю, какие детали мне следует использовать при создании клиента.
Также при создании IdentityServerWebHostBuilder
.AddApiResources
? (Может глупый вопрос, но
это имеет значение)Если кто-нибудь может направить меня, это было бы очень признательно.
Вот мой тест:
[Fact]
public async Task Attempt_To_Test_InMemory_IdentityServer()
{
// Create a client
var clientConfiguration = new ClientConfiguration("MyClient", "MySecret");
var client = new Client
{
ClientId = clientConfiguration.Id,
ClientSecrets = new List<Secret>
{
new Secret(clientConfiguration.Secret.Sha256())
},
AllowedScopes = new[] { "api1" },
AllowedGrantTypes = new[] { GrantType.ClientCredentials },
AccessTokenType = AccessTokenType.Jwt,
AllowOfflineAccess = true
};
var webHostBuilder = new IdentityServerWebHostBuilder()
.AddClients(client)
.AddApiResources(new ApiResource("api1", "api1name"))
.CreateWebHostBuilder();
var identityServerProxy = new IdentityServerProxy(webHostBuilder);
var tokenResponse = await identityServerProxy.GetClientAccessTokenAsync(clientConfiguration, "api1");
// *****
// Note: creating an IdentityServerProxy above in order to get an access token
// causes the next line to throw an exception stating: WebHostBuilder allows creation only of a single instance of WebHost
// *****
// Create an auth server from the IdentityServerWebHostBuilder
HttpMessageHandler handler;
try
{
var fakeAuthServer = new TestServer(webHostBuilder);
handler = fakeAuthServer.CreateHandler();
}
catch (Exception e)
{
throw;
}
// Create an auth server from the IdentityServerWebHostBuilder
HttpMessageHandler handler;
try
{
var fakeAuthServer = new TestServer(webHostBuilder);
handler = fakeAuthServer.CreateHandler();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
// Set the BackChannelHandler of the 'production' IdentityServer to use the
// handler form the fakeAuthServer
Startup.BackChannelHandler = handler;
// Create the apiServer
var apiServer = new TestServer(new WebHostBuilder().UseStartup<Startup>());
var apiClient = apiServer.CreateClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var user = new User
{
Username = "[email protected]",
Password = "Password-123"
};
var req = new HttpRequestMessage(new HttpMethod("GET"), "/api/users/login")
{
Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"),
};
// Act
var response = await apiClient.SendAsync(req);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Мой класс запуска:
public class Startup
{
public IConfiguration Configuration { get; }
public static HttpMessageHandler BackChannelHandler { get; set; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
ConfigureAuth(services);
services.AddTransient<IPassportService, PassportService>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
protected virtual void ConfigureAuth(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
options.BackchannelHttpHandler = BackChannelHandler;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
app.UseExceptionMiddleware();
}
}
@alsami Я получаю «неавторизованный», что имеет смысл, потому что я не передавал токен носителя, поэтому я добавил некоторый код, который, как я думал, сделает это, но теперь вызывает другие проблемы, которые, надеюсь, я объяснил в комментариях в обновленном коде.
@alsami Хотя теперь я могу получить токен доступа, я не знаю, как связать свой TestServer
для API с IdentityServerProxy
Можете ли вы предоставить полный исходный код на github? Мне нужно будет проверить вручную, чтобы увидеть, что не работает.
Редактировать:
Предложение ниже было одной проблемой. Исходный исходный код не удался из-за исключения при попытке сборки WebHostBuilder
дважды. Во-вторых, файл конфигурации присутствовал только в проекте API, а не в тестовом проекте, поэтому полномочия также не были установлены.
Вместо того, чтобы делать это
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
options.BackchannelHttpHandler = BackChannelHandler;
});
Вы должны сделать что-то вроде этого:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.JwtBackChannelHandler = BackChannelHandler;
});
Вы можете найти образец здесь.
Надеюсь, это поможет, сработало для меня!
Спасибо за ваше предложение, но я тоже не мог заставить это работать. Я добавил git репозиторий, как вы просили. Это очень простой API, который использует IdentityServer для аутентификации. В проекте также есть проект IntegrationTest, который включает тест, который я разместил здесь. Я был бы очень признателен, если бы вы могли взглянуть и сказать мне, где я ошибаюсь.
получил мерж-реквест, который это исправил: github.com/simax/SuperSimpleAPI/pull/1
Это потрясающе. Большое вам спасибо за вашу помощь. Одна вещь - среди многих :), которая меня смущала, заключалась в том, что я не понимал, что мне нужно вызвать CreateHandler()
из identityServerProxy.IdentityServer
, я думаю, что искал это прямо у identityServerProxy
. Еще раз большое спасибо за создание пакета nuget и за вашу помощь — это очень ценно.
@alsami Я попробовал ваше исправление, github.com/simax/SuperSimpleAPI, и я продолжаю получать http 302. Я подал заявку, как вы описали. Есть идеи, почему?
@Ktt, вы могли бы предоставить репо, чтобы я мог его воспроизвести. У меня в голове нет ответа, почему это происходит.
@alsami Я преодолел это, наша инициализация IOC намного сложнее, чем предоставленный вами код, в конечном итоге попытка за попыткой, мне пришлось удалить строку app.UseIdentityServer() из TestStartup и использовать IdentityServerProxy и его обработчик, как в вашем примере. Думаю написать об этом в БЖ. Я поделюсь с вами ссылкой, когда сделаю. Ваш пример очень помог. Спасибо большое
@Ktt рад, что все получилось! Да, я тоже думал об этом. Еще один образец можно найти здесь, между прочим: github.com/cleancodelabs/…
@alsami, вот что я опубликовал, я также сослался на ваш код в github. medium.com/@kutlu_eren/…
Спасибо! Поделюсь!
Возможно, вы также захотите добавить ссылку на пакет :) nuget.org/packages/IdentityServer4.Contrib.AspNetCore.Testing
@alsami, обязательно сойдет, еще раз спасибо :) пожалуйста, найдите ссылку в блоге, где она указана как «Возможно, вы захотите проверить ее потрясающие реализации здесь».
Если вы не хотите полагаться на статическую переменную для хранения HttpHandler, я обнаружил, что работает следующее. Я думаю, что это намного чище.
Сначала создайте объект, который вы можете создать до того, как будет создан ваш TestHost. Это связано с тем, что у вас не будет HttpHandler до тех пор, пока не будет создан TestHost, поэтому вам нужно использовать оболочку.
public class TestHttpMessageHandler : DelegatingHandler
{
private ILogger _logger;
public TestHttpMessageHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.Information($"Sending HTTP message using TestHttpMessageHandler. Uri: '{request.RequestUri.ToString()}'");
if (WrappedMessageHandler == null) throw new Exception("You must set WrappedMessageHandler before TestHttpMessageHandler can be used.");
var method = typeof(HttpMessageHandler).GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
var result = method.Invoke(this.WrappedMessageHandler, new object[] { request, cancellationToken });
return await (Task<HttpResponseMessage>)result;
}
public HttpMessageHandler WrappedMessageHandler { get; set; }
}
потом
var testMessageHandler = new TestHttpMessageHandler(logger);
var webHostBuilder = new WebHostBuilder()
...
services.PostConfigureAll<JwtBearerOptions>(options =>
{
options.Audience = "http://localhost";
options.Authority = "http://localhost";
options.BackchannelHttpHandler = testMessageHandler;
});
...
var server = new TestServer(webHostBuilder);
var innerHttpMessageHandler = server.CreateHandler();
testMessageHandler.WrappedMessageHandler = innerHttpMessageHandler;
Решение, которое не влияет на производственный код:
public class TestApiWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly HttpClient _identityServerClient;
public TestApiWebApplicationFactory(HttpClient identityServerClient)
{
_identityServerClient = identityServerClient;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(
s =>
{
s.AddSingleton<IConfigureOptions<JwtBearerOptions>>(services =>
{
return new TestJwtBearerOptions(_identityServerClient);
});
});
}
}
и его использование:
_factory = new WebApplicationFactory<Startup>()
{
ClientOptions = {BaseAddress = new Uri("http://localhost:5000/")}
};
_apiFactory = new TestApiWebApplicationFactory<SampleApi.Startup>(_factory.CreateClient())
{
ClientOptions = {BaseAddress = new Uri("http://localhost:5001/")}
};
TestJwtBearerOptions просто проксирует запросы к identityServerClient. Реализацию вы можете найти здесь: https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687
Ссылка не работает: у вас есть код для TestJwtBearerOptions?
Нет, случайно удалил. Но это должно быть довольно просто реализовать. Вам нужно будет наследовать JwtBearerOptions и поместить точки останова в его методы, чтобы выяснить, какой из них обрабатывает HTTP-сообщения. AFAIR, это был BackchannelHttpHandler.
Не могли бы вы добавить код, используемый для фактического запроса токена? Какую ошибку вы получаете?