Reputation: 251
I have IdentityServer4 running as the auth system wired up to use both Google and Amazon along with the local accounts. I have a separate web app using the IdentityServer4 system as the oidc auth provider and things seem to work with authenticating and issuing tokens. The issue is that it does not maintain the returnUrl after authentication. I am using the OIDC StateDataFormatterCache that is built-in to Id4 via the Redis distributed cache and have verified in Redis that it is writing the redirect and even a test parameter I added at challenge time.
After authentication it does not redirect me back to the original url and when I try to access the parameters and AuthenticationProperties set in the challenge, they don't seem to be there after authentication.
I have stepped through the code on the IdentityServer4 side where the login for local and external callback are and the returnUrl never shows up over there. I assume that the OIDC StateDataFormatterCache intercepts this to prevent the potential long url issues and is supposed to intercept the response and inject the return url. That part doesn't seem to work so I was trying to see if I could stick in my own custom parameter into the auth props and grab it back out after auth occurs but that turns up empty. AuthenticationProperties just seems to have the access_token, refresh_token, id_token,etc but is missing the redirect url and my custom parameter.
services.AddOidcStateDataFormatterCache("oidc");
...
var authProps = new AuthenticationProperties { RedirectUri = originalPath + context.Request.QueryString };
Microsoft.Extensions.Primitives.StringValues stateValue;
if (context.Request.Query.TryGetValue("state", out stateValue))
{
authProps.SetParameter("itk", stateValue);
}
await Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync(context,"oidc", authProps); //authProps has custom parameter 'itk' and a redirect
...
var authContext = await context.AuthenticateAsync("oidc");
//authContext.Properties has data but not the RedirectUri or the custom parameter I set and verified to exist in the redis store
Here's what shows up in Redis:
{
"Items": {
".redirect": "/?state=foo",
".xsrf": "DkRjG29eE4qDFaNLSktyTDKfhdzXH8FfVpAwBbje6tc",
"OpenIdConnect.Code.RedirectUri": "https://localhost/signin-oidc"
},
"Parameters": {
"itk": [
"foo"
]
},
"RedirectUri": "/?state=foo"
}
Any ideas on why I am unable to retrieve the info placed into Redis by the state data formatter?
I need to be able to get the value of "itk" in the Parameters.
I found my issue with the redirect not getting through but I still need to be able to get that Parameter.
Upvotes: 5
Views: 4139
Reputation: 4869
The behavior is by design, except persisting to Redis. The comment on the Parameters
says:
Collection of parameters that are passed to the authentication handler. These are not intended for serialization or persistence, only for flowing data between call sites.
We have to employ the Items
collection for serialization or persistence.
The way it works for me:
//configuration
services.AddOidcStateDataFormatterCache(Constants.MyExternalIdIdpName);
//AccountController
[HttpGet]
public IActionResult Login(string returnUrl)
{
string provider = Constants.MyExternalIdIdpName;
string returnUrl2 = Url.Action("ExternalLoginCallback", new { returnUrl = returnUrl });
// start challenge and roundtrip the return URL
var props = new AuthenticationProperties
{
RedirectUri = returnUrl2,
Items = { { "scheme", provider } }
};
return new ChallengeResult(provider, props);
}
and then
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl)
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve claims of the external user
var claims = result.Principal.Claims.ToList();
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
var user = <users>.FindByExternalProvider(provider, userIdClaim.Value);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.SubjectId, user.Username));
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, props, additionalClaims.ToArray());
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
// validate return URL and redirect back to authorization endpoint or a local page
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
}
Upvotes: 2