AspNetCore:如何模拟外部身份验证/Microsoft 帐户以进行集成测试?
AspNetCore: How to mock external authentication / Microsoft account for integration tests?
我的应用程序堆栈中有一个 OpenID Connect / OAuth2 服务器 (IdP)。 IdP 允许本地和外部身份验证。
我有涵盖大多数场景的集成测试,但很难为外部身份验证场景创建端到端测试。有多个外部提供程序,但从我的应用程序角度来看,它们都在 OpenID Connect 上使用相同的工作流,只有细微差别(参数,即重定向 uri、方案名称等)。所以测试其中一个就足够了。其中之一是 Microsoft 帐户(又名 Azure AD)
集成测试基于WebApplicationFactory(对应HttpClient
的内存服务器)。本地身份验证非常简单,因为整个部分都在我的应用程序域中运行,可以访问完整的源代码等。我只需创建一个对授权端点的请求,并在出现提示时 post 返回用户凭据(我仍然需要解析登录页面以检索防伪令牌,但这是可行的)
但是当涉及到外部,例如Microsoft Account,登录涉及多个步骤 AJAX 和最后的 post 超过10个参数,我无法逆向工程师。其他供应商也有同样的难度。
由于外部提供者只是黑盒,从我的 IdP 的角度来看,它只是发出一个挑战(重定向到外部授权)并在重定向后接收。有没有好的方法来模拟“中间”部分?
我的解决方案是创建一个中间件,它将模拟外部身份验证。然后 re-configure 外部身份验证方案的选项指向中间件正在处理的路径。您可能还想覆盖签名密钥(或关闭签名验证)。所以这段代码转到 WebApplicationFactory 的 ConfigureServices/ConfigureTestServices(等,取决于您的设置),以覆盖原始设置:
services.AddTransient<IStartupFilter, FakeExternalAuthenticationStartupFilter>();
services.Configure(AuthenticationSchemes.ExternalMicrosoft, (OpenIdConnectOptions options) =>
{
options.Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = FakeExternalAuthenticationStartupFilter.AuthorizeEndpoint,
};
options.TokenValidationParameters.IssuerSigningKey = FakeExternalAuthenticationStartupFilter.SecurityKey;
});
备注:WebApplicationFactory没有提供覆盖IApplicationBuilder(中间件)栈的方法,所以需要添加IStartupFilter
然后,中间件需要使用安全密钥发出一个令牌,并发出一个表单 post 返回到重定向 uri。实现此目的的通常方法是 return 简单的 HTML 页面,其中包含一个表单,该表单将在加载后自行提交。这在浏览器中运行良好,但 HttpClient 不会执行任何操作,因此测试必须解析响应并手动创建 post 请求。
虽然这是可行的,但我想省去这个额外的步骤,必须解析响应并 re-send 它,并使其成为一个步骤。困难是:
- 无法重定向(以 GET 请求开始,应以 POST 结束,还需要表单数据)
- OpenIdConnectHandler 在重定向(相关性和随机数)之前发出的 cookie 是恢复状态所必需的,仅在重定向 uri 路径(Set-Cookie with path=)
我的解决方案是在设置重定向 uri 的同一路径上创建处理授权 (GET) 请求的中间件,发出令牌并重写请求,以便 OpenIdConnectHandler 能够接收。这是中间件的 Invoke 方法:
public async Task Invoke(HttpContext httpContext)
{
if (!HttpMethods.IsGet(httpContext.Request.Method) || !httpContext.Request.Path.StartsWithSegments(AuthorizeEndpoint))
{
await _next(httpContext);
return;
}
// get and validate query parameters
// Note: these are absolute minimal, might need to add more depending on your flow logic
var clientId = httpContext.Request.Query["client_id"].FirstOrDefault();
var state = httpContext.Request.Query["state"].FirstOrDefault();
var nonce = httpContext.Request.Query["nonce"].FirstOrDefault();
if (clientId is null || state is null || nonce is null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
var token = CreateToken(clientId, state, nonce); // CreateToken implementation omitted, use same signing key as used above
httpContext.Request.Method = HttpMethods.Post;
httpContext.Request.QueryString = QueryString.Empty;
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
var content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
["id_token"] = token,
["token_type"] = "Bearer",
["expires_in"] = "3600",
["state"] = state,
});
using var buffer = new MemoryStream();
await content.CopyToAsync(buffer, httpContext.RequestAborted);
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
var oldBody = httpContext.Request.Body;
httpContext.Request.Body = buffer;
await _next(httpContext);
httpContext.Request.Body = oldBody;
}
我的应用程序堆栈中有一个 OpenID Connect / OAuth2 服务器 (IdP)。 IdP 允许本地和外部身份验证。
我有涵盖大多数场景的集成测试,但很难为外部身份验证场景创建端到端测试。有多个外部提供程序,但从我的应用程序角度来看,它们都在 OpenID Connect 上使用相同的工作流,只有细微差别(参数,即重定向 uri、方案名称等)。所以测试其中一个就足够了。其中之一是 Microsoft 帐户(又名 Azure AD)
集成测试基于WebApplicationFactory(对应HttpClient
的内存服务器)。本地身份验证非常简单,因为整个部分都在我的应用程序域中运行,可以访问完整的源代码等。我只需创建一个对授权端点的请求,并在出现提示时 post 返回用户凭据(我仍然需要解析登录页面以检索防伪令牌,但这是可行的)
但是当涉及到外部,例如Microsoft Account,登录涉及多个步骤 AJAX 和最后的 post 超过10个参数,我无法逆向工程师。其他供应商也有同样的难度。
由于外部提供者只是黑盒,从我的 IdP 的角度来看,它只是发出一个挑战(重定向到外部授权)并在重定向后接收。有没有好的方法来模拟“中间”部分?
我的解决方案是创建一个中间件,它将模拟外部身份验证。然后 re-configure 外部身份验证方案的选项指向中间件正在处理的路径。您可能还想覆盖签名密钥(或关闭签名验证)。所以这段代码转到 WebApplicationFactory 的 ConfigureServices/ConfigureTestServices(等,取决于您的设置),以覆盖原始设置:
services.AddTransient<IStartupFilter, FakeExternalAuthenticationStartupFilter>();
services.Configure(AuthenticationSchemes.ExternalMicrosoft, (OpenIdConnectOptions options) =>
{
options.Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = FakeExternalAuthenticationStartupFilter.AuthorizeEndpoint,
};
options.TokenValidationParameters.IssuerSigningKey = FakeExternalAuthenticationStartupFilter.SecurityKey;
});
备注:WebApplicationFactory没有提供覆盖IApplicationBuilder(中间件)栈的方法,所以需要添加IStartupFilter
然后,中间件需要使用安全密钥发出一个令牌,并发出一个表单 post 返回到重定向 uri。实现此目的的通常方法是 return 简单的 HTML 页面,其中包含一个表单,该表单将在加载后自行提交。这在浏览器中运行良好,但 HttpClient 不会执行任何操作,因此测试必须解析响应并手动创建 post 请求。
虽然这是可行的,但我想省去这个额外的步骤,必须解析响应并 re-send 它,并使其成为一个步骤。困难是:
- 无法重定向(以 GET 请求开始,应以 POST 结束,还需要表单数据)
- OpenIdConnectHandler 在重定向(相关性和随机数)之前发出的 cookie 是恢复状态所必需的,仅在重定向 uri 路径(Set-Cookie with path=)
我的解决方案是在设置重定向 uri 的同一路径上创建处理授权 (GET) 请求的中间件,发出令牌并重写请求,以便 OpenIdConnectHandler 能够接收。这是中间件的 Invoke 方法:
public async Task Invoke(HttpContext httpContext)
{
if (!HttpMethods.IsGet(httpContext.Request.Method) || !httpContext.Request.Path.StartsWithSegments(AuthorizeEndpoint))
{
await _next(httpContext);
return;
}
// get and validate query parameters
// Note: these are absolute minimal, might need to add more depending on your flow logic
var clientId = httpContext.Request.Query["client_id"].FirstOrDefault();
var state = httpContext.Request.Query["state"].FirstOrDefault();
var nonce = httpContext.Request.Query["nonce"].FirstOrDefault();
if (clientId is null || state is null || nonce is null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
var token = CreateToken(clientId, state, nonce); // CreateToken implementation omitted, use same signing key as used above
httpContext.Request.Method = HttpMethods.Post;
httpContext.Request.QueryString = QueryString.Empty;
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
var content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
["id_token"] = token,
["token_type"] = "Bearer",
["expires_in"] = "3600",
["state"] = state,
});
using var buffer = new MemoryStream();
await content.CopyToAsync(buffer, httpContext.RequestAborted);
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
var oldBody = httpContext.Request.Body;
httpContext.Request.Body = buffer;
await _next(httpContext);
httpContext.Request.Body = oldBody;
}