Open Closed

Logout from External Provider #7453


User avatar
0
neethucp created
  • ABP Framework version: v8.2.0
  • UI Type: Blazor Server
  • Database System: EF Core
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes

Hi,

We have integrated Azure AD authentication in our application. However, when we try to logout, it does not logout from Azure AD. Can you please guide us on how to implement logout from external provider in abp?


29 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    it is already like this https://support.abp.io/QA/Questions/5244/B2C-user-is-logged-in-as-local-user-on-signup-flow

  • User Avatar
    0
    neethucp created

    Hi,

    What we are looking for is to logout from the external provider, by invoking the end session endpoint, and perform a single sign out. We have registered Azure AD as external provider in the auth server using OpenID Connect with dynamic options. So, when we logout we want to redirect to Azure AD logout uri.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    After my check, This is the default behavior.

    If you want to logout from Azure Id, You need to redirect manually.

    For example:

    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LogoutModel))]
    public class MyLoginOutModel : LogoutModel
    {
        public override async Task<IActionResult> OnGetAsync()
        {
            if (CurrentUser.IsAuthenticated)
            {
                await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                {
                    Identity = IdentitySecurityLogIdentityConsts.Identity,
                    Action = IdentitySecurityLogActionConsts.Logout
                });
            }
            //
            await SignInManager.SignOutAsync();
            await HttpContext.SignOutAsync(ConfirmUserModel.ConfirmUserScheme);
            await HttpContext.SignOutAsync(ChangePasswordModel.ChangePasswordScheme);
            
            // redirect to azure id
            return Redirect("https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=https://localhost:44382/Account/Login");
    
        }
    }
    
  • User Avatar
    0
    neethucp created

    Hi,

    I tried extending LogoutModel. But it is not getting invoked. I also tried extending the LogoutController and adding the following in GetAsync method. But I'm getting an error.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    It works for me

    context.Services.AddAuthentication()
        .AddOpenIdConnect("AzureOpenId", "Azure AD OpenId", options =>
        {
            options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/";
            options.ClientId = configuration["AzureAd:ClientId"];
            options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
            options.CallbackPath = configuration["AzureAd:CallbackPath"];
            options.ClientSecret = configuration["AzureAd:ClientSecret"];
            options.RequireHttpsMetadata = false;
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Add("email");
            options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
        });
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LogoutModel))]
    public class MyLoginOutModel : LogoutModel
    {
        public override async Task<IActionResult> OnGetAsync()
        {
            if (CurrentUser.IsAuthenticated)
            {
                await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                {
                    Identity = IdentitySecurityLogIdentityConsts.Identity,
                    Action = IdentitySecurityLogActionConsts.Logout
                });
            }
            //
            await SignInManager.SignOutAsync();
            await HttpContext.SignOutAsync(ConfirmUserModel.ConfirmUserScheme);
            await HttpContext.SignOutAsync(ChangePasswordModel.ChangePasswordScheme);
           
            return SignOut("AzureOpenId");
         
        }
    }
    
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

  • User Avatar
    0
    neethucp created

    The following is our configuration. Added client credentials as dynamic options, so that each tenant can configure their own credentials.

    OnGetAsync of logout model is not even getting executed. Is there anything else I have to do to make this work? Is the configuration added in Auth Server in the sample you provided?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Is the configuration added in Auth Server in the sample you provided?

    No, i didn't do any configuration else. that's all

    you can share a simple example with me, i will check it.

    my email is shiwei.liang@volosoft.com

  • User Avatar
    0
    neethucp created

    Hi,

    I have just added the configuration as mentioned in the document.

    https://docs.abp.io/en/commercial/latest/modules/account#ipostconfigureaccountexternalprovideroptions

    I have also added dynamic options configuration in the identity service. Login is working perfectly. When I checked the AbpAccountAuthenticationRequestHandler I couldn't find any handling for Signout.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    you can share a simple example with me, i will check it.

  • User Avatar
    0
    neethucp created

    Hi,

    Is there any update on this?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I can confirm the problem, we will fix it in the next version.

    You can try:

    public class MyAbpAccountAuthenticationRequestHandler<TOptions, THandler> : IAuthenticationRequestHandler, IAuthenticationSignOutHandler
        where TOptions : RemoteAuthenticationOptions, new()
        where THandler : RemoteAuthenticationHandler<TOptions>
    {
        protected readonly THandler InnerHandler;
        protected readonly IOptions<TOptions> OptionsManager;
    
        public MyAbpAccountAuthenticationRequestHandler(THandler innerHandler, IOptions<TOptions> optionsManager)
        {
            InnerHandler = innerHandler;
            OptionsManager = optionsManager;
        }
    
        public virtual async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
        {
            await InnerHandler.InitializeAsync(scheme, context);
        }
    
        public virtual async Task<AuthenticateResult> AuthenticateAsync()
        {
            return await InnerHandler.AuthenticateAsync();
        }
    
        public virtual async Task ChallengeAsync(AuthenticationProperties properties)
        {
            await OptionsManager.SetAsync(InnerHandler.Scheme.Name);
            ObjectHelper.TrySetProperty(InnerHandler, handler => handler.Options, () => OptionsManager.Value);
    
            await InnerHandler.ChallengeAsync(properties);
        }
    
        public virtual async Task ForbidAsync(AuthenticationProperties properties)
        {
            await InnerHandler.ForbidAsync(properties);
        }
    
        public async Task SignOutAsync(AuthenticationProperties properties)
        {
            await OptionsManager.SetAsync(InnerHandler.Scheme.Name);
            ObjectHelper.TrySetProperty(InnerHandler, handler => handler.Options, () => OptionsManager.Value);
            var signOutHandler = InnerHandler as IAuthenticationSignOutHandler;
            if (signOutHandler == null)
            {
                throw new InvalidOperationException($"The authentication handler registered for scheme '{InnerHandler.Scheme}' is '{InnerHandler.GetType().Name}' which cannot be used for SignOutAsync");
            }
    
            await signOutHandler.SignOutAsync(properties);
        }
    
        public virtual async Task<bool> HandleRequestAsync()
        {
            if (await InnerHandler.ShouldHandleRequestAsync())
            {
                await OptionsManager.SetAsync(InnerHandler.Scheme.Name);
                ObjectHelper.TrySetProperty(InnerHandler, handler => handler.Options, () => OptionsManager.Value);
            }
    
            return await InnerHandler.HandleRequestAsync();
        }
    
        public virtual THandler GetHandler()
        {
            return InnerHandler;
        }
    }
    
    
    public static class AuthenticationBuilderExtensions
    {
        public static AuthenticationBuilder AddMyAbpAccountDynamicOptions<TOptions, THandler>(this AuthenticationBuilder authenticationBuilder)
            where TOptions : RemoteAuthenticationOptions, new()
            where THandler : RemoteAuthenticationHandler<TOptions>
        {
            authenticationBuilder.Services.AddScoped(typeof(AccountExternalProviderOptionsManager<TOptions>));
    
            authenticationBuilder.Services.Replace(ServiceDescriptor.Scoped(typeof(IOptions<TOptions>),
                provider => provider.GetRequiredService<AccountExternalProviderOptionsManager<TOptions>>()));
            authenticationBuilder.Services.Replace(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<TOptions>),
                provider => provider.GetRequiredService<AccountExternalProviderOptionsManager<TOptions>>()));
            authenticationBuilder.Services.Replace(ServiceDescriptor.Scoped(typeof(IOptionsMonitor<TOptions>),
                provider => provider.GetRequiredService<AccountExternalProviderOptionsManager<TOptions>>()));
    
            authenticationBuilder.Services.Replace(ServiceDescriptor.Transient<IOptionsFactory<TOptions>, AccountExternalProviderOptionsFactory<TOptions>>());
    
            var handler = authenticationBuilder.Services.LastOrDefault(x => x.ServiceType == typeof(THandler));
            authenticationBuilder.Services.Replace(new ServiceDescriptor(
                typeof(THandler),
                provider => new MyAbpAccountAuthenticationRequestHandler<TOptions, THandler>(
                    (THandler)ActivatorUtilities.CreateInstance(provider, typeof(THandler)),
                    provider.GetRequiredService<IOptions<TOptions>>()),
                handler?.Lifetime ?? ServiceLifetime.Transient));
    
            return authenticationBuilder;
        }
    }
    
    
    context.Services.AddAuthentication()
       .AddOpenIdConnect("AzureAD", "Azure AD", options =>
       {
           options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
           options.RequireHttpsMetadata = false;
           options.SaveTokens = true;
           options.GetClaimsFromUserInfoEndpoint = true;
           options.Scope.Add("email");
           options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub");
           options.CallbackPath = configuration["AzureAd:CallbackPath"];
       })
        .AddMyAbpAccountDynamicOptions<OpenIdConnectOptions, OpenIdConnectHandler>()
        .Services.AddDynamicExternalLoginProviderOptions<OpenIdConnectOptions>("AzureAD",
            options =>
            {
                options.WithProperty(x => x.Authority);
                options.WithProperty(x => x.ClientId);
                options.WithProperty(x => x.ClientSecret, isSecret: true);
            });    
    
    [Route("connect/logout")]
    [ApiExplorerSettings(IgnoreApi = true)]
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LogoutController))]
    public class MyLogoutController : LogoutController
    {
        [HttpGet]
        public override async Task<IActionResult> GetAsync()
        {
            await SignInManager.SignOutAsync();
    
            return SignOut(authenticationSchemes: "AzureAD");
        }
    }
    
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    your ticket was refunded

  • User Avatar
    0
    roberto.fiocchi created

    HI, liangshiwei is this fix also valid for Blazor WASM?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    yes

  • User Avatar
    0
    neethucp created

    Hi, The logout is working, but it is redirecting back to the auth server login page. How can we invoke the post logout uri of the client application?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    You can add the parameters post logout uri manually

  • User Avatar
    0
    neethucp created

    This is my logout controller now. The HttpContext.Request has the post redirect uri of the blazor client application. But it is not invoking the blazor post redirect uri when I login through external provider. Can you please help on how I can implement this?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    get redirect uri from HttpContext.Request and add to AuthenticationProperties.RedirectUri

  • User Avatar
    0
    neethucp created

    It shows a blank page as state is missing in the signout-callback uri

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    You can try this ,it works for me:

    [Route("connect/logout")]
    [ApiExplorerSettings(IgnoreApi = true)]
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LogoutController))]
    public class MyLogoutController : LogoutController
    {
        [HttpGet]
        public override async Task<IActionResult> GetAsync()
        {
            await SignInManager.SignOutAsync();
            
            return SignOut(authenticationSchemes: "AzureAD" , properties: new AuthenticationProperties()
            {
                RedirectUri = "https://localhost:44314" // client URL
            });
        }
    }
    
  • User Avatar
    0
    roberto.fiocchi created

    Hi liangshiwei, If instead of "https://localhost:44314" you showed how to retrieve the client URL from settings or configuration it would be a better solution

    :-)

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can get the client URL from the HTTP Request.

  • User Avatar
    0
    roberto.fiocchi created

    Can you update your example with the final code we should use?

    Thank you

  • User Avatar
    0
    neethucp created

    @liangshiwei The blazor application has initiated an oidc signout flow and the expectation is that auth server will redirect back to the post logout uri with a state param.

Made with ❤️ on ABP v9.0.0-preview Updated on September 19, 2024, 10:13