0

I have two .NET Core 3.x apps:

  1. "Business App" for all the business logic stuff, where the error occurs
  2. "Auth App", and Identity Server app for signing into and redirecting back to Business App

I'm requesting a password reset on one browser/device, and following the emailed link on another browser/device.

The problem seems to be that I'm sending a link to the original sign in process, that can easily be invalidated:

  1. I navigate to Business App, I'm not signed in yet
  2. It auto navigates me to the Auth App to sign in, a unique url is generated
  3. I click the "Forgot password" link
  4. It creates a password reset link with my current sign in attempt as the return url and sends it to my email
  5. I open the link on a different browser/device/incognito tab
  6. It takes me to the Auth App password reset page
  7. I submit the password reset, it goes through, and then sends me to my return url
  8. Because my return url was for my original sign in attempt, it is not valid on this new browser/device
  9. I get an error saying I'm missing a OpenIdConnect cookie

Upon reaching the Business App, I get a cookie not found error saying:

Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/2.0 POST https://BusinessApp/signin-oidc application/x-www-form-urlencoded 672
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Warning: '.AspNetCore.Correlation.OpenIdConnect.xxxxxxx...' cookie not found.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Information: Error from RemoteAuthentication: Correlation failed..

The password reset url looks like:

https://AuthApp/Account/ResetPassword/?userId=xxxxxx  
&code=xxxxxx  
&returnUrl=/connect/authorize/callback?client_id=business_app  
&redirect_uri=https://BusinessApp/signin-oidc  
&response_type=code  
&scope=openid profile business_app_api offline_access auth_app_api  
&code_challenge=xxxxxx  
&code_challenge_method=S256  
&response_mode=form_post  
&nonce=xxxxxx  
&state=xxxxxx  
&x-client-SKU=ID_NETSTANDARD2_0  
&x-client-ver=5.5.0.0  

Here is the Business App Startup.cs that seems relevant:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(cfg =>
    {
        cfg.SlidingExpiration = true;
        cfg.Cookie.SameSite = SameSiteMode.None;
        cfg.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        cfg.Cookie.IsEssential = true;
    })
    .AddIdentityServerAuthentication("Bearer", options =>
    {
        options.Authority = identityUrl;
        options.ApiName = ApiName;
        options.ApiSecret = "xxxxxx";
        options.SaveToken = true;
        options.SupportedTokens = IdentityServer4.AccessTokenValidation.SupportedTokens.Jwt;
    })
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "OpenID Connect", options =>
    {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = identityUrl;
        options.SignedOutRedirectUri = redirectUrl;
        options.RequireHttpsMetadata = true;

        options.ClientId = ClientId;
        options.ClientSecret = "xxxxxx";
        options.ResponseType = "code";

        options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name", ClaimValueTypes.String);
        options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name", ClaimValueTypes.String);
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add(ApiName);
        options.Scope.Add("offline_access");
        options.Scope.Add("auth_app_api");
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    });
    ...
}

Here is Auth App's ForgotPassword(...) method that generates the email and sends the link, the returnUrl is generated by the Business App:

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model, string returnUrl)
{
    var user = await _userManager.FindByEmailAsync(model.Email);

    var code = await _userManager.GeneratePasswordResetTokenAsync(user);

    // This feels wrong
    var confirmationUrl = $"{Request.Scheme}://{Request.Host}/Account/ResetPassword/?userId={user.Id}&code={UrlEncoder.Default.Encode(code)}&returnUrl={UrlEncoder.Default.Encode(returnUrl)}";

    var viewModel = new ResetPasswordSendGridViewModel(model.Email, confirmationUrl);
    await _sendGridSender.SendEmailResetPasswordAsync(viewModel);

    return RedirectToAction(nameof(ForgotPasswordConfirmation), new { returnUrl = returnUrl });
}

I've tried doing small things like changing SameSiteMode and CookieSecurePolicy, but they don't change anything.

The issue seems squarely focused on how the cookie is just lost in this odd user flow.

I think I need to figure out how to set the return url to a landing page on Business App and let start a new sign in process, rather than try to connect to the original sign-in process.

Any ideas?

2

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.