编写针对隐式流身份验证的集成测试

时间:2019-10-24 12:02:22

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

上下文

我维护一个基于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库提供的扩展方法,它们支持不同的授权流程。该库中不存在该事实的事实强烈表明我当前的方法可能是错误的。

您对如何使用集成测试中的隐式流进行登录有任何建议吗?还是如果不可能,您能指出一种可以用来测试我的新功能的目标的不同方法吗?

1 个答案:

答案 0 :(得分:0)

嗯,这很难,因为:

  • 隐式流是 interactive :它需要浏览器和用户交互,而这两者都是很难模拟的;
  • 它涉及多个不平凡的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", Seed.adminEmail) },
        { new KeyValuePair<string, string>("Input.Password", Seed.adminPassword) },
        { 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之类的工具和真正的端到端/集成测试可能会更容易?再说一次...:-)