【问题标题】:Writing an integration test for implicit flow auth为隐式流身份验证编写集成测试
【发布时间】:2020-02-20 18:31:22
【问题描述】:

上下文

我维护一个基于 Identity Server 4 和 .NET Core Identity 的身份提供程序。我的用户使用 SPA,在必要时会提示他们使用隐式流程登录(顺便说一句,我知道这不再是推荐的 SPA 流程)。

最近,我添加了一项功能来跟踪为给定用户发布最新令牌的时间。这很容易通过添加ICustomAuthorizeRequestValidator 的实例来完成(参见下面的简化版本):

public class AuthRequestValidator : ICustomAuthorizeRequestValidator
{
    private readonly UserManager<ApplicationUser> _userManager;

    public AuthRequestValidator(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
    {
        if (context.Result.IsError)
        {
            return;
        }

        var userName = context.Result.ValidatedRequest?.Subject?.Identity?.Name;

        var user = await _userManager.FindByNameAsync(userName);
        user.LastTokenIssuedUtc = DateTimeOffset.UtcNow;
        await _userManager.UpdateAsync(user);
    }
}

问题

现在我正在尝试编写一个集成测试,以检查用户登录或请求新令牌时是否更新了日期时间。理想情况下,这将如下所示:

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);

在上面的示例中,我使用RequestTokenImplicitFlowAsync 方法及其参数来说明我的意图。不幸的是,这种方法在现实中并不存在,我也无法弄清楚如何自己实现它。甚至可能吗?在其他测试中,我使用了IdentityModel 库提供的扩展方法,它们支持不同的授权流程。它在该库中不存在的事实强烈暗示我当前的方法可能是错误的。

您对如何使用我的集成测试中的隐式流程登录有什么建议吗?或者,如果这不可能,您能否指出一种我可以用来实现测试新功能目标的不同方法?

【问题讨论】:

    标签: c# oauth-2.0 integration-testing identityserver4 openid-connect


    【解决方案1】:

    嗯,这很难,因为:

    • 隐式流程是交互式的:它需要浏览器和用户交互,这两者都很难模拟;
    • 它涉及多个重要的 GET 和 POST 请求,可能包括一个带有 CSRF 令牌的请求
    • 这取决于您的特定 IdentityServer 的登录屏幕,任何人都可能不同

    无论如何,这里有一个模板解决方案,我已经在我的 IdentityServer4 解决方案中进行了测试,该解决方案支持通过使用 ASP.NET Core Identity 搭建的表单进行本地登录:

    // 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);
    }
    

    但是,如果您要进行大量依赖于模拟用户交互性的测试,那么使用 Selenium 之类的东西和真正的 e2e/集成测试可能会更容易?再说一遍...... :-)

    【讨论】:

    • 种子从何而来?在 Seed.adminEmail 中使用
    • 好点,我在发布之前只重构了一半的代码。在我的 Stack Overflow 示例中,它应该是常量,将更新我的帖子。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多