This post is made to be a simple guide for setting up the basics for working with EPiServer and OpenID Connect, it’s based on 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, let's 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
1Install-Package Microsoft.Owin.Security.OpenIdConnect
Configure OpenID Connect
Configure OpenID Connect in your OWIN startup file Startup.cs
1// ---------------------------------------------------2// Copyright 2017 - Johan Boström3// File: Startup.cs4// ---------------------------------------------------56using System;7using System.IdentityModel.Tokens;8using System.Security.Claims;9using System.Threading.Tasks;10using System.Web;11using EPiServer.OicExample;12using EPiServer.Security;13using EPiServer.ServiceLocation;14using EPiServer.Web;15using Microsoft.Owin;16using Microsoft.Owin.Extensions;17using Microsoft.Owin.Security;18using Microsoft.Owin.Security.Cookies;19using Microsoft.Owin.Security.OpenIdConnect;20using Owin;2122[assembly: OwinStartup(typeof(Startup))]2324namespace EPiServer.OicExample25{26 public class Startup27 {28 private const string UrlLogout = "/util/logout.aspx";29 private const string UrlLogin = "/login";3031 private const string OicClientId = "episerver.hybrid";32 private const string OicAuthority = "https://localhost:44333/core";33 private const string OicScopes = "openid roles profile email";3435 private const string OicResponseType = "code id_token token";36 // Used for hybrid flow, for just imlicit flow just use id_token3738 private const string OicPostLogoutRedirectUri = "http://localhost:64286/";3940 public void Configuration(IAppBuilder app)41 {42 app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);4344 app.UseCookieAuthentication(new CookieAuthenticationOptions());45 app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions46 {47 ClientId = OicClientId,48 Authority = OicAuthority,49 PostLogoutRedirectUri = OicPostLogoutRedirectUri,50 ResponseType = OicResponseType,51 Scope = OicScopes,52 TokenValidationParameters = new TokenValidationParameters53 {54 ValidateIssuer = false,55 NameClaimType = ClaimTypes.NameIdentifier,56 RoleClaimType = ClaimTypes.Role57 },58 Notifications = new OpenIdConnectAuthenticationNotifications59 {60 AuthenticationFailed = context =>61 {62 context.HandleResponse();63 context.Response.Write(context.Exception.Message);64 return Task.FromResult(0);65 },66 RedirectToIdentityProvider = context =>67 {68 if (context.ProtocolMessage.RedirectUri == null)69 {70 var currentUrl = SiteDefinition.Current.SiteUrl;71 context.ProtocolMessage.RedirectUri = new UriBuilder(72 currentUrl.Scheme,73 currentUrl.Host,74 currentUrl.Port,75 HttpContext.Current.Request.Url.AbsolutePath).ToString();76 }7778 if (context.OwinContext.Response.StatusCode == 401 &&79 context.OwinContext.Authentication.User.Identity.IsAuthenticated)80 {81 context.OwinContext.Response.StatusCode = 403;82 context.HandleResponse();83 }84 return Task.FromResult(0);85 },86 SecurityTokenValidated = ctx =>87 {88 var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri,89 UriKind.RelativeOrAbsolute);90 if (redirectUri.IsAbsoluteUri)91 ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;9293 ServiceLocator.Current.GetInstance<ISynchronizingUserService>()94 .SynchronizeAsync(ctx.AuthenticationTicket.Identity);9596 return Task.FromResult(0);97 }98 }99 });100101 app.UseStageMarker(PipelineStage.Authenticate);102103 app.Map(UrlLogin, config =>104 {105 config.Run(ctx =>106 {107 if (ctx.Authentication.User == null || !ctx.Authentication.User.Identity.IsAuthenticated)108 ctx.Response.StatusCode = 401;109 else110 ctx.Response.Redirect("/");111 return Task.FromResult(0);112 });113 });114115 app.Map(UrlLogout, config =>116 {117 config.Run(ctx =>118 {119 ctx.Authentication.SignOut();120 return Task.FromResult(0);121 });122 });123124 // If the application throws an antiforgery token exception like “AntiForgeryToken: A Claim of Type NameIdentifier or IdentityProvider Was Not Present on Provided ClaimsIdentity,”125 // use this:126 // AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;127 }128 }129}
Synchronize user service
This can be used to transform claims
1// ---------------------------------------------------2// Copyright 2017 - Johan Boström3// File: OicSynchronizingUserService.cs4// ---------------------------------------------------56using System.Collections.Generic;7using System.Security.Claims;8using System.Threading.Tasks;9using EPiServer.Security;10using EPiServer.ServiceLocation;1112namespace EPiServer.OicExample13{14 [ServiceConfiguration(typeof(ISynchronizingUserService))]15 public class OicSynchronizingUserService : ISynchronizingUserService16 {17 public Task SynchronizeAsync(ClaimsIdentity identity, IEnumerable<string> additionalClaimsToSync)18 {19 // Do sync and mapping here20 return Task.FromResult(0);21 }22 }23}
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
1// ---------------------------------------------------2// Copyright 2017 - Johan Boström3// File: Clients.cs4// ---------------------------------------------------56using System.Collections.Generic;7using IdentityServer3.Core;8using IdentityServer3.Core.Models;910namespace IdentityServer.SelfHosted.Config11{12 public class Clients13 {14 public static List<Client> Get()15 {16 return new List<Client>17 {18 new Client19 {20 ClientName = "EPiServer Hybrid Client",21 ClientId = "episerver.hybrid",22 Flow = Flows.Hybrid,23 AllowAccessTokensViaBrowser = true,24 ClientSecrets = new List<Secret>25 {26 new Secret("episerver".Sha256())27 },28 AllowedScopes = new List<string>29 {30 Constants.StandardScopes.OpenId,31 Constants.StandardScopes.Email,32 Constants.StandardScopes.Profile,33 Constants.StandardScopes.Roles34 },35 ClientUri = "https://johanbostrom.se",36 RequireConsent = false,37 RedirectUris = new List<string>38 {39 "http://localhost:64286/",40 "http://localhost:64286/episerver",41 "http://localhost:64286/login"42 },43 PostLogoutRedirectUris = new List<string>44 {45 "http://localhost:64286/"46 },47 LogoutSessionRequired = true48 }49 };50 }51 }52}
Users
Here is the in memory users
1// ---------------------------------------------------2// Copyright 2017 - Johan Boström3// File: Users.cs4// ---------------------------------------------------56using System.Collections.Generic;7using System.Security.Claims;8using IdentityServer3.Core;9using IdentityServer3.Core.Services.InMemory;1011namespace IdentityServer.SelfHosted.Config12{13 internal static class Users14 {15 public static List<InMemoryUser> Get()16 {17 var users = new List<InMemoryUser>18 {19 new InMemoryUser20 {21 Subject = "dae962db-f092-4df0-8c6d-34c16ee78c98",22 Username = "admin",23 Password = "admin",24 Claims = new[]25 {26 new Claim(Constants.ClaimTypes.Name, "Leia Organa"),27 new Claim(Constants.ClaimTypes.GivenName, "Leia"),28 new Claim(Constants.ClaimTypes.FamilyName, "Organa"),29 new Claim(Constants.ClaimTypes.Email, "[email protected]"),30 new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),31 new Claim(Constants.ClaimTypes.Role, "Administrators")32 }33 },34 new InMemoryUser35 {36 Subject = "903306c0-45ad-4ed5-904f-8f6c8c95fcf1",37 Username = "editor",38 Password = "editor",39 Claims = new[]40 {41 new Claim(Constants.ClaimTypes.Name, "Carrie Fisher"),42 new Claim(Constants.ClaimTypes.GivenName, "Carrie"),43 new Claim(Constants.ClaimTypes.FamilyName, "Fisher"),44 new Claim(Constants.ClaimTypes.Email, "[email protected]"),45 new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),46 new Claim(Constants.ClaimTypes.Role, "WebEditors")47 }48 }49 };5051 return users;52 }53 }54}
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