Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

kalle
Nov 18, 2014
  8576
(0 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

Please login to comment.
Latest blogs
Optimizely Configured Commerce and Spire CMS - Figuring out Handlers

I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially...

Ritu Madan | Mar 12, 2025

Another console app for calling the Optimizely CMS REST API

Introducing a Spectre.Console.Cli app for exploring an Optimizely SaaS CMS instance and to source code control definitions.

Johan Kronberg | Mar 11, 2025 |

Extending UrlResolver to Generate Lowercase Links in Optimizely CMS 12

When working with Optimizely CMS 12, URL consistency is crucial for SEO and usability. By default, Optimizely does not enforce lowercase URLs, whic...

Santiago Morla | Mar 7, 2025 |

Optimizing Experiences with Optimizely: Custom Audience Criteria for Mobile Visitors

In today’s mobile-first world, delivering personalized experiences to visitors using mobile devices is crucial for maximizing engagement and...

Nenad Nicevski | Mar 5, 2025 |

Unable to view Optimizely Forms submissions when some values are too long

I discovered a form where the form submissions could not be viewed in the Optimizely UI, only downloaded. Learn how to fix the issue.

Tomas Hensrud Gulla | Mar 4, 2025 |

CMS 12 DXP Migrations - Time Zones

When it comes to migrating a project from CMS 11 and .NET Framework on the DXP to CMS 12 and .NET Core one thing you need to be aware of is the...

Scott Reed | Mar 4, 2025