IdentityServer / IdentityServer3

OpenID Connect Provider and OAuth 2.0 Authorization Server Framework for ASP.NET 4.x/Katana
https://identityserver.github.io/Documentation/
Apache License 2.0
2.01k stars 764 forks source link

Azure AD federated logout not redirecting to client application #2657

Closed Steve887 closed 7 years ago

Steve887 commented 8 years ago

I am using IdSvr for a central authentication server to a .Net MVC web application I am building.

I have configured the authentication server to use the Open ID Connect identity provider in order to allow users to authenticate against a multi-tenant Azure Active Directory account, using the Hybrid flow.

Currently, sign in works as expected with my client application redirecting to the authentication server which in turn redirects to Microsoft for login before returning back to my client application with a correctly populated Access Token.

However, when I try to logout I am redirected to Microsoft correctly, but the page stops when it arrives back at the authentication server, rather than continuing back to my client application.

I believe I have setup the post logout redirect correctly as outlined here and I think all of my settings are ok.

When I pull the Identity Server 3 code down and debug it, it is correctly setting the signOutMessageId onto the query string, but hits the following error inside the UseAutofacMiddleware method when it is trying to redirect to my mapped signoutcallback location:

Exception thrown: 'System.InvalidOperationException' in mscorlib.dll

Additional information: Headers already sent

My Authentication Server setup:

app.Map("identity", idsrvApp => {
    var idSvrFactory = new IdentityServerServiceFactory();

    var options = new IdentityServerOptions
    {                
        SiteName = "Site Name",
        SigningCertificate = <Certificate>,
        Factory = idSvrFactory,
        AuthenticationOptions = new AuthenticationOptions
        {
            IdentityProviders = ConfigureIdentityProviders,
            EnablePostSignOutAutoRedirect = true,
            PostSignOutAutoRedirectDelay = 3
        }
    };
    idsrvApp.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
    idsrvApp.UseIdentityServer(options);

    idsrvApp.Map("/signoutcallback", cb => {
                    cb.Run(async ctx => {
                               var state = ctx.Request.Cookies["state"];
                               await ctx.Environment.RenderLoggedOutViewAsync(state);
                    });
                });
});

My Open Id Connect setup to connect to Azure AD:

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        AuthenticationType = "aad",
        SignInAsAuthenticationType = signInAsType,

        Authority = "https://login.microsoftonline.com/common/",
        ClientId = <Client ID>,
        AuthenticationMode = AuthenticationMode.Active,
        TokenValidationParameters = new TokenValidationParameters
        {
            AuthenticationType = Constants.ExternalAuthenticationType,
            ValidateIssuer = false,
        },
        Notifications = new OpenIdConnectAuthenticationNotifications()
        {
            AuthorizationCodeReceived = (context) =>
            {
                var code = context.Code;
                ClientCredential credential = new ClientCredential(<Client ID>, <Client Secret>);
                string tenantId = context.AuthenticationTicket.Identity.FindFirst("tid").Value;
                AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}");
                AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                            code, new Uri(<Identity Server URI"), credential, "https://graph.windows.net");

                return Task.FromResult(0);
            },
            RedirectToIdentityProvider = (context) =>
            {
                string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
                context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
                context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl + "/signoutcallback";

                if (context.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest)
                {
                    var signOutMessageId = context.OwinContext.Environment.GetSignOutMessageId();
                    if (signOutMessageId != null)
                    {
                        context.OwinContext.Response.Cookies.Append("state", signOutMessageId);
                    }
                }
                return Task.FromResult(0);
            }
    });

I cannot find any information about the cause of or solution to this problem. How do I configure this to correctly redirect back to my client application?

brockallen commented 8 years ago

Can you capture a callstack for the "Headers already sent" error?

Steve887 commented 8 years ago

Callstack:

at Microsoft.Owin.Host.HttpListener.RequestProcessing.OwinHttpListenerResponse.RegisterForOnSendingHeaders(Action1 callback, Object state) at Microsoft.Owin.Security.Infrastructure.AuthenticationHandler.<BaseInitializeAsync>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Owin.Security.Infrastructure.AuthenticationMiddleware1.d0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Owin.Cors.CorsMiddleware.d0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at Owin.OwinExtensions.<>c__DisplayClass0_0.<b__0>d.MoveNext() in D:\Applications\IdentityServer3-master\source\Core\Internal\Autofac.Integration.Owin\AutofacAppBuilderExtensions.cs:line 68

