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.