aspnet / Security

[Archived] Middleware for security and authorization of web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
1.27k stars 600 forks source link

Handling incomplete remote signouts #1917

Closed karthik25 closed 5 years ago

karthik25 commented 5 years ago

I have an identity server which supports multiple open id providers one of which is B2C. After I upgraded the application from .net core 1.1 to .net core 2.1, the signout redirects have stopped working. The application reaches the signout-callback-oidc handler and stops there with an empty page with no information about what went wrong.

The production instance that uses .Net 1.1 instance still works. I tried setting the signout url in the list of reply urls for the B2C application as suggested in one of the Q & A sites, but that did not help either. Can you please offer some pointers on how I could identify whats going on? Tried looking up for ways to hook up logging for AddOpenIdConnect, but did not find anything for that either.

Sequence of urls traversed

.Net core 2.1 (Stops at signout-callback-oidc?state=<state> w/ a blank page)

.Net core 1.1 (Successful)

Open ID Setup for B2C

   services.AddOpenIdConnect(adSettingsB2c.SchemeName, adSettingsB2c.DisplayName, options =>
    {
    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
    options.SignOutScheme = IdentityServerConstants.SignoutScheme;
    options.Authority = $"{adSettingsB2c.AADInstance}/{adSettingsB2c.Tenant}/B2C_1_{adSettingsB2c.SignInPolicyId}/v2.0";
    options.CallbackPath = adSettingsB2c.CallbackPath;
    options.ClientId = adSettingsB2c.ClientId;
    options.ResponseType = OpenIdConnectResponseType.IdToken;
    options.SaveTokens = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true
    };
    options.Events = new OpenIdConnectEvents
    {
        OnRedirectToIdentityProvider = r =>
        {
        var defaultPolicy = adSettingsB2c.SignInPolicyId;
        if (r.Properties.Items.TryGetValue("Policy", out var policy) &&
            !policy.Equals(defaultPolicy))
        {
            r.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile;
            r.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
            r.ProtocolMessage.IssuerAddress = r.ProtocolMessage.IssuerAddress.ToLower().Replace(defaultPolicy.ToLower(), policy.ToLower());
            r.Properties.Items.Remove("Policy");
        }
        if (r.Properties.Items.ContainsKey("email_address"))
        {
            r.ProtocolMessage.SetParameter("login_hint", r.Properties.Items["email_address"]);
        }
        return Task.FromResult(0);
        },
        OnRemoteFailure = r => { // ... }
    };
    })
Tratcher commented 5 years ago

Can you share a Fiddler trace file showing the signout flow?

karthik25 commented 5 years ago

@Tratcher Thanks for responding. Can I send the fiddler traces privately?

EDIT: I see your email address in your profile. Would it be okay to email the traces to that email address?

Tratcher commented 5 years ago

Yes

karthik25 commented 5 years ago

@Tratcher Just sent an email with the traces. Thank you!

Tratcher commented 5 years ago

Hmm, it looks like it doesn't redirect because it's missing the RedirectUri within the state field. Oddly that hasn't changed between 1.1 and 2.1, we only added a new SignedOutCallbackRedirect event.

If you hook into that event do you see a RedirectUri in the AuthenticationProperties collection?

karthik25 commented 5 years ago

I did try out the following too as a last resort earlier. Interestingly the callback was never invoked (locally and hence after deployment to our app service too).

OnSignedOutCallbackRedirect = r =>
{
     r.Response.Redirect("http://www.somesite.com");
     r.HandleResponse();

     return Task.FromResult(0);
}

Even more interesting fact is that this does not happen in my development environment. That makes it more difficult to debug.

karthik25 commented 5 years ago

@Tratcher I was also wondering if the state query parameter passed to b2clogin.com had the redirect uri. post_logout_uri is plain text, so that seems fine. Is it getting lost when its passed on to https://our-id-server/signout-callback-oidc?state= ?

Is there a utility where this state can be decoded? May be a naive question, I did not find one online and never had had to decode it until today!

Tratcher commented 5 years ago

No, it's encrypted. It's only readable as the AuthenticationProperties collection in your OIDC events.

karthik25 commented 5 years ago

@Tratcher Makes sense, I am adding more logging to see if I can figure out what's happening. While I am doing that I was wondering about the following:

Tratcher commented 5 years ago
karthik25 commented 5 years ago

@Tratcher Yes, I tried hooking in to OnSignedOutCallbackRedirect for another provider that works fine and that did not get called either. I will have to see why. And regarding the 2nd question, I was hoping for something else apart from that since OnSignedOutCallbackRedirect is not getting invoked. But let me keep investigating. Let me leave the issue open until I know more. Thanks!

karthik25 commented 5 years ago

@Tratcher Okay, I still have no idea why the callback is not invoked. Also I am sure the redirect uri is set before the external sign out is initiated as shown below:

string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
return SignOut(new Microsoft.AspNetCore.Authentication.AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);

I can see the redirect url in the log, its about 988 bytes. Is that causing issues may be? After this step the oauth/v2.0/logout endpoint is called. So I am sure the state includes it. Wondering why signout-callback-oidc does not include it.

I will keep investigating - I have a feeling its the length of the url. Identity server supports the ability to customize the logout id. I will try that out. Thanks!

karthik25 commented 5 years ago

@Tratcher Apparently for my other Open id providers, OnSignedOutCallbackRedirect is being called. Not for B2C though. Also I tried a custom logout message store shortening the url from 988 bytes to less than 100 bytes, but still had the same issue.

I had one question (if you still have the traces I emailed). Earlier you said the the state passed to signout-callback-oidc did not include the redirect url. I was wondering if the state passed to b2clogin.com contained the redirect uri. Since before the sign out redirect I see the redirect url being included in AuthenticationProperties.

Tratcher commented 5 years ago

I was not able to decode the contents of the state field, I only inferred the missing field from the code. Since you have multiple OIDC providers you'll want to give them different SignedOutCallbackPath values. Otherwise they will conflict and the first one registered will handle the requests. That would explain why your events are not getting called.

karthik25 commented 5 years ago

@Tratcher The callback gets called now! I am setting the SignedoutCallbackPath for all of my open id providers now. Did not face any issue in .net core 1.1 where I only was setting the CallbackPath. So this never occurred to me! I cannot thank you enough, really, was about to go mad. If at all you are in Baltimore beer is on me :)