EPiServer authentication done with OpenID Connect and IdentityServer

IdentityServer + OpenID Connect <3 EPiServer

This post is made to be a simple guide for setting up the basics for working with EPiServer and OpenID Connect, it's based of this guide over at EPiServer World. There will be a few steps about IdentityServer3 as well but not a full setup guide, for that I recommend checking out the documentation.

When I made this post I used EPiServer 10 and started out with an Alloy site.

Well lets get to it!

I can also recommend having a look at this talk about OpenID Connect and OAuth2 by Dominick Baier who is one of the creators behind IdentityServer, if you want to learn more about OpenID Connect.

EPiServer

Install nuget packages

Install-Package Microsoft.Owin.Security.OpenIdConnect

Configure OpenID Connect

Configure OpenID Connect in your OWIN startup file Startup.cs

// ---------------------------------------------------
// Copyright 2017 - Johan Boström
// File: Startup.cs
// ---------------------------------------------------
 
using System;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using EPiServer.OicExample;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
 
[assembly: OwinStartup(typeof(Startup))]
 
namespace EPiServer.OicExample
{
	public class Startup
	{
		private const string UrlLogout = "/util/logout.aspx";
		private const string UrlLogin = "/login";
 
		private const string OicClientId = "episerver.hybrid";
		private const string OicAuthority = "https://localhost:44333/core";
		private const string OicScopes = "openid roles profile email";
 
		private const string OicResponseType = "code id_token token";
		// Used for hybrid flow, for just imlicit flow just use id_token
 
		private const string OicPostLogoutRedirectUri = "http://localhost:64286/";
 
		public void Configuration(IAppBuilder app)
		{
			app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
 
			app.UseCookieAuthentication(new CookieAuthenticationOptions());
			app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
			{
				ClientId = OicClientId,
				Authority = OicAuthority,
				PostLogoutRedirectUri = OicPostLogoutRedirectUri,
				ResponseType = OicResponseType,
				Scope = OicScopes,
				TokenValidationParameters = new TokenValidationParameters
				{
					ValidateIssuer = false,
					NameClaimType = ClaimTypes.NameIdentifier,
					RoleClaimType = ClaimTypes.Role
				},
				Notifications = new OpenIdConnectAuthenticationNotifications
				{
					AuthenticationFailed = context =>
					{
						context.HandleResponse();
						context.Response.Write(context.Exception.Message);
						return Task.FromResult(0);
					},
					RedirectToIdentityProvider = context =>
					{
						if (context.ProtocolMessage.RedirectUri == null)
						{
							var currentUrl = SiteDefinition.Current.SiteUrl;
							context.ProtocolMessage.RedirectUri = new UriBuilder(
								currentUrl.Scheme,
								currentUrl.Host,
								currentUrl.Port,
								HttpContext.Current.Request.Url.AbsolutePath).ToString();
						}
 
						if (context.OwinContext.Response.StatusCode == 401 &&
							context.OwinContext.Authentication.User.Identity.IsAuthenticated)
						{
							context.OwinContext.Response.StatusCode = 403;
							context.HandleResponse();
						}
						return Task.FromResult(0);
					},
					SecurityTokenValidated = ctx =>
					{
						var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri,
							UriKind.RelativeOrAbsolute);
						if (redirectUri.IsAbsoluteUri)
							ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
 
						ServiceLocator.Current.GetInstance<ISynchronizingUserService>()
							.SynchronizeAsync(ctx.AuthenticationTicket.Identity);
 
						return Task.FromResult(0);
					}
				}
			});
 
			app.UseStageMarker(PipelineStage.Authenticate);
 
			app.Map(UrlLogin, config =>
			{
				config.Run(ctx =>
				{
					if (ctx.Authentication.User == null || !ctx.Authentication.User.Identity.IsAuthenticated)
						ctx.Response.StatusCode = 401;
					else
						ctx.Response.Redirect("/");
					return Task.FromResult(0);
				});
			});
 
			app.Map(UrlLogout, config =>
			{
				config.Run(ctx =>
				{
					ctx.Authentication.SignOut();
					return Task.FromResult(0);
				});
			});
 
			// If the application throws an antiforgery token exception like “AntiForgeryToken: A Claim of Type NameIdentifier or IdentityProvider Was Not Present on Provided ClaimsIdentity,” 
			// use this:
			// AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
		}
	}
}

Synchronize user service

This can be used to transform claims

// ---------------------------------------------------
// Copyright 2017 - Johan Boström
// File: OicSynchronizingUserService.cs
// ---------------------------------------------------
 
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using EPiServer.Security;
using EPiServer.ServiceLocation;
 
