kalle
Nov 18, 2014
  12826
(4 votes)

Using Azure Active Directory as identity provider

EPiServer has now support for federated security and there is a good description of how to get started,http://world.episerver.com/Documentation/Items/Developers-Guide/EPiServer-CMS/75/Security/Federated-Security/

During my presentation at the partner forum, I showed how to use Azure Active Directory to manage authentication for EPiServer, here's the summary.

 

Azure

Open the Azure portal and create a new application in the Active Directory that you want to use for authentication, select the ADD APPLICATION MY ORGANIZATION IS DEVELOPING, enter a name and select WEB APPLICATION AND/OR WEB API.

Add application 1 Add application 2

In the last section, enter the url to your site in the SIGN-ON URL field and a unique url to identify your application in the APP ID URI field,  this is the url that you also should specify in the web.config for your EPiServer site , click the Complete button.

Add application 3

 

Endpoints

Once the application is created, we enter the configuration tab get the metadata url, click on the icon VIEW ENDPOINTS along the bottom to open dialog, highlight the field FEDERATION DOCUMENT METADATA and copy the value, this should also be pasted into the web.config.

Azure Endpoints

Client ID and Secret Key

Before we close the Azure portal, we need to have another couple of settings from the configuration to get it work.

Copy the value from the field CLIENT ID and paste it to your web.config, we must also create a secret key that is used with the client ID when requesting the Azure's Graph API. Choose if your secret key will be valid for a year or two, and then click save, when the application is saved, you can copy the value of the secret key and paste it to the web.config.

Azure Secret Key

The last thing we need to do before we jump over to EPiServer is to set permissions so that the application can request and read the information via the Graph API, select Windows Azure Active Directory application and at least read access for the Application Permissions as well as Read directory data and Enable sign-on and Read users profiles for the Delegated Permissions, click save.

Azure Permissions

 

Update web.config

When all settings from Azure has been copied to your web.config, the following appSettings should be included:

   1: <add key="MetadataAddress" value="https://login.windows.net/c570145b-647d-456c-9a3b-171724a4af74/federationmetadata/2007-06/federationmetadata.xml" />
   2: <add key="Wtrealm" value="https://kalleljungepiserver.onmicrosoft.com/partnerforum" />
   3: <add key="TenantName" value="kalleljungepiserver.onmicrosoft.com" />
   4: <add key="ClientId" value="4181422f-7c7d-452a-a84c-91b03e91a062" />
   5: <add key="ClientSecret" value="sZvD2A8ac5jv4ifD2GfTGLt3uoPrbfr1teh1nQ4wQjU=" />
   6: <add key="GraphUrl" value="https://graph.windows.net" />

Create user groups

Select the GROUPS tab for your directory and click on ADD A GROUP, enter a name and click save, I choose to create a group named WebAdmins since this group has access rights to both EPiServers edit and admin interface as default.

Azure Group

Once the group is created, go in and add the users who will be members.

 

EPiServer

The federated security require OWIN middleware to run, install the following nuget packages.

Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Security.WsFederation
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.Azure.ActiveDirectory.GraphClient
Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory

Retrieve  groups for the authenticated user

One problem with Azure Active Directory is that the groups that user is a member of is not sent with the Claims, we need the groups to be able to set access rights in EPiServer.