Fiddler trace: image

brockallen commented 8 years ago

Did you implement a custom callback endpoint in IdSvr for the signout?

Steve887 commented 8 years ago

Yes, it's in the first block of code. Do I need to create my own page for that, or should idsvr handle the redirect back to the client app?

On 3 Mar 2016, at 11:09 PM, Brock Allen notifications@github.com wrote:

Did you implement a custom callback endpoint in IdSvr for the signout?

— Reply to this email directly or view it on GitHub.

brockallen commented 8 years ago

Ok, i see now... so that should render our "logged out" page from the view service. When you say it "stops at IdSvr" do you see the logged out page, or just this exception?

brockallen commented 8 years ago

I guess I can try to reproduce this with azure to see if I can see what's happening.

Steve887 commented 8 years ago

I only see the exception when I copy the symbols into my application. Otherwise it stops at the signoutcallback page with no further progress.

On 3 Mar 2016, at 11:30 PM, Brock Allen notifications@github.com wrote:

Ok, i see now... so that should render our "logged out" page from the view service. When you say it "stops at IdSvr" do you see the logged out page, or just this exception?

— Reply to this email directly or view it on GitHub.

brockallen commented 8 years ago

But do you see the "logged out" html?

Steve887 commented 8 years ago

You mean the "you are now logged out..."? Then no.

On 4 Mar 2016, at 12:11 AM, Brock Allen notifications@github.com wrote:

But do you see the "logged out" html?

— Reply to this email directly or view it on GitHub.

brockallen commented 8 years ago

Ok, I see now -- so something is triggering a response to emit early, and so our logged out page isn't handling that. If something else is causing the response to be rendered, then presumably it's rendering the result and thus we shouldn't (I guess?).

Does any of this ring a bell?

brockallen commented 8 years ago

On the dev branch I just checked in sample code to the host that shows this working for signing out of AAD. Can you compare that to what you're doing so we can figure out why yours is not working? Thx.

Steve887 commented 8 years ago

As far as I can tell, your example is the same as mine, except for this line:

ctx.Response.Cookies.Append("state", ".", new Microsoft.Owin.CookieOptions { Expires = DateTime.UtcNow.AddYears(-1) });

But I guess that just removes the cookie and shouldn't impact what's happening.

Also, if I put a breakpoint into the signoutcallback method, it is getting hit multiple times. Even when sitting on the blank signoutcallback page after a minute or two.

I've attached a screenshot of my Azure AD configuration, can you confirm this matches yours?

image

brockallen commented 8 years ago

You don't need to configure the signout in the reply to URLs (at least I didn't).

Steve887 commented 8 years ago

Still doesn't work when I remove the https://localhost:44333/identity/signoutcallback reply URL from Azure.

brockallen commented 8 years ago

Well, since I can't repo it then I don't know what else I can offer at this point. The call stack showing autofac is misleading and is mainly because it's the first MW in the pipeline.

Steve887 commented 8 years ago

I'll try and reproduce it in a standalone app and attach it here.

On 8 Mar 2016, at 10:11 AM, Brock Allen notifications@github.com wrote:

Well, since I can't repo it then I don't know what else I can offer at this point.

— Reply to this email directly or view it on GitHub.

Steve887 commented 8 years ago

I've been able to reproduce in a stand alone application. I've uploaded it here: https://github.com/Steve887/IdentityServer-Azure/

The only thing to update is the client id and secret for Azure AD in the startup file.

Let me know if this reproduces the issue for you.

Steve887 commented 8 years ago

@brockallen have you had any luck reproducing from my repository?

brockallen commented 8 years ago

I was able to repro the issue in your project. It seems this recent fix addresses your issue: https://github.com/IdentityServer/IdentityServer3/issues/2678.

Are you able to work from the MyGet feed until we release 2.5?

Steve887 commented 8 years ago

I cloned the dev branch and replaced the 2.4.0 package with the built assemblies and the problem persisted. I'm not sure that commit would fix this issue as that is just checking for null signoutid and I am passing an id to the context.