namespace EPiServer.OicExample
{
	[ServiceConfiguration(typeof(ISynchronizingUserService))]
	public class OicSynchronizingUserService : ISynchronizingUserService
	{
		public Task SynchronizeAsync(ClaimsIdentity identity, IEnumerable<string> additionalClaimsToSync)
		{
			// Do sync and mapping here
			return Task.FromResult(0);
		}
	}
}
That is it for the setup in EPiServer down below follows som example code on what parameters I did setup in IdentityServer

IdentityServer

Client

Here is the in memory client list i setup

// ---------------------------------------------------
// Copyright 2017 - Johan Boström
// File: Clients.cs
// ---------------------------------------------------
 
using System.Collections.Generic;
using IdentityServer3.Core;
using IdentityServer3.Core.Models;
 
namespace IdentityServer.SelfHosted.Config
{
	public class Clients
	{
		public static List<Client> Get()
		{
			return new List<Client>
			{
				new Client
				{
					ClientName = "EPiServer Hybrid Client",
					ClientId = "episerver.hybrid",
					Flow = Flows.Hybrid,
					AllowAccessTokensViaBrowser = true,
					ClientSecrets = new List<Secret>
					{
						new Secret("episerver".Sha256())
					},
					AllowedScopes = new List<string>
					{
						Constants.StandardScopes.OpenId,
						Constants.StandardScopes.Email,
						Constants.StandardScopes.Profile,
						Constants.StandardScopes.Roles
					},
					ClientUri = "https://johanbostrom.se",
					RequireConsent = false,
					RedirectUris = new List<string>
					{
						"http://localhost:64286/",
						"http://localhost:64286/episerver",
						"http://localhost:64286/login"
					},
					PostLogoutRedirectUris = new List<string>
					{
						"http://localhost:64286/"
					},
					LogoutSessionRequired = true
				}
			};
		}
	}
}

Users

Here is the in memory users

// ---------------------------------------------------
// Copyright 2017 - Johan Boström
// File: Users.cs
// ---------------------------------------------------
 
using System.Collections.Generic;
using System.Security.Claims;
using IdentityServer3.Core;
using IdentityServer3.Core.Services.InMemory;
 
namespace IdentityServer.SelfHosted.Config
{
	internal static class Users
	{
		public static List<InMemoryUser> Get()
		{
			var users = new List<InMemoryUser>
			{
				new InMemoryUser
				{
					Subject = "dae962db-f092-4df0-8c6d-34c16ee78c98",
					Username = "admin",
					Password = "admin",
					Claims = new[]
					{
						new Claim(Constants.ClaimTypes.Name, "Leia Organa"),
						new Claim(Constants.ClaimTypes.GivenName, "Leia"),
						new Claim(Constants.ClaimTypes.FamilyName, "Organa"),
						new Claim(Constants.ClaimTypes.Email, "[email protected]"),
						new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
						new Claim(Constants.ClaimTypes.Role, "Administrators")
					}
				},
				new InMemoryUser
				{
					Subject = "903306c0-45ad-4ed5-904f-8f6c8c95fcf1",
					Username = "editor",
					Password = "editor",
					Claims = new[]
					{
						new Claim(Constants.ClaimTypes.Name, "Carrie Fisher"),
						new Claim(Constants.ClaimTypes.GivenName, "Carrie"),
						new Claim(Constants.ClaimTypes.FamilyName, "Fisher"),
						new Claim(Constants.ClaimTypes.Email, "[email protected]"),
						new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
						new Claim(Constants.ClaimTypes.Role, "WebEditors")
					}
				}
			};
 
			return users;
		}
	}
}

You can find all the example code at my github repository EPiServer.OidcExample

I'm going to try to keep this as up to date as possible, and try to add data about the user and fetch from /connect/userinfo

Here are some more good videos to help understanding what's going on:

OWIN/Katana: Brock Allen - OWIN and Katana: What the Func?

IdentityServer: Introduction to IdentityServer - Brock Allen

Share post
About Johan Boström

I'm a system architect, developer and solution expert, with experience in web-, application-, server- and windows service development with main focus on .NET / C#. Special skills with many kinds of CMS-tools, like EPiServer, Umbraco and Litium Studio.

Comments

Related posts

identityserver3-starterkit-header

Part one i a small guide-series in creating a starter kit for IdentityServer 3 with ASP.NET Identity and Entity Framework for persistent data. In this part I will be focusing on setting up the basics, and configuring IdentityServer and ASP.NET Identity.