EPiServer authentication done with OpenID Connect and IdentityServer

A simple guide and example code for setting up the basics for working with EPiServer, OpenID Connect, and IdentityServer.

IdentityServerSecurityTutorialsEPiServer
IdentityServer and 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 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öm
3// File: Startup.cs
4// ---------------------------------------------------
5
6using 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;
21
22[assembly: OwinStartup(typeof(Startup))]
23
24namespace EPiServer.OicExample
25{
26 public class Startup
27 {
28 private const string UrlLogout = "/util/logout.aspx";
29 private const string UrlLogin = "/login";
30
31 private const string OicClientId = "episerver.hybrid";
32 private const string OicAuthority = "https://localhost:44333/core";
33 private const string OicScopes = "openid roles profile email";
34
35 private const string OicResponseType = "code id_token token";
36 // Used for hybrid flow, for just imlicit flow just use id_token
37
38 private const string OicPostLogoutRedirectUri = "http://localhost:64286/";
39
40 public void Configuration(IAppBuilder app)
41 {
42 app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
43
44 app.UseCookieAuthentication(new CookieAuthenticationOptions());
45 app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
46 {
47 ClientId = OicClientId,
48 Authority = OicAuthority,
49 PostLogoutRedirectUri = OicPostLogoutRedirectUri,
50 ResponseType = OicResponseType,
51 Scope = OicScopes,
52 TokenValidationParameters = new TokenValidationParameters
53 {
54 ValidateIssuer = false,
55 NameClaimType = ClaimTypes.NameIdentifier,
56 RoleClaimType = ClaimTypes.Role
57 },
58 Notifications = new OpenIdConnectAuthenticationNotifications
59 {
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 }
77
78 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;
92
93 ServiceLocator.Current.GetInstance<ISynchronizingUserService>()
94 .SynchronizeAsync(ctx.AuthenticationTicket.Identity);
95
96 return Task.FromResult(0);
97 }
98 }
99 });
100
101 app.UseStageMarker(PipelineStage.Authenticate);
102
103 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 else
110 ctx.Response.Redirect("/");
111 return Task.FromResult(0);
112 });
113 });
114
115 app.Map(UrlLogout, config =>
116 {
117 config.Run(ctx =>
118 {
119 ctx.Authentication.SignOut();
120 return Task.FromResult(0);
121 });
122 });
123
124 // 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öm
3// File: OicSynchronizingUserService.cs
4// ---------------------------------------------------
5
6using System.Collections.Generic;
7using System.Security.Claims;
8using System.Threading.Tasks;
9using EPiServer.Security;
10using EPiServer.ServiceLocation;
11
12namespace EPiServer.OicExample
13{
14 [ServiceConfiguration(typeof(ISynchronizingUserService))]
15 public class OicSynchronizingUserService : ISynchronizingUserService
16 {
17 public Task SynchronizeAsync(ClaimsIdentity identity, IEnumerable<string> additionalClaimsToSync)
18 {
19 // Do sync and mapping here
20 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öm
3// File: Clients.cs
4// ---------------------------------------------------
5
6using System.Collections.Generic;
7using IdentityServer3.Core;
8using IdentityServer3.Core.Models;
9
10namespace IdentityServer.SelfHosted.Config
11{
12 public class Clients
13 {
14 public static List<Client> Get()
15 {
16 return new List<Client>
17 {
18 new Client
19 {
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.Roles
34 },
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 = true
48 }
49 };
50 }
51 }
52}

Users

Here is the in memory users

1// ---------------------------------------------------
2// Copyright 2017 - Johan Boström
3// File: Users.cs
4// ---------------------------------------------------
5
6using System.Collections.Generic;
7using System.Security.Claims;
8using IdentityServer3.Core;
9using IdentityServer3.Core.Services.InMemory;
10
11namespace IdentityServer.SelfHosted.Config
12{
13 internal static class Users
14 {
15 public static List<InMemoryUser> Get()
16 {
17 var users = new List<InMemoryUser>
18 {
19 new InMemoryUser
20 {
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 InMemoryUser
35 {
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 };
50
51 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

Share to: