Allows users to log in to a Sitecore 9 site using federated authentication

Sitecore 9 SSO

How to implement federated authentication on sitecore 9 to allow visitors to log in to your site using their google or facebook accounts.

Versions used: Sitecore Experience Platform 9.0 rev. 171219 (9.0 Update-1).

Update/Warning:
Updated code so it passes the IdentityProvider name to the middleware so you can use whatever name you want instead of default one. AuthenticationType = IdentityProvider.Name.

On a previous post I explained how to implement federated authentication on Sitecore 8 (using Okta). Things have changed on sitecore 9 and the implementation is easier than back then.

Sitecore 9 comes with an OWIN implementation to delegate authentication to other providers. Most of the job required to achieve federated authentication is through configuration files. In this post I will outline how to implement federated authentication with Facebook and Google so visitors can log in to your sitecore 9 site. Source code here.

Sitecore will automatically render redirect buttons on the login page if you add your provider to admin and shell sites. As we want our providers to handle the website site instead, we will create a rendering to render the redirect links on the NoAccessUrl page.

The steps to implement federated authentication are:

  • Configure third-party (facebook and google) providers
  • Enable and configure providers
  • Create providers’ processors to map claims received to Sitecore user properties and roles
  • Create rendering to render login links and patch NoAccessUrl setting
  • Set the authentication mode to None in the Web.config
  • Remove the FormsAuthentication module, like: <system.webServer><modules><remove name="FormsAuthentication" />

Configure third-party (facebook and google) providers

Follow the following links to create and configure your applications: facebook and google.

IMPORTANT: Redirects URIs should be set to http://YOURSITE/signin-facebook and http://YOURSITE/signin-google respectively. This url is automatically generated by the middlewares being used here.

Enable and configure providers

First, enable federated authentication and add default services. A configuration sample file comes with sitecore, it sits under: \App_Config\Include\Examples\Sitecore.Owin.Authentication.Enabler.config.example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore role:require="Standalone or ContentDelivery or ContentManagement">
<settings>
<setting name="FederatedAuthentication.Enabled">
<patch:attribute name="value">true</patch:attribute>
</setting>
<setting name="NoAccessUrl">
<patch:attribute name="value">/no-access</patch:attribute>
</setting>
</settings>
<services>
<register serviceType="Sitecore.Abstractions.BaseAuthenticationManager, Sitecore.Kernel"
implementationType="Sitecore.Owin.Authentication.Security.AuthenticationManager, Sitecore.Owin.Authentication"
lifetime="Singleton" />
<register serviceType="Sitecore.Abstractions.BaseTicketManager, Sitecore.Kernel"
implementationType="Sitecore.Owin.Authentication.Security.TicketManager, Sitecore.Owin.Authentication"
lifetime="Singleton" />
<register serviceType="Sitecore.Abstractions.BasePreviewManager, Sitecore.Kernel"
implementationType="Sitecore.Owin.Authentication.Publishing.PreviewManager, Sitecore.Owin.Authentication"
lifetime="Singleton" />
</services>
</sitecore>
</configuration>

Note NoAccessUrl is being set to /no-access. This item needs to exist in sitecore and it’s where the rendering that will generate the login links needs to be added to.

Then, let’s add our providers and transformations. I’ll just show facebook’s identityProvider as google’s is pretty much the same. See full config here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<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>
<processor type="Sitecore9SSO.Pipelines.IdentityProviders.FacebookIdentityProvider, Sitecore9SSO" resolve="true" />
<processor type="Sitecore9SSO.Pipelines.IdentityProviders.GoogleIdentityProvider, Sitecore9SSO" resolve="true" />
</owin.identityProviders>
</pipelines>

<federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
<identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
<mapEntry name="all" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
<sites hint="list">
<site>website</site>
</sites>

<identityProviders hint="list:AddIdentityProvider">
<identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='Facebook']" />
<identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='Google']" />
</identityProviders>

<externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
<param desc="isPersistentUser">false</param>
</externalUserBuilder>
</mapEntry>
</identityProvidersPerSites>

<identityProviders hint="list:AddIdentityProvider">
<identityProvider id="Facebook" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
<param desc="name">$(id)</param>
<param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
<caption>Log in with Facebook</caption>
<icon>/assets/fb.png</icon>
<domain>sitecore</domain>

<transformations hint="list:AddTransformation">
<transformation name="map role to idp" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="idp" value="Facebook" />
</sources>
<targets hint="raw:AddTarget">
<claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="sitecore\Developer" />
</targets>
<keepSource>true</keepSource>
</transformation>

<transformation name="fullname" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
</sources>
<targets hint="raw:AddTarget">
<claim name="FullName" />
</targets>
</transformation>

</transformations>
</identityProvider>

<identityProvider id="Google" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
...
...
</identityProvider>
</identityProviders>

<propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
<!-- global mappings go here -->
</propertyInitializer>
</federatedAuthentication>
</sitecore>
</configuration>