brockallen commented 8 years ago

Well, I updated your repro with the latest from MyGet and the problem was solved.

Steve887 commented 8 years ago

I did the same thing from MyGet v2.4.1-build00452 into my repo and it didn't work. Can you confirm your Azure AD configuration?

brockallen commented 8 years ago

I used the same AAD config we have in our host.

Steve887 commented 8 years ago

I mean the config in the Azure portal, as per my screenshot above. Because if you're able to get my repo to work and I'm not, that's the only difference remaining.

leastprivilege commented 8 years ago

We have:

SignOn URL: https://localhost:44333/core/aadcb AppID URI: http://idsrv3 Reply URL: https://localhost:44333/core/aadcb

that's it.

Steve887 commented 8 years ago

Well then I don't know what else to try, I set my Azure AD settings to be the same as those and I'm still getting the problem.

brockallen commented 8 years ago

I don't think I have access to our AAD config -- @leastprivilege does.

strtdusty commented 8 years ago

It appears to me that AuthenticationController.Logout is hit twice during the logout. Once prior to the external provider's logout page is displayed, and once after. The initial call Queues and clears the signout cookie so that the second time it is not available when rendering the logout page. A quick and maybe dirty fix I made was to surround the call in AuthenticationController.Logout to context.QueueRemovealOfSignoutMessageCookie() with a check to see if this is the final time in the Logout method.: if (context.ShouldRenderLoggedOutPage()) { context.QueueRemovalOfSignOutMessageCookie(id); }

Steve887 commented 8 years ago

@strtdusty I tried that, but my AuthenticationController.Logout is only getting hit once so this fix doesn't do anything.

strtdusty commented 8 years ago

@Steve887 One thing that you are doing different is setting the RedirectUri and PostLogoutRedirectUri in the RedirectToIdentityProvider notification handler. I believe the IS3 examples show those settings being set on the OpenIdConnectAuthenticationOptions.

brockallen commented 7 years ago

Sorry this has fallen way off the radar. I guess since you've not had any activity here, this got sorted out (or you moved on). What's the status?

strtdusty commented 7 years ago

Thanks for following up @brockallen . We have upgraded to IS4 and I have not been seeing the problem. Feel free to close.

Steve887 commented 7 years ago

@brockallen I have moved on to a different project not using Azure authentication. If this is resolved in IS4, happy to close unless someone has the same issue.

iulian-popescu commented 7 years ago

For idsrv3 it didn't work for me, but as workaround I made a redirect to the logout page. Another thing, i passed the state to the external provider, instead of having in the cookie; this works for both OKTA and Azure AD.

instead of:

idsrvApp.Map("/signoutcallback", cb => {
                    cb.Run(async ctx => {
                               var state = ctx.Request.Cookies["state"];
                               await ctx.Environment.RenderLoggedOutViewAsync(state);
                    });
                });

and

if (context.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest)
                {
                    var signOutMessageId = context.OwinContext.Environment.GetSignOutMessageId();
                    if (signOutMessageId != null)
                    {
                        context.OwinContext.Response.Cookies.Append("state", signOutMessageId);
                    }
                }

have this:

idsrvApp.Map("/signoutcallback", cb => {
                    cb.Run(async ctx => {
                               cb.Run(async ctx =>
                                {
                                    //var state = ctx.Request.Cookies["state"];
                                    var state = ctx.Request.Query.Get("state");
                                    ctx.Response.Redirect(ctx.Request.Uri.AbsoluteUri.Replace("/signoutcallback?state=","/logout?id="));
                                    await Task.FromResult(true);
                                });
                    });
                });

and

if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                        {                            
                            var signOutMessageId = n.OwinContext.Request.Query.Get("id");
                            //var signOutMessageId = context.OwinContext.Environment.GetSignOutMessageId();
                            //if (signOutMessageId != null)
                            //{
                            //    n.OwinContext.Response.Cookies.Append("state", signOutMessageId);
                            //}

                            if (id_token != null)
                            {
                                n.ProtocolMessage.IdTokenHint = id_token; //this is mandatory for okta
                                n.ProtocolMessage.State = signOutMessageId;
                            }
                        }