I've been struggling to get Federated Authentication working with Sitecore 9 using IdentityServer 3 as the IDP. I've followed the example seen in http://blog.baslijten.com/enable-federated-authentication-and-configure-auth0-as-an-identity-provider-in-sitecore-9-0/ for Auth0, and have converted that to IDS3. But what I experienced was an endless loop between the IDP and Sitecore.
It seems that upon authentication, IdentityServer 3 redirects back to Sitecore, which fails to convert the authentication into a cookie. I'm left with a .nonce cookie instead. Sitecore, not seeing an authenticated user, redirects over to the IDP and this continues until I stop the process.
My IdentityProviderProcessor (with dummy values):
using System.Threading.Tasks;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Sitecore.Diagnostics;
using Sitecore.Owin.Authentication.Configuration;
using Sitecore.Owin.Authentication.Pipelines.IdentityProviders;
using Sitecore.Owin.Authentication.Services;
namespace xx.xxxx.SC.Foundation.Authentication
{
public class IdentityProviderProcessor : IdentityProvidersProcessor
{
public IdentityProviderProcessor(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration) : base(federatedAuthenticationConfiguration)
{
}
/// <summary>
/// Identityprovidr name. Has to match the configuration
/// </summary>
protected override string IdentityProviderName
{
get { return "ids3"; }
}
protected override void ProcessCore(IdentityProvidersArgs args)
{
Assert.ArgumentNotNull(args, "args");
IdentityProvider identityProvider = this.GetIdentityProvider();
string authenticationType = this.GetAuthenticationType();
args.App.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
args.App.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "xxxx",
ClientId = "xxxx",
Scope = "openid profile xxxx",
RedirectUri = "xxxx",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (context) =>
{
var identity = context.AuthenticationTicket.Identity;
foreach (Transformation current in identityProvider.Transformations)
{
current.Transform(identity, new TransformationContext(FederatedAuthenticationConfiguration, identityProvider));
}
var virtualUser = Sitecore.Security.Authentication.AuthenticationManager.BuildVirtualUser("xxxx\\[email protected]", true);
// You can add roles to the Virtual user
virtualUser.Roles.Add(Sitecore.Security.Accounts.Role.FromName("extranet\\MyRole"));
// You can even work with the profile if you wish
virtualUser.Profile.SetCustomProperty("CustomProperty", "12345");
virtualUser.Profile.Email = "[email protected]";
virtualUser.Profile.Name = "My User";
// Login the virtual user
Sitecore.Security.Authentication.AuthenticationManager.LoginVirtualUser(virtualUser);
return Task.FromResult(0);
},
},
});
}
}
}
And my config file:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore role:require="Standalone or ContentDelivery or ContentManagement">
<pipelines>
<owin.identityProviders>
<!-- Processors for coniguring providers. Each provider must have its own processor-->
<processor type="xx.xxxx.SC.Foundation.Authentication.IdentityProviderProcessor, xx.xxxx.SC.Foundation.Authentication" resolve="true" />
</owin.identityProviders>
</pipelines>
<federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
<!--Provider mappings to sites-->
<identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
<!--The list of providers assigned to all sites-->
<mapEntry name="all sites" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
<sites hint="list">
<sites hint="list">
<site>modules_website</site>
<site>website</site>
</sites>
</sites>
<identityProviders hint="list:AddIdentityProvider">
<identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='ids3']" />
</identityProviders>
<externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
<param desc="isPersistentUser">false</param>
</externalUserBuilder>
</mapEntry>
</identityProvidersPerSites>
<!--Definitions of providers-->
<identityProviders hint="list:AddIdentityProvider">
<!--Auth0 provider-->
<identityProvider id="ids3" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
<param desc="name">$(id)</param>
<param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
<!--This text will be showed for button-->
<caption></caption>
<icon></icon>
<!--Domain name which will be added when create a user-->
<domain>sitecore</domain>
<!--list of identity transfromations which are applied to the provider when a user signin-->
<transformations hint="list:AddTransformation">
<!--SetIdpClaim transformation-->
<transformation name="set idp claim" ref="federatedAuthentication/sharedTransformations/setIdpClaim" />
</transformations>
</identityProvider>
</identityProviders>
<sharedTransformations hint="list:AddTransformation">
</sharedTransformations>
</federatedAuthentication>
</sitecore>
</configuration>
Note that the only way I could accomplish this was to create a VirtualUser on validation. Given the near-total lack of documentation on this topic, I'm not sure if this is a needed step, or if something is wrong with the way I've set this up.
For now, the VirtualUser works like a champ and we'll likely keep this in place. I would like to know, however, is the creation of a VirtualUser here required or did I do something wrong?
Thanks for any input.
I see several issues in your overall configuration, but the most important is the first one (and the workaround must be removed of course):
The implementation of the IdentityProvidersProcessor
must contain only a middleware to configure authentication to external provider, like UseOpenIdConnectAuthentication
or UseAuth0Authentication
or UseFacebookAuthentication.
It must not configure the cookie authentication, because it is already done for you in the Sitecore.Owin.Authentication.config:
<pipelines>
...
<owin.initialize>
...
<processor type="Sitecore.Owin.Authentication.Pipelines.Initialize.CookieAuthentication, Sitecore.Owin.Authentication"
resolve="true" patch:before="processor[@method='Authenticate']" />
...
</owin.initialize>
</pipelines>
Note: if you need to handle any of OWIN cookie authentication events, just use corresponding pipeline owin.cookieAuthentication.*
Actions:
UseCookieAuthentication
middleware.string authenticationType = this.GetAuthenticationType();
to set the SignInAsAuthenticationType
property of the OpenIdConnectAuthenticationOptions
object (the authenticationType
variable is unused in your code).Not an issue, but there is an extension method exists in the Sitecore.Owin.Authentication.Extensions namespace that could replace the whole foreach
statement:
notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, identityProvider));
You tried to manually build virtual users and authenticate them, but Sitecore takes care of it, when you fix the first issue.
Action: replace SecurityTokenValidated
handler with:
SecurityTokenValidated = notification =>
{
notification.AuthenticationTicket.Identity
.ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, identityProvider));
return Task.CompletedTask;
}
Another "not an issue, but..": it was the case for the non-RTM Sitecore 9.0 builds, but now you don't need to manually specify the setIdpClaim
transformation for every identity provider. All transformation, that are specified in the federatedAuthentication/sharedTransformations node, are automatically executed for all identity providers.
Action: remove extra transformation
<!--SetIdpClaim transformation-->
<transformation name="set idp claim" ref="federatedAuthentication/sharedTransformations/setIdpClaim" />
Make sure you have a proper value in the RedirectUri
property. It must be presented in the RedirectUris
property of the appropriate IdentityServer3.Core.Models.Client object.
When everything is done, your external user will be authenticated, but it will not have any assigned roles yet. User has assigned roles when any of these is true:
The best way to assign role claims is to use the claim transformations.
Example: assume that you want to assign a sitecore\Developer role to all Azure AD users that are included in the group with an object id 3e12be6e-58af-479a-a4dc-7a3d5ef61c71. The claim transformation for the AzureAD identity provider will look like this:
<transformation name="developer role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="groups" value="3e12be6e-58af-479a-a4dc-7a3d5ef61c70" />
</sources>
<targets hint="raw:AddTarget">
<claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="sitecore\Developer " />
</targets>
</transformation>
Important note: AzureAD does not send back the group claims by default. You need to set the value of groupMembershipClaims
to the SecurityGroup
in the app manifest.
Now your user has roles, but it's profile is not filled up. Unlike the claim transformations, the property mappings configuration is shared between all identity providers. The general idea behind that is to apply personalized claim transformations for different identity providers and receive the "normalized" ClaimsIdentity with claim types that you expect to see.
For instance, first provider gives you "second name" claim, second one gives "surname" and the third one "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname". Then you write two claim transformations for corresponding providers that maps "second name" to "surname" and "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" to "surname" as well:
in the fist provider config node:
<transformation name="surname" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="second name" />
</sources>
<targets hint="raw:AddTarget">
<claim name="surname" />
</targets>
</transformation>
in the second provider config node:
<transformation name="surname" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
</sources>
<targets hint="raw:AddTarget">
<claim name="surname" />
</targets>
</transformation>
Since now, you have a normalized ClaimsIdentity, you can write one property mapping:
<map name="surname" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
<data hint="raw:AddData">
<source name="surname" />
<target name="Surname" />
</data>
</map>
the user profile data could be read with user.Profile["Surname"]
.
Note: it's possible and easy to implement custom claim transformations and property mappings if you need them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With