Before EPiServer synchronizes the roles of the authenticated user, they must retrieved through a request to the graph API. I created a AzureGraphService that handles the request to Azure and retrieves roles/grupes for the authenticated user and saves it as Claims.

   1: using System;
   2: using System.Configuration;
   3: using System.Linq;
   4: using System.Security.Claims;
   5: using System.Threading.Tasks;
   6: using EPiServer.ServiceLocation;
   7: using Microsoft.Azure.ActiveDirectory.GraphClient;
   8: using Microsoft.IdentityModel.Clients.ActiveDirectory;
   9:  
  10: namespace Federated_Security.Business.Security
  11: {
  12:     [ServiceConfiguration(typeof(AzureGraphService))]
  13:     public class AzureGraphService
  14:     {
  15:         public async Task CreateRoleClaimsAsync(ClaimsIdentity identity)
  16:         {
  17:             // Get the Windows Azure Active Directory tenantId
  18:             var tenantId = identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
  19:  
  20:             // Get the userId
  21:             var currentUserObjectId = identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
  22:  
  23:             var servicePointUri = new Uri(ConfigurationManager.AppSettings["GraphUrl"]);
  24:             var serviceRoot = new Uri(servicePointUri, tenantId);
  25:             var activeDirectoryClient = new ActiveDirectoryClient(serviceRoot,
  26:                 async () => await AcquireTokenAsyncForApplication());
  27:  
  28:             var userResult = await activeDirectoryClient.Users
  29:                 .Where(u => u.ObjectId == currentUserObjectId).ExecuteAsync();
  30:             var currentUser = userResult.CurrentPage.FirstOrDefault() as IUserFetcher;
  31:  
  32:             var pagedCollection = await currentUser.MemberOf.OfType<Group>().ExecuteAsync();
  33:             do
  34:             {
  35:                 var groups = pagedCollection.CurrentPage.ToList();
  36:                 foreach (Group role in groups)
  37:                 {
  38:                     ((ClaimsIdentity)identity).AddClaim(new Claim(ClaimTypes.Role, role.displayName, ClaimValueTypes.String, "AzureGraphService"));
  39:  
  40:                 }
  41:                 pagedCollection = pagedCollection.GetNextPageAsync().Result;
  42:             } while (pagedCollection != null && pagedCollection.MorePagesAvailable);
  43:         }
  44:  
  45:         public async Task<string> AcquireTokenAsyncForApplication()
  46:         {
  47:             var authenticationContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}", 
  48:                 ConfigurationManager.AppSettings["TenantName"]), false);
  49:  
  50:             // Config for OAuth client credentials 
  51:             var clientCred = new ClientCredential(ConfigurationManager.AppSettings["ClientId"], ConfigurationManager.AppSettings["ClientSecret"]);
  52:             var authenticationResult = authenticationContext.AcquireToken(ConfigurationManager.AppSettings["GraphUrl"], clientCred);
  53:             return authenticationResult.AccessToken;
  54:         }
  55:     }
  56: }

Owin configuration

Add a owin startup class and make sure you call the AzureGraphService before EPiServer is synchronizing the roles.

   1: using System;
   2: using System.Security.Claims;
   3: using System.Threading.Tasks;
   4: using System.Web.Helpers;
   5: using System.Configuration;
   6: using Federated_Security.Business.Security;
   7: using Owin;
   8: using Microsoft.Owin;
   9: using Microsoft.Owin.Extensions;
  10: using Microsoft.Owin.Security;
  11: using Microsoft.Owin.Security.Cookies;
  12: using Microsoft.Owin.Security.WsFederation;
  13: using EPiServer.Security;
  14: using EPiServer.ServiceLocation;
  15:  
  16: [assembly: OwinStartup(typeof(Federated_Security.Startup))]
  17:  
  18: namespace Federated_Security
  19: {
  20:     public class Startup
  21:     {
  22:         const string LogoutUrl = "/util/logout.aspx";
  23:  
  24:         public void Configuration(IAppBuilder app)
  25:         {
  26:             // Enable cookie authentication, used to store the claims between requests
  27:             app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);
  28:             app.UseCookieAuthentication(new CookieAuthenticationOptions
  29:             {
  30:                 AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
  31:             });
  32:  
  33:             // Enable federated authentication
  34:             app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
  35:             {
  36:                 // Trusted URL to federation server meta data
  37:                 MetadataAddress = ConfigurationManager.AppSettings["MetadataAddress"],
  38:  
  39:                 // Value of Wtreal must *exactly* match what is configured in the federation server
  40:                 Wtrealm = ConfigurationManager.AppSettings["Wtrealm"],
  41:  
  42:                 Notifications = new WsFederationAuthenticationNotifications()
  43:                 {
  44:                     RedirectToIdentityProvider = (ctx) =>
  45:                     {
  46:                         //  To avoid a redirect loop to the federation server send 403 when user is authenticated but does not have access
  47:                         if (ctx.OwinContext.Response.StatusCode == 401 && ctx.OwinContext.Authentication.User.Identity.IsAuthenticated)
  48:                         {
  49:                             ctx.OwinContext.Response.StatusCode = 403;
  50:                             ctx.HandleResponse();
  51:                         }
  52:                         return Task.FromResult(0);
  53:                     },
  54:                     SecurityTokenValidated = async (ctx) =>
  55:                     {
  56:                         // Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
  57:                         var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
  58:                         if (redirectUri.IsAbsoluteUri)
  59:                         {
  60:                             ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
  61:                         }
  62:  
  63:                         #region Azure
  64:  
  65:                         // Create claims for roles
  66:                         await ServiceLocator.Current.GetInstance<AzureGraphService>().CreateRoleClaimsAsync(ctx.AuthenticationTicket.Identity);
  67:  
  68:                         #endregion
  69:  
  70:                         // Sync user and the roles to EPiServer in the background
  71:                         await ServiceLocator.Current.GetInstance<SynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
  72:                     }
  73:                 }
  74:             });
  75:  
  76:             // Add stage marker to make sure WsFederation runs on Authenticate (before URL Authorization and virtual roles)
  77:             app.UseStageMarker(PipelineStage.Authenticate);
  78:  
  79:             // Remap logout to a federated logout
  80:             app.Map(LogoutUrl, map =>
  81:             {
  82:                 map.Run(ctx =>
  83:                 {
  84:                     ctx.Authentication.SignOut();
  85:                     return Task.FromResult(0);
  86:                 });
  87:             });
  88:  
  89:             // Tell antiforgery to use the name claim
  90:             AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
  91:         }
  92:     }
  93: }

