Reputation: 6246
I maintain an identity provider based on Identity Server 4 and .NET Core Identity. My users use a SPA, where they are prompted to login using the implicit flow whenever necessary (btw, I know it is no longer the recommended flow for SPAs).
Recently, I added a feature to track the moment at which the latest token was issued for a given user. This was easily done by adding an instance of ICustomAuthorizeRequestValidator
(see below for a simplified version):
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);
}
}
Now I am trying to write an integration test that checks whether the datetime is being updated when the user logs in or when they request a new token. Ideally, this would look like the following:
var user = GetUserFromDb("[email protected]");
var oldLatestToken = user.LastTokenIssuedUtc;
RequestTokenImplicitFlowAsync(new ImplicitFlowRequestParams
{
UserName = "[email protected]",
Password = "secret",
Scope = "scope"
});
user = GetUserFromDb("[email protected]");
Assert.True(oldLatestToken < user.LastTokenIssuedUtc);
In the example above I use RequestTokenImplicitFlowAsync
method and its parameters to illustrate my intent. Unfortunately, such a method does not exist in reality and I haven't been able to figure out how I could implement it myself. Is it even possible? In other tests I am using the extension methods provided by the IdentityModel library, which support different authorization flows. The fact that it doesn't exist in that library is a strong hint that my current approach is probably wrong.
Do you have any suggestions on how to log in using the implicit flow from my integration test? Or if that is not possible, could you point out a different approach I could use to achieve the goal of testing my new feature?
Upvotes: 2
Views: 1024
Reputation: 63739
Well, this is pretty hard, because:
At any rate, here is a template solution that I've tested to work in my IdentityServer4 solution that supports local logins via forms scaffolded with ASP.NET Core Identity:
// Prerequisites:
const string usernameSeededInDatabase = "[email protected]";
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);
}
However, if you're going to do a lot of these tests relying on simulating user interactivity, using something like Selenium and a real e2e/integration test may be easier? Then again... :-)
Upvotes: 1