Article guideContents, topics, tags, and RSS
This historical guide connects EPiServer 10 to IdentityServer3 with OpenID Connect. It covers the EPiServer-side OWIN configuration, user synchronization, and the corresponding IdentityServer3 client and test users.
The sample started with an EPiServer 10 Alloy site. It does not describe current Optimizely CMS authentication.
For protocol background, the OpenID Connect specifications are the durable reference.
EPiServer
Install NuGet packages
Install-Package Microsoft.Owin.Security.OpenIdConnect
Configure OpenID Connect
Configure OpenID Connect in the 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
The synchronizing user service can transform and map 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);
}
}
}
The remaining historical code shows the matching IdentityServer3 client and users.
IdentityServer
Client
This was the in-memory client configuration:
// ---------------------------------------------------
// 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
These in-memory users were included for local demonstration only. Never use fixed sample passwords in a deployed identity system.
// ---------------------------------------------------
// 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;
}
}
}
The complete historical sample remains available in zarxor/EPiServer.OidcExample.
For current work, follow Optimizely’s supported OpenID Connect or platform login options and the documentation for your active identity provider. Do not translate this OWIN configuration line by line into a current application.
