Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Matthias 3 posts 93 karma points
    Apr 11, 2024 @ 12:12
    Matthias
    0

    UmbracoExternalLoginController.ExternalLogin not working as expected when hosting on Azure App Service

    Greetings,

    I have implemented an external login provider (not for the backoffice, but it connects to it using autolinking, implemented using this example) and it works perfectly when running the Umbraco site locally.

    I click on the button, it redirects to the okta login page, then redirects back to the homepage of my site after login. I can see that I am logged in because I display a welcome message with the name of the user.

    My problem arises when I host this website on Azure App Service.

    I click on the button, I get redirected to the okta login page, then i get redirected back to: mydomain/?ufprt=CfDJ8LXoX0ANCN5BnQZS4V14uiXzehm3idER69mD-aAkGsOiqZRnB1QorygvC25AfZodnBxEdtOTXwNgfUTDPkO_RJFr9yvI8XJQiFphdItEM6nm3SUK_urKsSiCbnFS4hKrxyaLCTNm1UCqom-v06Jx5r9z0JjLA1ghiYZg6NUQftKeGj6bkE_R03oS_1UrdSWL4A and I am not logged in.

    I already did some searching and it turns out that this is what Umbraco uses to find the right controller action (correct me if i'm wrong) when using an UmbracoForm. It adds this to the homepage url of my website and it doesn't log me in.

    Here is the code for the button/form:

    foreach(var login in await _memberExternalLoginProviders.GetMemberProvidersAsync()){
    
    
    using (Html.BeginUmbracoForm<UmbExternalLoginController>(nameof(UmbExternalLoginController.ExternalLogin), new {ReturnUrl = "/"}, new { @class = "align-self-end ml-4" }))
    {                                 
        <button type="submit" class="btn btn-outline-warning" name="provider" value="@login.ExternalLoginProvider.AuthenticationType">
        Sign in
        </button>
    }}
    

    This is my .AddOpenIdConnect extension:

        public static IUmbracoBuilder AddOpenIdConnectAuthentication(this IUmbracoBuilder builder)
    {
        builder.Services.ConfigureOptions<OpenIdConnectMemberExternalLoginProviderOptions>();
    
        builder.AddMemberExternalLogins(logins =>
        {
            logins.AddMemberLogin(
                memberAuthenticationBuilder =>
                {
                    memberAuthenticationBuilder.AddOpenIdConnect(
                        // The scheme must be set with this method to work for the umbraco members
                        memberAuthenticationBuilder.SchemeForMembers(OpenIdConnectMemberExternalLoginProviderOptions.SchemeName),
                        options =>
                        {
                            var config = builder.Config;
                            options.ResponseType = "code";
                            options.Scope.Add("openid");
                            options.Scope.Add("profile");
                            options.RequireHttpsMetadata = true;
                            options.MetadataAddress = config["OpenIdConnect:MetadataAddress"];
                            options.ClientId = config["OpenIdConnect:ClientId"];
                            options.CallbackPath = "/authorization-code/callback";
    
                            options.ClientSecret = config["OpenIdConnect:ClientSecret"];
                            options.SaveTokens = true;
                            options.TokenValidationParameters.SaveSigninToken = true;
                            options.GetClaimsFromUserInfoEndpoint = true;
                            options.SignedOutCallbackPath = "/signout/callback";
                            options.Events.OnTokenValidated = async context =>
                            {
                                var claims = context?.Principal?.Claims.ToList();
                                var email = claims?.SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                                if (email != null)
                                {
                                    // The email claim is required for auto linking.
                                    // So get it from another claim and put it in the email claim.
                                    claims?.Add(new Claim(ClaimTypes.Email, email.Value));
                                }
    
                                var name = claims?.SingleOrDefault(x => x.Type == "name");
                                if (name != null)
                                {
                                    // The name claim is required for auto linking.
                                    // So get it from another claim and put it in the name claim.
                                    claims?.Add(new Claim(ClaimTypes.Name, name.Value));
                                }
    
                                if (context != null)
                                {
                                    // Since we added new claims create a new principal.
                                    var authenticationType = context.Principal?.Identity?.AuthenticationType;
                                    context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType));
                                }
    
                                await Task.FromResult(0);
                            };
                            options.Events.OnRedirectToIdentityProviderForSignOut = async notification =>
                            {
                                var protocolMessage = notification.ProtocolMessage;
    
                                // Since we're in a static extension method we need this approach to get the member manager. 
                                var memberManager = notification.HttpContext.RequestServices.GetService<IMemberManager>();
                                if (memberManager != null)
                                {
                                    var currentMember = await memberManager.GetCurrentMemberAsync();
    
                                    // On the current member we can find all their login tokens from the external login provider.
                                    // These tokens are stored in the umbracoExternalLoginToken table.
                                    var idToken = currentMember?.LoginTokens.FirstOrDefault(x => x.Name == "id_token");
                                    if (idToken != null && !string.IsNullOrEmpty(idToken.Value))
                                    {
                                        // Some external login providers need the IdTokenHint.
                                        // By setting the IdTokenHint the user can be redirected back from the external login provider to this website. 
                                        protocolMessage.IdTokenHint = idToken.Value;
                                    }
                                }
    
                                await Task.FromResult(0);
                            };
                        });
                });
        });
        return builder;
    }}
    

    This is my OpenIdConnectMemberExternalLoginProviderOptions class:

        public static IUmbracoBuilder AddOpenIdConnectAuthentication(this IUmbracoBuilder builder)
    {
        builder.Services.ConfigureOptions<OpenIdConnectMemberExternalLoginProviderOptions>();
    
        builder.AddMemberExternalLogins(logins =>
        {
            logins.AddMemberLogin(
                memberAuthenticationBuilder =>
                {
                    memberAuthenticationBuilder.AddOpenIdConnect(
                        // The scheme must be set with this method to work for the umbraco members
                        memberAuthenticationBuilder.SchemeForMembers(OpenIdConnectMemberExternalLoginProviderOptions.SchemeName),
                        options =>
                        {
                            var config = builder.Config;
                            options.ResponseType = "code";
                            options.Scope.Add("openid");
                            options.Scope.Add("profile");
                            options.RequireHttpsMetadata = true;
                            options.MetadataAddress = config["OpenIdConnect:MetadataAddress"];
                            options.ClientId = config["OpenIdConnect:ClientId"];
                            options.CallbackPath = "/authorization-code/callback";
    
                            options.ClientSecret = config["OpenIdConnect:ClientSecret"];
                            options.SaveTokens = true;
                            options.TokenValidationParameters.SaveSigninToken = true;
                            options.GetClaimsFromUserInfoEndpoint = true;
                            options.SignedOutCallbackPath = "/signout/callback";
                            options.Events.OnTokenValidated = async context =>
                            {
                                var claims = context?.Principal?.Claims.ToList();
                                var email = claims?.SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                                if (email != null)
                                {
                                    // The email claim is required for auto linking.
                                    // So get it from another claim and put it in the email claim.
                                    claims?.Add(new Claim(ClaimTypes.Email, email.Value));
                                }
    
                                var name = claims?.SingleOrDefault(x => x.Type == "name");
                                if (name != null)
                                {
                                    // The name claim is required for auto linking.
                                    // So get it from another claim and put it in the name claim.
                                    claims?.Add(new Claim(ClaimTypes.Name, name.Value));
                                }
    
                                if (context != null)
                                {
                                    // Since we added new claims create a new principal.
                                    var authenticationType = context.Principal?.Identity?.AuthenticationType;
                                    context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType));
                                }
    
                                await Task.FromResult(0);
                            };
                            options.Events.OnRedirectToIdentityProviderForSignOut = async notification =>
                            {
                                var protocolMessage = notification.ProtocolMessage;
    
                                // Since we're in a static extension method we need this approach to get the member manager. 
                                var memberManager = notification.HttpContext.RequestServices.GetService<IMemberManager>();
                                if (memberManager != null)
                                {
                                    var currentMember = await memberManager.GetCurrentMemberAsync();
    
                                    // On the current member we can find all their login tokens from the external login provider.
                                    // These tokens are stored in the umbracoExternalLoginToken table.
                                    var idToken = currentMember?.LoginTokens.FirstOrDefault(x => x.Name == "id_token");
                                    if (idToken != null && !string.IsNullOrEmpty(idToken.Value))
                                    {
                                        // Some external login providers need the IdTokenHint.
                                        // By setting the IdTokenHint the user can be redirected back from the external login provider to this website. 
                                        protocolMessage.IdTokenHint = idToken.Value;
                                    }
                                }
    
                                await Task.FromResult(0);
                            };
                        });
                });
        });
        return builder;
    }}
    

    I am getting very desperate so any help would be appreciated! I am absolutely stumped why this behaviour would occur when hosting it on azure. I am certain my okta config is correct so I have already ruled that out. I am using Umbraco 12 if that makes a difference.

  • Matthias 3 posts 93 karma points
    Apr 11, 2024 @ 14:00
    Matthias
    0

    After some more digging I discovered that in the UmbracoExternalLoginController.ExternalLoginCallback method, the result from the _memberSignInManager.ExternalLoginSignInAsync call will return a AutoLinkSignInResult that includes an error in it's "Errors" connection. It states "invalid email".

    But I am still not sure if it is actually invalid seeing as I have no problem login in to my local. I have ensured the email claim is present too.

    Edit: i tried login in with a user that I created with email "[email protected]" and it came up as invalid email again, but this time in local.

  • Matthias 3 posts 93 karma points
    Apr 12, 2024 @ 09:00
    Matthias
    100

    If anyone is looking at this in the future and has the same problem as me with Autolinking members using Okta OIDC:

    The email was invalid because in this example from the Umbraco AutoLinking docs, the "email" of the user is inside of the "nameidentifier" claim.

    var claims = context?.Principal?.Claims.ToList();
                                        var email = claims?.SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
                                        if (email != null)
                                        {
                                            // The email claim is required for auto linking.
                                            // So get it from another claim and put it in the email claim.
                                            claims?.Add(new Claim(ClaimTypes.Email, email.Value));
                                        }
    

    While if you are using Okta, the nameidentifier claim contains a unique id and NOT an email address. Okta OIDC provides an email claim by itself, so the above code snippet is actually redundant if you include options.Scope.Add("email"); in the options.

    tl;dr: had nothing to do with azure, only with the email claim

Please Sign in or register to post replies

Write your reply to:

Draft