Ready

Start up your website and try to access the EPiServer editor interface, now you should be redirected to the Azure login page.

Nov 18, 2014

Comments

Christer Pettersson
Christer Pettersson Sep 9, 2015 02:05 PM

How did you retrieve the tenantname? I have seemed to missed this part and now recieve a AdalServiceexception No service namespace named '...'

Olav Aukan
Olav Aukan Oct 21, 2016 05:17 PM

So this is a bit old now, and I guess some things have changed. I've set this up on my dev machine and after successfully authenticating in Azure and being sent back to EPiServer I get this error:

IDX10213: SecurityTokens must be signed. SecurityToken: '{0}'.

[SecurityTokenValidationException: IDX10213: SecurityTokens must be signed. SecurityToken: '{0}'.]
   Microsoft.IdentityModel.Tokens.Saml2SecurityTokenHandler.ValidateToken(String securityToken, TokenValidationParameters validationParameters, SecurityToken& validatedToken) +1882
   Microsoft.IdentityModel.Extensions.SecurityTokenHandlerCollectionExtensions.ValidateToken(SecurityTokenHandlerCollection tokenHandlers, String securityToken, TokenValidationParameters validationParameters, SecurityToken& validatedToken) +228
   Microsoft.Owin.Security.WsFederation.d__1f.MoveNext() +3619
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +31
   Microsoft.Owin.Security.WsFederation.d__1f.MoveNext() +5641
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13891908
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Security.Infrastructure.d__0.MoveNext() +822
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13891908
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Security.Infrastructure.d__0.MoveNext() +333
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13891908
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Security.Infrastructure.d__0.MoveNext() +774
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13891908
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Mapping.d__0.MoveNext() +870
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13891908
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.d__5.MoveNext() +203
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13891908
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.d__2.MoveNext() +193
   Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.StageAsyncResult.End(IAsyncResult ar) +96
   System.Web.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +509
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +146

Any idea what's going on here?

Robert L
Robert L Jan 16, 2017 03:29 PM

@Olav

I got the exact same error after signing in with azure AD. After alot of testing and debugging I finally tested downgrading the Microsoft.IdentityModel.Protocol.Extensions package from version 1.0.3.308261200 to 1.0.2.206221351 and it just worked after that.

Please login to comment.
Latest blogs
Optimizely Forms: You cannot submit this form because an administrator has turned off data storage.

Do not let this error message scare you, the solution is quite simple!

Tomas Hensrud Gulla | Oct 4, 2024 | Syndicated blog

Add your own tools to the Optimizely CMS 12 admin menu

The menus in Optimizely CMS can be extended using a MenuProvider, and using the path parameter you decide what menu you want to add additional menu...

Tomas Hensrud Gulla | Oct 3, 2024 | Syndicated blog

Integrating Optimizely DAM with Your Website

This article is the second in a series about integrating Optimizely DAM with websites. It discusses how to install the necessary package and code t...

Andrew Markham | Sep 28, 2024 | Syndicated blog

Opticon 2024 - highlights

I went to Opticon in Stockholm and here are my brief highlights based on the demos, presentations and roadmaps  Optimizely CMS SaaS will start to...

Daniel Ovaska | Sep 27, 2024