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