Use IdentityServer to Implement a Custom Sitefinity Identity Provider
In this post, I detail the steps to implement a custom external identity provider in Sitefinity. At the end, you will have a Sitefinity instance configured as a client to a separate IdentityServer application. When a user authenticates to IdentityServer the user will be authenticated to Sitefinity and assigned the 'User' role. This uses the OpenID Connect standard. I do not assume there is a pre-configured IdentityServer application already running. I will cover both the IdentityServer setup and Sitefinity setup. his post uses Sitefinity 11 and IdentityServer 4. The software required for this tutorial is Visual Studio 2017, SQL Server Express, .NET framework 4.7.1, dotnet core 2.0, and git (Optional).
Install Applications
The first step is to download the required applications. We need a running instance of Sitefinity 11. The trial can be downloaded from https://www.sitefinity.com/try-now. This Sitefinity Documentation details how to create a new project using the Sitefinity Project Manager. Once the site is running, follow the onscreen prompts to configure the database and default admin user. Once it is created, open the csproj file in Visual Studio and save the solution. Then build and run it out of Visual Studio. This does two things. The first is we are going to make code changes to the Sitefinity project later in the post so it is a good idea to get it building now. The second is when launched from Visual Studio the port number changes. The port number will be important when it comes to configuring the Open ID Connect settings. It is a good idea to make a note of the port number now, in case it is different from mine.
Once you have the trial sitefinity site up and running, we need to setup an IdentityServer instance. Don't worry the IdentityServer GitHub page has examples. To get started, clone or download the examples. We are interested in the Implicit Flow Authentication quickstart example. IdentityServer has accompanying documentation for using this quickstart here.
IdentityServer Authentication Flow
The next step is to make sure we have IdentityServer running correctly. This is the same authentication flow that will take place from Sitefinity, once we set it up. Open the "3_ImplicitFlowAuthentication" sample in Visual Studio and start both the
QuickstartIdentityServer
and the MvcClient
projects. Click on the Secure menu item of the MvcClient
app.

This redirects to the login page of IdentityServer. You can tell because the client app's url is
http://localhost:5002
and the login url is http://localhost:5000/account/login
with a bunch of stuff after it.

The IdentityServer example keeps all data in memory. The clients and users can be found in
Config.cs
. There are 2 users setup by default alice and bob. Both use the password, password. Enter credentials for one of them and click login. You are presented with a standard client authorization page. This page is asking if it is OK for the client "MVC Client" to have access to the user's data. Click "Yes, Allow".

Finally, you are redirected back to the original page you were trying to access. In this case,
http://localhost:5002/Home/Secure
. It displays user data the mvc client application knows about the user.

Alice is now successfully authenticated to the client app through the IdentityServer app. This is possible because the client app is explicitly setup as a client in the identity service. In the next step we will add Sitefinity as another client that can authenticate through our IdentityServer.
Sitefintiy as an IdentityServer client
At this point we have Sitefinity and IdentityServer setup. Now we can make them talk to each other. First, we will add sitefinity as a client in IdentityServer. As mentioned earlier, data in the example is kept in
Config.cs
. There is a statically defined list of clients in public static IEnumerable<Client> GetClients()
. We will add sfcustom
to this list as detailed below.
new Client
{
ClientId = "sfcustom",
ClientName = "sfcustom",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:60877/Sitefinity/Authenticate/OpenID/signin-custom" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
}
}
There are a couple important points about the code snippet above. The
RedirectUris
property needs to go to your local Sitefinity instance. You may need to change the port number on the host name. Furthermore, it needs to point to the sitefinity signin url which is /Sitefinity/Authenticate/OpenID/<custom_identifier>
. <custom_identifier>
can be anything as long as it is unique among your custom providers in Sitefinity. Since this is the first one we're setting up, we should be ok.
Sitefinity also requires email as an identity resource so we need to add
new IdentityResources.Email()
to the GetIdentityResources()
method in Config.cs
. It is also defined in the allowed scopes above. The full method is below.
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
One last change is required to the users defined in IdentityServer. We need to add
email
as a claim on our users. Go to the section of Config.cs
where Alice and Bob are defined and add an email
claim to each like this new Claim("email", "bob@test.com")
and new Claim("email", "alice@test.com").
Finally, Sitefinity supports claims for given_name
and family_name
so to make these users show up a little better in Sitefinity, we're going to remove the name claim and add these. The full config.cs
file is listed below.
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;
namespace QuickstartIdentityServer
{
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
},
new Client
{
ClientId = "sfcustom",
ClientName = "sfcustom",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:60877/Sitefinity/Authenticate/OpenID/signin-custom" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
}
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>
{
new Claim("given_name", "Alice"),
new Claim("nickname", "Alice"),
new Claim("website", "https://alice.com"),
new Claim("email", "alice@test.com"),
new Claim ("family_name", "Taylor")
}
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>
{
new Claim("given_name", "Robert"),
new Claim("nickname", "Bob"),
new Claim("website", "https://bob.com"),
new Claim("email", "bob@test.com"),
new Claim("family_name", "Smith")
}
}
};
}
}
}
Sitefinity Setup
The final piece of the puzzle is to setup Sitefinity to be a client of our IdentityServer. The biggest challenge is to get all of the settings exactly correct to make this work. The first step is to configure Sitefinity to be a client based on the settings entered in IdentityServer. To do this, launch Sitefinity from Visual Studio or the Sitefinity project manager and login as the admin user you created when you set up the site. Go to Administration > Settings > Advanced Settings. Expand the Authentication menu on the left side. Expand SecurityTokenService and click on AuthenticationProviders.
Click the Create New button on the right side of the screen and choose
AuthenticationProviderElement
. Below are the initial settings for the custom provider. Fill these out and click Save Changes.
The trickiest part of the setup is to get all of the parameters correct and they have to match our client setup in IdentityServer we created earlier. Each parameter name and value is case sensitive and must match exactly for this to work. I’m going to go over each one in detail.
Create a new parameter with key
clientid
and value sfcustom
. This must match the clientid we setup in IdentityServer for the Sitefinity client.
Create a new parameter with key
issuer
and value http://localhost:5000/
This is the url to our IdentityServer instance. Make sure the port matches the port of your IdentityServer instance that we setup earlier in this post.
Create a new parameter with key
redirectUri
and value http://localhost:60877/Sitefinity/Authenticate/OpenID/signin-custom
. This must match the RedirectUris property we setup in IdentityServer for the Sitefinity client. Again, double check the port number.
Create a new parameter with key
responseType
and value id_token
.
Create a new parameter with key
scope
and value openid profile email
. This must match the scope setup in the client in IdentityServer.
Create a new paramter with key
caption
and value LoginCustom
. This value actually doesn’t matter. It’s just the text shown on the login button.
The next step is to create a custom initializer to initialize our external identity provider at Sitefinity Startup. This involves creating a class that inherits from the
AuthenticationProvidersInitializer
class. Open the Sitefinity app in Visual Studio. Create a new class called AuthenticationProvidersInitializerExtender
. This Sitefinity article provides the class and how to register it. For completeness I have also included these below. You can copy and paste these directly into your project. The name CustomSTS
must match the Name configured in the custom authentication provider settings above.
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Telerik.Sitefinity.Authentication;
using Telerik.Sitefinity.Authentication.Configuration;
using Telerik.Sitefinity.Authentication.Configuration.SecurityTokenService.ExternalProviders;
using Telerik.Sitefinity.Configuration;
using Telerik.Sitefinity.Security.Claims;
public class AuthenticationProvidersInitializerExtender : AuthenticationProvidersInitializer
{
public override Dictionary<string, Action<IAppBuilder, string, AuthenticationProviderElement>> GetAdditionalIdentityProviders()
{
var providers = base.GetAdditionalIdentityProviders();
// 'CustomSTS' is the name of the external authentication provider as configured in the Advanced settings
providers.Add("CustomSTS", (IAppBuilder app, string signInAsType, AuthenticationProviderElement providerConfig) =>
{
var clientId = providerConfig.GetParameter("clientId");
var issuer = providerConfig.GetParameter("issuer").Trim('/');
var redirectUri = providerConfig.GetParameter("redirectUri");
var responseType = providerConfig.GetParameter("responseType");
var scope = providerConfig.GetParameter("scope");
var caption = providerConfig.GetParameter("caption");
var localStsRelativePath = Config.Get<AuthenticationConfig>().SecurityTokenService.ServicePath.Trim('/');
var options = new OpenIdConnectAuthenticationOptions()
{
ClientId = clientId,
Authority = issuer + "/",
AuthenticationType = providerConfig.Name,
SignInAsAuthenticationType = signInAsType,
RedirectUri = redirectUri,
ResponseType = responseType,
Scope = scope,
Caption = caption,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = n => this.SecurityTokenValidatedInternal(n),
}
};
app.UseOpenIdConnectAuthentication(options);
});
return providers;
}
private Task SecurityTokenValidatedInternal(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var identity = notification.AuthenticationTicket.Identity;
var externalUserEmail = identity.FindFirst("email");
if (externalUserEmail != null)
identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserEmail, externalUserEmail.Value));
var externalUserId = identity.FindFirst("sub");
if (externalUserId != null)
identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserId, externalUserId.Value));
var externalUserFirstName = identity.FindFirst("given_name") != null ? identity.FindFirst("given_name").Value : string.Empty;
var externalUserFamilyName = identity.FindFirst("family_name") != null ? identity.FindFirst("family_name").Value : string.Empty;
var externalUserFullName = externalUserFirstName + " " + externalUserFamilyName;
identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserName, externalUserFullName));
var externalUserNickName = identity.FindFirst("nickname") != null ? identity.FindFirst("nickname").Value : string.Empty;
identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserNickName, externalUserNickName));
var externalUserPicture = identity.FindFirst("picture");
if (externalUserPicture != null)
identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserPictureUrl, externalUserPicture.Value));
return Task.FromResult(0);
}
}
We also need to register our AuthenticationProvidersInitializerExtender
class in the Global.asax file. Create a new Global.asax file because one isn't created by default with a new Sitefinity project. Copy and paste the code below.
using System;
using Telerik.Microsoft.Practices.Unity;
using Telerik.Sitefinity.Abstractions;
using Telerik.Sitefinity.Authentication;
using Telerik.Sitefinity.Services;
namespace SitefinityWebApp
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
SystemManager.ApplicationStart += SystemManager_ApplicationStart;
}
private void SystemManager_ApplicationStart(object sender, EventArgs e)
{
// Register the backend logic for the new external provider
ObjectFactory.Container.RegisterType<AuthenticationProvidersInitializer, AuthenticationProvidersInitializerExtender>(new ContainerControlledLifetimeManager());
}
}
}
Put It All Together
We are ready to test Sitefinity. From Visual Studio, build and start theQuickstartIdentityServer
project and build and start the Sitefinity site. Navigate to /sitefinity
and click the LoginCustom
button. (Or whatever value you assigned to the caption
paramter.) If you don't see the button, you must rebuild and restart the site after making the configuration changes above.
The login flow looks identical to the one for
MvcClient
above. Use one of the users we configured in IdentityServer, either Alice or Bob.
After successfully logging in and accepting the permissions for the
sfcustom
client, you will be redirected to /sitefinity
. This will display a message that the user doesn't have permission. This is because we only assigned the User
role which can't access the backend by default.
However, this verifies that sitefinity knows who the user is. Login as the default administrator and view the users. The user name, role, and email will be in the list.
If you made it this far, we have an IdentityServer with a Sitefinity client. You can configure IdentityServer any way you want to control user accounts. You can also create content in Sitefinity using role based authorization tied to the external users.
That concludes how to setup Sitefinity to be a client of IdentityServer. The high level steps are: setup a basic IdentityServer, setup a basic Sitefinity website, create a custom client in both IdentityServer and Sitefinity that have matching properties, and create a custom
AuthenticationProvidersInitializer
to initialize the custom provider at Sitefinity startup. There are more steps to implement a fully functioning user management system but I hope this guide helps you figure out the initial settings to get up and running with IdentityServer and Sitefinity.