My co-worker Nick Agnostopolus and I just went through a process of figuring out how to configure Sitecore Federated Authentication in 9.1.1 to use Insite Identity Server as an OpenID Connect provider. After a few long days we spent reflecting the Insite and Sitecore code, we have arrived at the solution. A few customizations had to be done on Insite side to make the whole thing work. Below is a detailed overview of what was done.
Configuring Insite Identity Provider in Sitecore
InsiteIdentity pipeline processor
First, a pipeline processor for Insite Identity had to be created. This is the class where most of the OpenID Connect magic happens.
public class InsiteIdentity : IdentityProvidersProcessor { public const string PostLogoutEndPointName = "postinsitelogout"; private readonly string oauthTokenEndpoint = "connect/token"; private readonly string oauthUserInfoEndpoint = "connect/userinfo"; public InsiteIdentity( FederatedAuthenticationConfiguration federatedAuthenticationConfiguration, ICookieManager cookieManager, BaseSettings settings) : base(federatedAuthenticationConfiguration, cookieManager, settings) { } public Collection<string> Scopes { get; } = new Collection<string>(); protected override string IdentityProviderName => "InsiteIdentity"; protected IdentityProvider IdentityProvider { get; set; } private string ClientId { get { return Settings.GetSetting("InsiteIdentity.ClientId", "Sitecore"); } } private string ClientSecret { get { return Settings.GetSetting("InsiteIdentity.ClientSecret"); } } private string Authority { get { return Settings.GetSetting("InsiteIdentity.Authority"); } } private string CallbackAuthority { get { return Settings.GetSetting("InsiteIdentity.CallbackAuthority"); } } protected override void ProcessCore([NotNull] IdentityProvidersArgs args) { Assert.ArgumentNotNull(args, nameof(args)); this.IdentityProvider = this.GetIdentityProvider(); var authenticationType = this.GetAuthenticationType(); args.App.UseOpenIdConnectAuthentication(this.CreateOpenIdConnectAuthenticationOptions(authenticationType, this.IdentityProvider)); } protected string BuildPostLogoutRedirectUri(IOwinContext owinContext, string authority) { Assert.ArgumentNotNull((object)owinContext, nameof(owinContext)); Uri uri = owinContext.Request.Uri; if (string.IsNullOrEmpty(authority)) { authority = uri.GetLeftPart(UriPartial.Authority); } return InsiteIdentity.CombineUrlPath(authority, this.Settings.IdentityProcessingPathPrefix(), InsiteIdentity.PostLogoutEndPointName) + "?ReturnUrl" + "=" + HttpUtility.UrlEncode(uri.ToString()); } private static string CombineUrlPath(params string[] parts) { List<string> list = ((IEnumerable<string>)parts).Where<string>((Func<string, bool>)(p => !string.IsNullOrEmpty(p))).ToList<string>(); string str = string.Join("/", list.Select<string, string>((Func<string, string>)(p => p.Trim('/')))); if (list.First<string>()[0] == '/') { str = "/" + str; } if (list.Last<string>()[0] == '/') { str += "/"; } return str; } private OpenIdConnectAuthenticationOptions CreateOpenIdConnectAuthenticationOptions(string authenticationType, IdentityProvider identityProvider) { var options = new OpenIdConnectAuthenticationOptions { Caption = this.IdentityProvider.Caption, AuthenticationType = authenticationType, AuthenticationMode = AuthenticationMode.Passive, ClientId = this.ClientId, ClientSecret = this.ClientSecret, Authority = this.Authority, ResponseType = OpenIdConnectResponseType.CodeIdTokenToken, Scope = string.Join(" ", (IEnumerable<string>)this.Scopes), UseTokenLifetime = false, CookieManager = this.CookieManager, Notifications = new OpenIdConnectAuthenticationNotifications { SecurityTokenValidated = async notification => { await SecurityTokenValidated(notification); }, RedirectToIdentityProvider = notification => { return RedirectToIdentityProvider(notification); }, } }; options.TokenValidationParameters.SaveSigninToken = identityProvider.TriggerExternalSignOut; return options; } private async Task SecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification) { UserInfoResponse userInfoResponse; using (UserInfoClient userInfoClient = new UserInfoClient(notification.Options.Authority.EnsureTrailingSlash() + this.oauthUserInfoEndpoint)) { LogManager.Diagnostic("[Sitecore Identity] auth_code: " + notification.ProtocolMessage.AccessToken, this); userInfoResponse = await userInfoClient.GetAsync(notification.ProtocolMessage.AccessToken, new CancellationToken()).ConfigureAwait(false); } var additionalClaims = new TransformationContextWithAdditionalClaims(this.FederatedAuthenticationConfiguration, this.IdentityProvider); if (userInfoResponse.IsError) { LogManager.Error("[Sitecore Identity] Unable to get user info - " + userInfoResponse.Error, userInfoResponse.Exception, this); LogManager.Diagnostic("[Sitecore Identity] userInfoResponse.Response.Raw: " + userInfoResponse.Raw, this); } else { additionalClaims.AdditionalClaims = userInfoResponse.Claims; } this.AddCustomClaims(notification, userInfoResponse); notification.AuthenticationTicket.Identity.ApplyClaimsTransformations((TransformationContext)additionalClaims); var transformationContext = new TransformationContextWithAdditionalClaims(this.FederatedAuthenticationConfiguration, this.IdentityProvider); foreach (Transformation transformation in (transformationContext.FederatedAuthenticationConfiguration?.SharedClaimsTransformations ?? Enumerable.Empty<Transformation>()).Concat((IEnumerable<Transformation>)transformationContext.IdentityProvider.Transformations)) { transformation.Transform(notification.AuthenticationTicket.Identity, transformationContext); } } private void AddCustomClaims(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification, UserInfoResponse userInfoResponse) { var firstName = string.Empty; var lastName = string.Empty; ////Retrieve the claim given_name, and assign to first_name if (userInfoResponse.Claims.Any(k => k.Type.Contains("given_name"))) { firstName = userInfoResponse.Claims.FirstOrDefault(k => k.Type.Contains("given_name")).Value; } ////The claim "family_name" is what was getting returned from the info if (userInfoResponse.Claims.Any(k => k.Type.Contains("family_name"))) { lastName = userInfoResponse.Claims.FirstOrDefault(k => k.Type.Contains("family_name")).Value; } ////Add a custom claim, which is then transformed to the Sitecore FullName field. notification.AuthenticationTicket.Identity.AddClaim(new Claim("ExternalFullName", firstName + " " + lastName)); ////Add another custom claim for comments: notification.AuthenticationTicket.Identity.AddClaim(new Claim("xComment", "Insite User")); notification.AuthenticationTicket.Identity.AddClaim(new Claim("insite_role", @"CommerceUsers\\Extranet User")); } private Task RedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification) { var owinContext = notification.OwinContext; OpenIdConnectMessage protocolMessage = notification.ProtocolMessage; if (protocolMessage.RequestType == OpenIdConnectRequestType.Authentication) { protocolMessage.AcrValues = this.GetAcrValues(owinContext); protocolMessage.RedirectUri = this.BuildRedirectUri(owinContext, "signin"); protocolMessage.Prompt = this.GetSuitablePrompt(owinContext, "login"); } else if (protocolMessage.RequestType == OpenIdConnectRequestType.Logout && this.GetIdentityProvider().TriggerExternalSignOut) { protocolMessage.IdTokenHint = this.GetIdTokenHint(owinContext); protocolMessage.PostLogoutRedirectUri = this.BuildPostLogoutRedirectUri(owinContext, string.Empty); } return Task.CompletedTask; } }
An InsiteIdentityProvider class was implemented:
public class InsiteIdentityProvider : DefaultIdentityProvider { public InsiteIdentityProvider(BaseDomainManager domainManager, BaseCorePipelineManager corePipelineManager) : base("InsiteIdentity", domainManager) { Assert.ArgumentNotNull((object)corePipelineManager, nameof(corePipelineManager)); this.CorePipelineManager = corePipelineManager; } protected BaseCorePipelineManager CorePipelineManager { get; } }
Foundation.Account.config file
The following configurations were added to Foundation.Account.config file.
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> <sitecore> <sc.variable name="insiteIdentityServerAuthority" value="https://xxxinsite.local.com" /> <settings> <setting name="InsiteIdentity.ClientId" value="xxx" /> <setting name="InsiteIdentity.ClientSecret" value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" /> <setting name="InsiteIdentity.OAuthRedirectUri" value="/identity/externallogincallback" /> <setting name="InsiteIdentity.Authority" value="$(insiteIdentityServerAuthority)/identity" /> <setting name="InsiteIdentity.CallbackAuthority" value="/identity/externallogincallback" /> </settings> <domainManager> <domains> <domain id="modules" type="Sitecore.Security.Domains.Domain, Sitecore.Kernel"> <param desc="name">$(id)</param> <ensureAnonymousUser>false</ensureAnonymousUser> </domain> </domains> </domainManager> <pipelines> <accounts.loggedIn /> <accounts.loggedOut /> <accounts.registered /> <owin.initialize> <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.JwtBearerAuthentication, Sitecore.Owin.Authentication.IdentityServer" patch:before="processor[@method='Authenticate']" resolve="true" > <identityProviderName>InsiteIdentity</identityProviderName> <audiences hint="raw:AddAudience"> <audience value="$(insiteIdentityServerAuthority)/resources"/> </audiences> <issuers hint="list"> <issuer>$(insiteIdentityServerAuthority)</issuer> </issuers> </processor> <processor patch:before="*[@type='Sitecore.Owin.Authentication.Pipelines.Initialize.HandlePostLogoutUrl, Sitecore.Owin.Authentication']" mode="on" type="Xc.Foundation.Account.Infrastructure.Pipelines.Initialize.HandlePostLogoutUrl, Xc.Foundation.Account" resolve="true"/> </owin.initialize> <owin.identityProviders> <processor id="InsiteIdentity" type="Xc.Foundation.Account.Infrastructure.Pipelines.IdentityProviders.InsiteIdentity, Xc.Foundation.Account" resolve="true"> <scopes hint="list"> <scope name="openid">openid</scope> <scope name="profile">profile</scope> <scope name="iscapi">iscapi</scope> </scopes> </processor> </owin.identityProviders> </pipelines> <federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication"> <!--Provider mappings to sites--> <identityProvidersPerSites hint="list:AddIdentityProvidersPerSites"> </identityProvidersPerSites> <!--Definitions of providers--> <identityProviders hint="list:AddIdentityProvider"> <identityProvider id="InsiteIdentity" type="Xc.Foundation.Account.Infrastructure.Providers.InsiteIdentityProvider, Xc.Foundation.Account" resolve="true"> <caption>Sign In</caption> <domain>CommerceUsers</domain> <enabled>true</enabled> <triggerExternalSignOut>true</triggerExternalSignOut> <transformations hint="list:AddTransformation"> <transformation name="apply additional claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.ApplyAdditionalClaims, Sitecore.Owin.Authentication.IdentityServer" resolve="true"/> <transformation name="name to long name" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication"> <sources hint="raw:AddSource"> <claim name="preferred_username"/> </sources> <targets hint="raw:AddTarget"> <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"/> </targets> <keepSource>true</keepSource> </transformation> <transformation name="role to long role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication"> <sources hint="raw:AddSource"> <claim name="insite_role"/> </sources> <targets hint="raw:AddTarget"> <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="CommerceUsers\Extranet User"/> </targets> <keepSource>false</keepSource> </transformation> <transformation name="set ShadowUser" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication"> <sources hint="raw:AddSource"> <claim name="http://schemas.microsoft.com/identity/claims/identityprovider" value="local"/> </sources> <targets hint="raw:AddTarget"> <claim name="http://www.sitecore.net/identity/claims/shadowuser" value="true"/> </targets> <keepSource>true</keepSource> </transformation> <!-- owin.cookieAuthentication.signIn pipeline uses http://www.sitecore.net/identity/claims/cookieExp claim to override authentication cookie expiration. 'exp' claim value can be configured on Sitecore Identity server on the client configuration by IdentityTokenLifetimeInSeconds setting. Note: Claim value is Unix time expressed as the number of seconds that have elapsed since 1970-01-01T00:00:00Z --> <transformation name="use exp claim for authentication cookie expiration" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication"> <sources hint="raw:AddSource"> <claim name="exp"/> </sources> <targets hint="raw:AddTarget"> <claim name="http://www.sitecore.net/identity/claims/cookieExp"/> </targets> <keepSource>true</keepSource> </transformation> <!--<transformation name="remove local role claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.RemoveLocalRoles, Sitecore.Owin.Authentication.IdentityServer"/>--> <transformation name="adjust NameIdentifier claim" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.AdjustNameIdentifierClaim, Sitecore.Owin.Authentication.IdentityServer" resolve="true"/> </transformations> </identityProvider> </identityProviders> <!--List of all shared transformations--> <sharedTransformations> </sharedTransformations> <!--Property mappings initializer--> <propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication"> <!--List of property mappings Note that all mappings from the list will be applied to each providers--> <maps hint="list"> <!--The mapping sets the Email property of the user profile from emailaddress claim--> <map name="email claim" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true"> <data hint="raw:AddData"> <!--claim name--> <source name="preferred_username" /> <!--property name--> <target name="Email" /> </data> </map> <map name="first_name" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true"> <data hint="raw:AddData"> <source name="given_name" /> <target name="FirstName" /> </data> </map> <map name="full name" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true"> <data hint="raw:AddData"> <source name="ExternalFullName" /> <target name="FullName" /> </data> </map> <map name="comment" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true"> <data hint="raw:AddData"> <source name="xComment" /> <target name="Comment" /> </data> </map> <map name="role" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true"> <data hint="raw:AddData"> <source name="insite_role" /> <target name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" /> </data> </map> </maps> </propertyInitializer> </federatedAuthentication> </sitecore> </configuration>
Project.XXX.Website.config
In the Project layer, I added the following configurations:<federatedAuthentication role:require="ContentDelivery or Standalone"> <identityProvidersPerSites> <mapEntry name="atccorg" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication"> <sites hint="list"> <site>xxx</site> </sites> <identityProviders hint="list:AddIdentityProvider"> <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='InsiteIdentity']" id="InsiteIdentity" /> </identityProviders> <externalUserBuilder type="Xc.Foundation.Account.Services.InsiteUserBuilder, Xc.Foundation.Account"> <param desc="isPersistentUser">true</param> </externalUserBuilder> </mapEntry> </identityProvidersPerSites> </federatedAuthentication>
InsiteUserBuilder.cs
User builder class had to be overwritten as well:public class InsiteUserBuilder : Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder { public InsiteUserBuilder() { } public InsiteUserBuilder(bool isPersistentUser) : base(isPersistentUser) { } public InsiteUserBuilder(string isPersistentUser) : base(isPersistentUser) { } public override ApplicationUser BuildUser(UserManager<ApplicationUser> userManager, ExternalLoginInfo externalLoginInfo) { ApplicationUser user = this.ApplicationUserFactory.CreateUser(this.CreateUniqueUserName(userManager, externalLoginInfo)); user.IsVirtual = !this.IsPersistentUser; return user; } protected override string CreateUniqueUserName(UserManager<ApplicationUser> userManager, ExternalLoginInfo externalLoginInfo) { IdentityProvider identityProvider = this.FederatedAuthenticationConfiguration.GetIdentityProvider(externalLoginInfo.ExternalIdentity); if (identityProvider == null) { throw new InvalidOperationException("Unable to retrieve an identity provider for the given identity"); } return identityProvider.Domain + "\\" + externalLoginInfo.ExternalIdentity.Name; } }
HandlePostLogoutUrl.cs
public class HandlePostLogoutUrl : InitializeProcessor { private readonly BaseSettings settings; private readonly ExternalLogoutManager externalLogoutManager; public HandlePostLogoutUrl(BaseSettings settings, ExternalLogoutManager externalLogoutManager) { Assert.ArgumentNotNull((object)settings, nameof(settings)); Assert.ArgumentNotNull((object)externalLogoutManager, nameof(externalLogoutManager)); this.settings = settings; this.externalLogoutManager = externalLogoutManager; } public override void Process(InitializeArgs args) { if (!this.settings.FederatedAuthenticationEnabled()) { return; } this.HandlePostExternalLogoutUrl(args); } private void HandlePostExternalLogoutUrl(InitializeArgs args) { args.App.Map( this.settings.IdentityProcessingPathPrefix().EnsureTrailingSlash() + InsiteIdentity.PostLogoutEndPointName, (Action<IAppBuilder>)(app => this.RunUnRegisterExternalLogout(app))); } private void RunUnRegisterExternalLogout(IAppBuilder app) { app.Run((Func<IOwinContext, Task>)(context => { this.UnRegisterExternalLogout(context); return Task.CompletedTask; })); } private void UnRegisterExternalLogout(IOwinContext owinContext) { Assert.ArgumentNotNull((object)owinContext, nameof(owinContext)); owinContext.Response.Cookies.Delete( Sitecore.Owin.Authentication.Constants.ExternalLogOutCookieName, new CookieOptions() { HttpOnly = true, Path = "/" }); string str = owinContext.Request.Query["ReturnUrl"]; if (string.IsNullOrEmpty(str) || WebUtil.IsExternalUrl(str, owinContext.Request.Uri.Host)) { owinContext.Response.Redirect("/"); } owinContext.Response.Redirect(str); } }
Site Definition
In the site definition the following attribute was added:
loginPage="$(loginPath)[xxxsitename]/InsiteIdentity"
Login Button
Controller Rendering:
public ActionResult SignIn() { var url = !string.IsNullOrEmpty(Request.QueryString[Constants.QueryString.Url]) ? HttpUtility.UrlDecode(Request.QueryString[Constants.QueryString.Url]) : Request.RawUrl; var model = new LoginInfo { ReturnUrl = !string.IsNullOrEmpty(Request.QueryString[Constants.QueryString.ReturnUrl]) ? HttpUtility.UrlDecode(Request.QueryString[Constants.QueryString.ReturnUrl]) : url, LoginButton = this.fedAuthLoginRepository.GetAll().FirstOrDefault() }; return this.View(model); }
View:
In the view responsible for rendering of the login button, the following code was added:if (Model.LoginButton != null) { <form action="@Model.LoginButton.Href" method="post" class="form-signin"> <button class="btn btn-block btn-secondary" type="submit"> @if (!string.IsNullOrWhiteSpace(Model.LoginButton.IconClass)) { <i class="@Model.LoginButton.IconClass"></i> } <span> @Model.LoginButton.Caption </span> </button> </form> }
LogOut
Controller action:
[HttpGet] public ActionResult SignOut() { this.accountRepository.Logout(); return this.Redirect("/"); }
In AccountRepository we have the following:
public bool LogOut() { AuthenticationManager.Logout(); return true; }
Above code and configurations force the user to be redirected to Insite Identity Server for authentication and log out.
No comments:
Post a Comment