The DefaultExternalUserBuilder class creates a sequence of user names for a given external user name. It then uses the first of these names that does not already exist in Sitecore. The values in the sequence depend only on the external username and the Sitecore domain configured for the given identity provider. 1

If you want to use another claim from the user instead, and you know it’s going to be unique, you can create your own processor and use whatever claim you like. Example below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomExternalUserBuilder : Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder
{
public CustomExternalUserBuilder(bool isPersistentUser)
: base(isPersistentUser)
{
}

public CustomExternalUserBuilder(string isPersistentUser)
: base(bool.Parse(isPersistentUser))
{
}

protected override string CreateUniqueUserName(UserManager<ApplicationUser> userManager, ExternalLoginInfo externalLoginInfo)
{
Assert.ArgumentNotNull(userManager, "userManager");
Assert.ArgumentNotNull(externalLoginInfo, "externalLoginInfo");

return externalLoginInfo.ExternalIdentity.FindFirstValue("FullName");
}
}

Federated authentication supports two types of users:
Persistent users – Sitecore stores information about persistent users (login name, email address, and so on) in the database, and uses the Membership provider by default
Virtual users – information about these users is stored in the session and disappears after the session is over. 2

The transformations above are doing 2 simple things. The first one map role to idp is adding the role sitecore\Developer to the user when its idp claim is equal to Facebook. This claim is added automatically by sitecore because of the shared claim transformation setIdpClaim under <sharedTransformations> in Sitecore.Owin.Authentication.config.

The other one, fullname, is just transforming the claim to FullName so you can retrieve easier programmatically (this is just an example and not actually being used).

The identity provider id must match the IdentityProviderName in your provider processor.

Create providers’ processors to map claims received to Sitecore user properties and roles

Create a processor (per provider) that inherits from IdentityProvidersProcessor and maps the claims received.

This implementation uses middlewares created by Microsoft. Nuget packages: google, facebook. Again, only showing facebook’s implementation as google’s is very similar. You can find full source code here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class FacebookIdentityProvider : IdentityProvidersProcessor
{
protected override string IdentityProviderName => "Facebook";
private const string AppId = "app id from facebook app";
private const string AppSecret = "app secret from facebook app";

public FacebookIdentityProvider(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration)
: base(federatedAuthenticationConfiguration)
{
}

protected override void ProcessCore(IdentityProvidersArgs args)
{
Assert.ArgumentNotNull(args, "args");
IdentityProvider identityProvider = this.GetIdentityProvider();
var provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider
{
OnAuthenticated = (context) =>
{
//map claims
context.Identity.ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, identityProvider));
return Task.CompletedTask;
},
};

var fbAuthOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
AppId = AppId,
AppSecret = AppSecret,
Provider = provider,
AuthenticationType = IdentityProvider.Name
};
args.App.UseFacebookAuthentication(fbAuthOptions);
}
}

The code is straight forward and microsoft’s middleware does most of the work. Once the user is authenticated and sent back to our site, we need to map the claims. We are doing this with sitecore’s ApplyClaimsTransformations helper. This will map the claims and transform them according to the transformations defined in our configuration file.

Do not forget to update AppId and AppSecret.

Create a controller rendering in sitecore and assign its controller to the following. It gets the login providers that have been configure for the site website and return the list of links.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Sitecore9SSO.Controllers
{
using System.Web.Mvc;
using Sitecore.Pipelines.GetSignInUrlInfo;
using Sitecore.Abstractions;

public class LoginLinksController : Controller
{
public ActionResult Index()
{
//get url to redirect to
var url = "/";
if(!string.IsNullOrEmpty(Request.QueryString?["item"]))
url = Request.QueryString["item"];

var corePipelineManager = DependencyResolver.Current.GetService<BaseCorePipelineManager>();
var args = new GetSignInUrlInfoArgs("website", url);
GetSignInUrlInfoPipeline.Run(corePipelineManager, args);

return View("/Views/LoginLinks.cshtml", args.Result);
}
}
}

Create a view to render the links.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@using Sitecore
@using Sitecore.Mvc

@model IEnumerable<Sitecore.Data.SignInUrlInfo>

@foreach (var signIn in Model)
{
using (Html.BeginForm(null, null, FormMethod.Post, new { @action = signIn.Href }))
{
<button type="submit">
<img src="@signIn.Icon" />
@signIn.Caption
</button>
}
}

That’s it. If you remove extranet\anonymous access to an item and allow access to sitecore\developer role, anonymous users will be redirected to no-access page (created above) and the login links will be shown.

Login List

====================
References:
https://doc.sitecore.net/sitecore_experience_platform/developing/developing_with_sitecore/federated_authentication/configure_federated_authentication
https://doc.sitecore.net/sitecore_experience_platform/developing/developing_with_sitecore/federated_authentication/using_federated_authentication_with_sitecore
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins?tabs=aspnetcore2x
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?tabs=aspnetcore2x


Please let me know what you think and/or if you can spot any errors.
/eom

Share