For you who want to read an introduction to this, can click here: http://sveinaandahl.blogspot.no/2016/04/how-to-do-identity-and-access.html
Install necessary Nuget Packages
First of all you need to install som nuget packages. Open the Tools > Nuget Package Manager > Package Manager Console and run this commands:
PM> Install-Package Microsoft.Owin.Security.Cookies
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.OpenIdConnect
NOTE
Since the implementaion of the OpenID Connect and OAuth2 protocols usually use JSON web token(JWT), the token format look something like this:
//claims
{
"sub": "svein@knowit.no",
"name": "Svein Aandahl",
"role": "Administator"
}
While the URI in a assertion (claims) in a Saml1.1 token looks like this
Role and Name claim
http://schemas.microsoft.com/ws/2008/06/identity/claims/role
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
Why is this importen to be aware of? The Episerver implementation only support the Saml 1.1 token standard. So when you use the OAuth 2.0 you would need to adjust some of the code to make this work in Episerver.
//claims
{
"sub": "svein@knowit.no",
"name": "Svein Aandahl",
"role": "Administator"
}
While the URI in a assertion (claims) in a Saml1.1 token looks like this
Role and Name claim
http://schemas.microsoft.com/ws/2008/06/identity/claims/role
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
Why is this importen to be aware of? The Episerver implementation only support the Saml 1.1 token standard. So when you use the OAuth 2.0 you would need to adjust some of the code to make this work in Episerver.
Code in Startup.cs
Create a startup.cs file on root and add following code.
using EPiServer.Security; using EPiServer.ServiceLocation; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Owin; using System; using System.Collections.Generic; using System.IdentityModel.Tokens; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using System.Web.Helpers; using IdentityModel.Client; using Microsoft.IdentityModel.Protocols; using Microsoft.Owin.Security.OpenIdConnect; [assembly: OwinStartup(typeof(EPiServer.Templates.Alloy.Startup))] namespace EPiServer.Templates.Alloy { public class Startup { const string LogoutUrl = "/util/logout.aspx"; const string LoginUrl = "/login"; const string clientId = "Client-Name"; const string IdSrvUrl = "https://idsrv.local"; const string ClientUrl = "http://website.local/"; public void Configuration(IAppBuilder app) { app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub"; JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>(); JwtSecurityTokenHandler.OutboundClaimTypeMap = new Dictionary<string, string>(); // Sets the authentication type for the owin middleware app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = "Cookies" }); // Uses OpenIdConnect middleware for authentication app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { ClientId = clientId, Authority = IdSrvUrl + "/core/", RedirectUri = ClientUrl, PostLogoutRedirectUri = ClientUrl, ResponseType = "code id_token token", Scope = "openid email profile offline_access roles", UseTokenLifetime = false, //To avoid that the session expires after 5 min SignInAsAuthenticationType = "Cookies", Notifications = new OpenIdConnectAuthenticationNotifications { AuthorizationCodeReceived = async n => { // use the code to get the access and refresh token var tokenClient = new TokenClient( IdSrvUrl + "/core/connect/token", clientId, "secret"); var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri); // use the access token to retrieve claims from userinfo var userInfoClient = new UserInfoClient( new Uri(IdSrvUrl + "/core/connect/userinfo"), tokenResponse.AccessToken); var userInfoResponse = await userInfoClient.GetAsync(); // create new identity var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType); if (id.IsAuthenticated) { id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims); // Add name claim as http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name var name = id.Claims.Where(x => x.Type == "name").Select(x => x.Value).FirstOrDefault() ?? id.Claims.Where(x => x.Type == "preferred_username").Select(x => x.Value).FirstOrDefault(); id.AddClaim(new Claim(ClaimTypes.Name, name)); // Add all role claims for the user as http://schemas.microsoft.com/ws/2008/06/identity/claims/role IEnumerable<Claim> roles = id.Claims.Where(x => x.Type == "role"); foreach (var role in roles) { id.AddClaim(new Claim(ClaimTypes.Role, role.Value)); } id.AddClaim(new Claim("access_token", tokenResponse.AccessToken)); id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString())); id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken)); id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken)); } n.AuthenticationTicket = new AuthenticationTicket( new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType), n.AuthenticationTicket.Properties); }, RedirectToIdentityProvider = n => { // if signing out, add the id_token_hint if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest) { var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token"); if (idTokenHint != null) { n.ProtocolMessage.IdTokenHint = idTokenHint.Value; } } return Task.FromResult(0); }, SecurityTokenValidated = (ctx) => { //Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute); if (redirectUri.IsAbsoluteUri) { ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery; } //Sync user and the roles to EPiServer in the background ServiceLocator.Current.GetInstance<OAuth2SynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity); return Task.FromResult(0); }, } }); //Remap login app.Map(LoginUrl, map => { map.Run(ctx => { if (ctx.Authentication.User == null || !ctx.Authentication.User.Identity.IsAuthenticated) { ctx.Response.StatusCode = 401; } else { ctx.Response.Redirect("/"); } return Task.FromResult(0); }); }); //Remap logout app.Map(LogoutUrl, map => { map.Run(ctx => { ctx.Authentication.SignOut(); return Task.FromResult(0); }); }); //Tell antiforgery to use the name claim AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name; } } }
To be able to sync roles up from Identity Server when the user login, we need to replace the "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" with "role".
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using EPiServer.Async; using EPiServer.Framework; using EPiServer.Security; using EPiServer.ServiceLocation; namespace TestSite.Business.ServiceLocation { [ServiceConfiguration(typeof(SynchronizingUserService))] public class OAuth2SynchronizingUserService : SynchronizingUserService { public override Task SynchronizeAsync(ClaimsIdentity identity) { Validator.ThrowIfNull("identity", identity); if (!identity.IsAuthenticated) throw new ArgumentException("The identity is not authenticated", nameof(identity)); var name = identity.Name ?? identity.Claims.Single(c => c.Type == "aud").Value; var roles = GetRolesFromClaims(identity); return ServiceLocator.Current.GetInstance<TaskExecutor>().Start(() => SynchronizeUserAndRoles(name, roles)); } private static List<string> GetRolesFromClaims(ClaimsIdentity identity) { return identity.Claims.Where(c => c.Type == "role").Where(c => !string.IsNullOrEmpty(c.Value)).Select(c =>
c.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } } }
Web.config
You need to do some modification in web.config and remove membership provider .
<authentication mode="None"/> <membership> <providers> <clear/> </providers> </membership> <roleManager enabled="false"> <providers> <clear/> </providers> </roleManager> <episerver.framework> <securityEntity> <providers> <add name="SynchronizingProvider" type ="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer"/> </providers> </securityEntity> <virtualRoles addClaims="true"> //existing virtual roles </virtualRoles>
Administration of users
After you have got everything up and running with both episerver and IdentityServer then you need to go into the admin to create a user and add roles. These roles will be populated in Episerver, so you can add them as groups in the Episerver tree.
After you have got everything up and running with both episerver and IdentityServer then you need to go into the admin to create a user and add roles. These roles will be populated in Episerver, so you can add them as groups in the Episerver tree.
To be able to login to Episerver editor you need to add the name and role claim as minimum.
Hi Svein, such a great post!
ReplyDeleteEverything works as expected except this small issue, also documented with EpiServer federated security but no luck.
"401.2 Access Denied when accessing edit mode instead of redirect to identity provider"
I can't figure out why I get 401 response for "/episerver" admin access when I'm not authenicated, instead of redirecting to login page of identity server.
Do you have some solution for this on your mind?
Thanks
Hi Bojan,
DeleteIt's a really easy fix. Just add one line to the CookieAuthenticationOptions:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
LoginPath = new PathString(LoginUrl)
});
Hope you didn't scratch your head for too long about this! :)
PS. Weird if EpiServers own team didn't manage to find the issue. If you look at their standard Startup.cs it looks like this:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(Global.LoginPath),
Provider = new CookieAuthenticationProvider
{
...
}
});
Hi I've implemented the same with my application's client id, client secret and end points which we got after registering our application in Cloud access manager. But don't know what's the issue but its not firing Authorizationcoderecieved section.
ReplyDeletePlease help me out. I can even post my code if you want to have a look at it.
Thanks,
Shanthi