Double Log in screen when logging in to Umbraco using Azure AD
Hi All
We've implemented Login using Azure AD and autolink users with an organisation AD account to their Umbraco users. Also implemented the callback OnExternalLogin where we assign the users to groups depending on the role claim received from Azure. This all works fine.
One thing I am having trouble with is the following.
Scenario:
User does not exist in umbraco database
User is already logged in to Azure AD (via the Azure Portal) so cookies and session data are present on the machine
User enters umbraco url. Prior to being presented with the log in screen, the user is auto linked and the user is created in the db.
User is presented with the log in screen
Sign in with Active Directory
User clicks, and OnExternalLogin callback is triggered assigning the user to groups depending on the role claims in Azure.
User is redirected to the following url:
/umbraco/#/login/false?returnPath=%252Fcontent
and presented with the log in screen again
Clicking the sign in button again signs them in and are able to access the content page !!
This only happens if the user has not been previously autolinked and created in the umbraco db. If the user follows the above procedure and is autolinked, closes the browser and was to clear all sessions, cookies etc and retry the process, the user is logged in after signing in with their AD account on the first attempt.
Could someone shed any light as to why this "double log in" might be happening for first time users?
Hi Dave....thanks for reaching out...we're running version 8.11.1
this is some of our code snippets
Im currently running tests locally which is why the redirect is pointing to localhost:
UmbracoStandardOwinStartup:
public class UmbracoStandardOwinStartup : UmbracoDefaultOwinStartup
{
/// <summary>
/// Configures the back office authentication for Umbraco
/// </summary>
/// <param name="app"></param>
protected override void ConfigureUmbracoAuthentication(IAppBuilder app)
{
// Must call the base implementation to configure the default back office authentication config.
base.ConfigureUmbracoAuthentication(app);
var adConfig = ServiceLocator.ServiceProvider.GetService<IConfiguration>().GetSection(CatalogueServiceAd.ConfigPath).Get<CatalogueServiceAd>();
if (adConfig.Enabled)
{
app.ConfigureBackOfficeAzureActiveDirectoryAuth(adConfig);
}
app.UseUmbracoBackOfficeTokenAuth(new BackOfficeAuthServerProviderOptions
{
CorsPolicy = new CorsPolicy
{
AllowAnyHeader = true,
AllowAnyMethod = true,
AllowAnyOrigin = true
}
});
}
}
and our UmbracoADAuthExtension class:
using KLP.Catalogue.Core.Configurations;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using KLP.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Security;
namespace KLP.Catalogue.Umbraco
{
public static class UmbracoADAuthExtensions
{
public static void ConfigureBackOfficeAzureActiveDirectoryAuth(this IAppBuilder app, CatalogueServiceAd adConfig,
string caption = "Active Directory", string style = "btn-microsoft", string icon = "fa-windows")
{
var adOptions = new OpenIdConnectAuthenticationOptions
{
SignInAsAuthenticationType = "UmbracoExternalCookie",
ClientId = adConfig.ClientId.GetTenantValueOrDefault(),
Authority = adConfig.Authority.GetTenantValueOrDefault(),
RedirectUri = "https://localhost:44317/umbraco/"
};
adOptions.ForUmbracoBackOffice(style, icon);
adOptions.Caption = caption;
//Need to set the auth tyep as the issuer path
adOptions.AuthenticationType = string.Format(
CultureInfo.InvariantCulture,
"https://sts.windows.net/{0}/",
adConfig.TenantId.GetTenantValueOrDefault());
app.UseOpenIdConnectAuthentication(adOptions);
adOptions.SetBackOfficeExternalLoginProviderOptions(
new BackOfficeExternalLoginProviderOptions
{
AutoLinkOptions = new ExternalSignInAutoLinkOptions(
// must be true for auto-linking to be enabled
autoLinkExternalAccount: true,
// Optionally specify default user group, else
// assign in the OnAutoLinking callback
// (default is editor)
defaultUserGroups: new[] { "" }.ToArray(),
// Optionally specify the default culture to create
// the user as. If null it will use the default
// culture defined in the web.config, or it can
// be dynamically assigned in the OnAutoLinking
// callback.
defaultCulture: null)
{
// Optionally you can disable the ability to link/unlink
// manually from within the back office. Set this to false
// if you don't want the user to unlink from this external
// provider.
AllowManualLinking = false,
// Optional callback
//OnAutoLinking = (_user, _externalLogin) =>
//{
// // You can customize the user before it's linked.
// // i.e. Modify the user's groups based on the Claims returned
// // in the externalLogin info
//},
// Optional callback
OnExternalLogin = (_user, _externalLogin) =>
{
// You can customize the user before it's saved whenever they have
// logged in with the external provider.
// i.e. Sync the user's name based on the Claims returned
// in the externalLogin info
return SyncAzureADRolesToUmbracoGroups(_user, _externalLogin);
}
},
// Optionally you can disable the ability for users
// to login with a username/password. If this is set
// to true, it will disable username/password login
// even if there are other external login providers installed.
DenyLocalLogin = true,
// Optionally choose to automatically redirect to the
// external login provider so the user doesn't have
// to click the login button. This is
AutoRedirectLoginToExternalProvider = true
});
}
private static bool SyncAzureADRolesToUmbracoGroups(BackOfficeIdentityUser backOfficeIdentityUser, ExternalLoginInfo externalLogin)
{
// flow the external claim to the umbraco
// back office identity cookie authentication
var extClaims = externalLogin
.ExternalIdentity
.Claims
.Where(x => x.Type == externalLogin.ExternalIdentity.RoleClaimType)
.Select(x => x.Value)
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
var umbracoGroupsToDelete = new HashSet<string>(); // Groups users should no longer be assigned to
// Identify group access that should be deleted or that should remain
foreach (var userGroup in backOfficeIdentityUser.Groups)
{
if (extClaims.Contains(userGroup.Alias))
{
extClaims.Remove(userGroup.Alias);
}
else
{
umbracoGroupsToDelete.Add(userGroup.Alias);
}
}
var userService = Current.Services.UserService;
var user = userService.GetUsersById(backOfficeIdentityUser.Id).FirstOrDefault();
if (user == null)
{
throw new Exception($"User can login but can't find the user in Umbraco. User Id: {backOfficeIdentityUser.Id}, Email: {backOfficeIdentityUser.Email}");
}
var logger = ServiceLocator.ServiceProvider.GetService<ILogger<Startup>>();
// Remove user from invalid user group
foreach (var groupToDelete in umbracoGroupsToDelete)
{
user.RemoveGroup(groupToDelete);
}
// Add roles
foreach (var groupToAdd in extClaims)
{
if (userService.GetUserGroupByAlias(groupToAdd) is IReadOnlyUserGroup userGroup)
{
user.AddGroup(userGroup);
}
else
{
logger.LogWarning($"Group '{groupToAdd}' couldn't be found in Umbraco");
}
}
// Commit changes to user
userService.Save(user);
return true;
}
}
}
finally managed to solve this issue. Using the above code, in our UmbracoADAuthExtension class, I modified the OpenIdConnectAuthenticationOptions as follows:
var adOptions = new OpenIdConnectAuthenticationOptions
{
SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType,
ClientId = adConfig.ClientId.GetTenantValueOrDefault(),
Authority = adConfig.Authority.GetTenantValueOrDefault(),
RedirectUri = adConfig.RedirectUrl,
AuthenticationMode = AuthenticationMode.Passive,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = async context =>
{
var userService = Current.Services.UserService;
var email = context.JwtSecurityToken.Claims.First(x => x.Type == "email").Value;
var issuer = context.JwtSecurityToken.Claims.First(x => x.Type == "iss").Value;
var providerKey = context.JwtSecurityToken.Claims.First(x => x.Type == "sub").Value;
var name = context.JwtSecurityToken.Claims.First(x => x.Type == "name").Value;
var userManager = context.OwinContext.GetUserManager<BackOfficeUserManager>();
var user = userService.GetByEmail(email) ?? userService.CreateUserWithIdentity(email, email);
var identity = await userManager.FindByEmailAsync(email);
if (identity.Logins.All(x => x.ProviderKey != providerKey))
{
identity.Logins.Add(new IdentityUserLogin(issuer, providerKey, user.Id));
identity.Name = name;
await userManager.UpdateAsync(identity);
}
}
}
};
Hope this helps out anyone that encounters the same issue
Double Log in screen when logging in to Umbraco using Azure AD
Hi All
We've implemented Login using Azure AD and autolink users with an organisation AD account to their Umbraco users. Also implemented the callback OnExternalLogin where we assign the users to groups depending on the role claim received from Azure. This all works fine.
One thing I am having trouble with is the following.
Scenario:
User does not exist in umbraco database
User is already logged in to Azure AD (via the Azure Portal) so cookies and session data are present on the machine
User enters umbraco url. Prior to being presented with the log in screen, the user is auto linked and the user is created in the db.
User is presented with the log in screen
User clicks, and OnExternalLogin callback is triggered assigning the user to groups depending on the role claims in Azure.
User is redirected to the following url: /umbraco/#/login/false?returnPath=%252Fcontent and presented with the log in screen again
Clicking the sign in button again signs them in and are able to access the content page !!
This only happens if the user has not been previously autolinked and created in the umbraco db. If the user follows the above procedure and is autolinked, closes the browser and was to clear all sessions, cookies etc and retry the process, the user is logged in after signing in with their AD account on the first attempt.
Could someone shed any light as to why this "double log in" might be happening for first time users?
Thanks in advance
Hi James,
I have almost the same working on Umbraco 8.8
What version of Umbraco are you working on and could you show some code ?
Dave
Hi Dave....thanks for reaching out...we're running version 8.11.1
this is some of our code snippets Im currently running tests locally which is why the redirect is pointing to localhost:
UmbracoStandardOwinStartup:
and our UmbracoADAuthExtension class:
Hi,
I believe when your user info gets overridden in your case. You might want to add following setting in your web.config/appSettings section.
There is more info regarding this one at shazwazza's blog.
Hi, thanks for posting Mehmet
I have already come across that post and tried it out.
I have added the app setting and also added the configuration in the startup file as suggested in the post, however still no success
finally managed to solve this issue. Using the above code, in our UmbracoADAuthExtension class, I modified the OpenIdConnectAuthenticationOptions as follows:
Hope this helps out anyone that encounters the same issue
is working on a reply...