C# 为隐式流身份验证编写集成测试 上下文
我维护一个基于identity Server 4和.NET Core identity的身份提供者。我的用户使用SPA,在那里他们会在必要时被提示使用隐式流登录(顺便说一句,我知道这不再是SPA的推荐流) 最近,我添加了一个功能来跟踪为给定用户颁发最新令牌的时刻。通过添加C# 为隐式流身份验证编写集成测试 上下文,c#,oauth-2.0,integration-testing,identityserver4,openid-connect,C#,Oauth 2.0,Integration Testing,Identityserver4,Openid Connect,我维护一个基于identity Server 4和.NET Core identity的身份提供者。我的用户使用SPA,在那里他们会在必要时被提示使用隐式流登录(顺便说一句,我知道这不再是SPA的推荐流) 最近,我添加了一个功能来跟踪为给定用户颁发最新令牌的时刻。通过添加ICustomAuthorizeRequestValidator的实例,可以轻松实现这一点(请参见下面的简化版本): 公共类AuthRequestValidator:ICustomAuthorizerRequestValidat
ICustomAuthorizeRequestValidator
的实例,可以轻松实现这一点(请参见下面的简化版本):
公共类AuthRequestValidator:ICustomAuthorizerRequestValidator
{
私有只读UserManager库,支持不同的授权流。该库中不存在该库的事实强烈暗示我当前的方法可能是错误的
对于如何使用集成测试中的隐式流登录,您有什么建议吗?或者,如果不可能,您能否指出一种不同的方法,我可以使用它来实现测试新功能的目标?嗯,这非常困难,因为:
- 隐式流是交互式的:它需要浏览器和用户的交互,这两者都是很难模拟的
- 它涉及多个重要的GET和POST请求,可能包括一个带有CSRF令牌的请求
- 这取决于您特定的IdentityServer登录屏幕,每个人的登录屏幕都可能不同
无论如何,这里有一个模板解决方案,我已经在我的IdentityServer4解决方案中测试过,该解决方案通过ASP.NET核心标识搭建的表单支持本地登录:
// Prerequisites:
const string usernameSeededInDatabase = "johndoe@example.org";
const string passwordSeededInDatabase = "Super123Secret!";
const string implicitFlowClientId = "my-implicit-flow-client"; // IDS4 Client setting
const string spaClientUri = "http://localhost:4200/"; // IDS4 Client setting
const string spaClientRedirectUri = "http://localhost:4200/silent-refresh.html"; // IDS4 Client setting
private readonly WebApplicationFactory _factory; // Injected in Test Class
[Fact]
public async Task Can_run_through_implicit_flow()
{
// Simulate Implicit flow with a client that retains cookies too:
var httpClient = _factory.CreateClient();
// Start by faking the "login" GET started from an SPA:
var authorizeRequestUrl = AuthorizeEndpoint
+ "?response_type=id_token token"
+ "&client_id=" + clientId
+ "&state=teststate"
+ "&redirect_uri=" + spaClientUri
+ "&scope=openid profile" // plus an api scope, if you like
+ "&nonce=testnonce";
var authorizeResponse = await httpClient.GetAsync(authorizeRequestUrl);
var authorizeResponseBody = await authorizeResponse.Content.ReadAsStringAsync();
// Our IDS will want you to POST to the same url you got redirected to previously (as it will also contain the returnUrl):
var loginRequestUrl = authorizeResponse.RequestMessage.RequestUri.AbsoluteUri;
// Extract CsrfToken from html:
var regex = new Regex("name=\"__RequestVerificationToken\" type=\"hidden\" value=\"(?<CsrfToken>[^\"]+)\"");
var match = regex.Match(authorizeResponseBody);
var requestVerificationToken = match.Groups["CsrfToken"].Value;
// Simulate the login form POST:
var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
{ new KeyValuePair<string, string>("Input.Email", usernameSeededInDatabase) },
{ new KeyValuePair<string, string>("Input.Password", passwordSeededInDatabase) },
{ new KeyValuePair<string, string>("__RequestVerificationToken", requestVerificationToken) },
});
var loginResponse = await httpClient.PostAsync(loginRequestUrl, content);
var loginResponseBody = await loginResponse.Content.ReadAsStringAsync();
// Now we should have a cookie on the HttpClient that allows silent refreshes:
var silentRefreshUrl = AuthorizeEndpoint
+ "?response_type=id_token token"
+ "&client_id=" + clientId
+ "&state=teststate"
+ "&redirect_uri=" + spaClientRedirectUri
+ "&scope=openid profile" // plus an api scope, if you like
+ "&nonce=testnonce"
+ "&prompt=none"; // Indicates silent refresh
var silentRefreshResponse = await httpClient.GetAsync(silentRefreshUrl);
// We should've been redirected to the silent-refresh.html page (response is probably a 404 since we're not serving the SPA):
Assert.Matches("http://localhost:4200/silent-refresh.html", silentRefreshResponse.RequestMessage.RequestUri.AbsoluteUri);
}
//先决条件:
常量字符串UserNameseedInDatabase=”johndoe@example.org";
const string passwordseedInDatabase=“Super123Secret!”;
const string implicitFlowClientId=“我的隐式流客户端”//IDS4客户端设置
常量字符串spaClientUri=”http://localhost:4200/“;//IDS4客户端设置
常量字符串spaClientRedirectUri=”http://localhost:4200/silent-refresh.html“//IDS4客户端设置
私有只读WebApplicationFactory _factory;//注入测试类
[事实]
公共异步任务可以通过\u隐式\u流()运行
{
//使用保留cookie的客户端模拟隐式流:
var httpClient=_factory.CreateClient();
//从假装“登录”开始从SPA开始:
var authorizeRequestUrl=AuthorizeEndpoint
+“?响应\u类型=id\u令牌”
+“&client_id=“+clientId
+“&state=teststate”
+“&redirect_uri=“+spaClientUri”
+“&scope=openid概要文件”//加上一个api范围,如果您愿意的话
+“&nonce=testnonce”;
var authorizeResponse=await-httpClient.GetAsync(authorizeRequestUrl);
var authorizeResponseBy=await authorizeResponse.Content.ReadAsStringAsync();
//我们的ID将希望您发布到之前重定向到的相同url(因为它还将包含returnUrl):
var loginRequestUrl=authorizeResponse.RequestMessage.RequestUri.AbsoluteUri;
//从html中提取CsrfToken:
var regex=new regex(“name=\”\uu RequestVerificationToken\“type=\“hidden\”value=\”(?[^\“]+)\);
var match=regex.match(authorizeResponseBody);
var requestVerificationToken=match.Groups[“CsrfToken”].Value;
//模拟登录表单POST:
var content=newformurlencodedcontent(新列表
{
{new KeyValuePair(“Input.Email”,usernameededindatabase)},
{new KeyValuePair(“Input.Password”,passwordseedInDatabase)},
{新的KeyValuePair(“\uu RequestVerificationToken”,RequestVerificationToken)},
});
var loginResponse=等待httpClient.PostAsync(loginRequestUrl,内容);
var loginResponseBody=await loginResponse.Content.ReadAsStringAsync();
//现在,我们应该在HttpClient上有一个允许静默刷新的cookie:
var silentRefreshUrl=AuthorizeEndpoint
+“?响应\u类型=id\u令牌”
+“&client_id=“+clientId
+“&state=teststate”
+“&redirect_uri=“+spaclienterrirecturi
+“&scope=openid概要文件”//加上一个api范围,如果您愿意的话
+“&nonce=testnonce”
+“&prompt=none”;//表示静默刷新
var silentRefreshResponse=wait httpClient.GetAsync(silentRefreshUrl);
//我们应该被重定向到silent-refresh.html页面(响应可能是404,因为我们没有为SPA提供服务):
Assert.Matches(“http://localhost:4200/silent-refresh.html”,silentRefreshResponse.RequestMessage.RequestUri.AbsoluteUri);
}
然而,如果你打算依靠模拟用户交互来做很多这样的测试,那么使用Selenium和真正的e2e/集成测试可能会更容易一些?然后再说一遍…:-)种子从哪里来?正如Seed.adminEmailGood point中使用的那样,我在发布之前只重构了一半代码。在我这里的堆栈溢出示例中,它是将是常量,将更新我的帖子。
var user = GetUserFromDb("foo@bar.xyz");
var oldLatestToken = user.LastTokenIssuedUtc;
RequestTokenImplicitFlowAsync(new ImplicitFlowRequestParams
{
UserName = "foo@bar.xyz",
Password = "secret",
Scope = "scope"
});
user = GetUserFromDb("foo@bar.xyz");
Assert.True(oldLatestToken < user.LastTokenIssuedUtc);
// Prerequisites:
const string usernameSeededInDatabase = "johndoe@example.org";
const string passwordSeededInDatabase = "Super123Secret!";
const string implicitFlowClientId = "my-implicit-flow-client"; // IDS4 Client setting
const string spaClientUri = "http://localhost:4200/"; // IDS4 Client setting
const string spaClientRedirectUri = "http://localhost:4200/silent-refresh.html"; // IDS4 Client setting
private readonly WebApplicationFactory _factory; // Injected in Test Class
[Fact]
public async Task Can_run_through_implicit_flow()
{
// Simulate Implicit flow with a client that retains cookies too:
var httpClient = _factory.CreateClient();
// Start by faking the "login" GET started from an SPA:
var authorizeRequestUrl = AuthorizeEndpoint
+ "?response_type=id_token token"
+ "&client_id=" + clientId
+ "&state=teststate"
+ "&redirect_uri=" + spaClientUri
+ "&scope=openid profile" // plus an api scope, if you like
+ "&nonce=testnonce";
var authorizeResponse = await httpClient.GetAsync(authorizeRequestUrl);
var authorizeResponseBody = await authorizeResponse.Content.ReadAsStringAsync();
// Our IDS will want you to POST to the same url you got redirected to previously (as it will also contain the returnUrl):
var loginRequestUrl = authorizeResponse.RequestMessage.RequestUri.AbsoluteUri;
// Extract CsrfToken from html:
var regex = new Regex("name=\"__RequestVerificationToken\" type=\"hidden\" value=\"(?<CsrfToken>[^\"]+)\"");
var match = regex.Match(authorizeResponseBody);
var requestVerificationToken = match.Groups["CsrfToken"].Value;
// Simulate the login form POST:
var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
{ new KeyValuePair<string, string>("Input.Email", usernameSeededInDatabase) },
{ new KeyValuePair<string, string>("Input.Password", passwordSeededInDatabase) },
{ new KeyValuePair<string, string>("__RequestVerificationToken", requestVerificationToken) },
});
var loginResponse = await httpClient.PostAsync(loginRequestUrl, content);
var loginResponseBody = await loginResponse.Content.ReadAsStringAsync();
// Now we should have a cookie on the HttpClient that allows silent refreshes:
var silentRefreshUrl = AuthorizeEndpoint
+ "?response_type=id_token token"
+ "&client_id=" + clientId
+ "&state=teststate"
+ "&redirect_uri=" + spaClientRedirectUri
+ "&scope=openid profile" // plus an api scope, if you like
+ "&nonce=testnonce"
+ "&prompt=none"; // Indicates silent refresh
var silentRefreshResponse = await httpClient.GetAsync(silentRefreshUrl);
// We should've been redirected to the silent-refresh.html page (response is probably a 404 since we're not serving the SPA):
Assert.Matches("http://localhost:4200/silent-refresh.html", silentRefreshResponse.RequestMessage.RequestUri.AbsoluteUri);
}