How do I set up an ADFS integration?

  • Page Owner: Not Set
  • Last Reviewed: 2021-09-01

A client is using on premises Active Director Federated Services (ADFS). How can I authenticate to it?


Answer

Every setup is going to be slightly different but in general:

  1. You need the metadata XML at a URL the site can read it. This may mean hosting it yourself, or having it on their server if the web server can access it.
  2. Install the WsFederation nuget packages.
  3. Create the startup.cs file:
            // If you're logging in exclusively with WsFederation, set it to default.
            app.SetDefaultSignInAsAuthenticationType(WsFederationAuthenticationDefaults.AuthenticationType);

            // You MUST also include cookie auth. Set the type to WsFederationAuthenticationDefaults.AuthenticationType
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
            });

            // Configuration the federation integration
            app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions()
            {
                // URL to Metadata XML. This path must be readable by the server.
                // Nothing in the XML is sensitive info, so host it elsewhere if necessary.
                // You can even self-host it.
                MetadataAddress = $"https://localhost/federation/metadata.xml",

                // This must match exactly what is configured for WT Realm on the server.
                Wtrealm = "https://www.example.com/",

                Notifications = new WsFederationAuthenticationNotifications()
                {
                    RedirectToIdentityProvider = (ctx) =>
                    {
                        // If the site is behind a load balancer, you may need to force HTTPS.
                        var forceHttps = ConfigurationManager.AppSettings["ida:forcehttps"];
                        var currentUrl = ctx.Request.Uri;

                        // You need to set the Reply URL based on what domain the user is on. This is to support multi-tenant
                        // sites.
                        if (forceHttps != null && bool.TryParse(forceHttps, out bool shouldForceHttps) && shouldForceHttps)
                        {
                            ctx.ProtocolMessage.Wreply = new UriBuilder(
                                "https",
                                currentUrl.Host,
                                443).ToString();
                        }
                        else
                        {
                            ctx.ProtocolMessage.Wreply = new UriBuilder(
                                currentUrl.Scheme,
                                currentUrl.Host,
                                currentUrl.Port).ToString();
                        }

                        //To avoid a redirect loop to the federation server send 403 when user is authenticated but does not have access
                        if (ctx.OwinContext.Response.StatusCode == 401 && ctx.OwinContext.Authentication.User.Identity.IsAuthenticated)
                        {
                            ctx.OwinContext.Response.StatusCode = 403;
                            ctx.HandleResponse();
                        }
                        if (ctx.OwinContext.Response.StatusCode == 401 && IsXhrRequest(ctx.OwinContext.Request))
                        {
                            ctx.HandleResponse();
                        }
                        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<ISynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);
                        return Task.FromResult(0);
                    }
                }
            });

#if DEBUG
            // Can be helpful for local debugging
            Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
#endif

            app.UseStageMarker(PipelineStage.Authenticate);
            
            //Remap logout to a federated logout
            app.Map(LogoutUrl, map =>
            {
                map.Run(ctx =>
                {
                    ctx.Authentication.SignOut();
                    ctx.Response.Redirect("/");
                    return Task.FromResult(0);
                });
            });

            // Tell antiforgery to use the name claim. If you don't use the name claim
            // any forms that have anti-XSS tokens may fail.
            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;

Other caveats:

  1. Make sure that any domains that use this auth are configured in the Relying Parties on the ADFS server.
  2. If you see a 401 unauthenticated response when you first deploy this update, you may need to set debug="true" in the web.config, save, then set it back to debug="false". This resets some cached files in ASP.NET and allows OWIN startup to work. This seems to happen when you first add OWIN to a project.

See the Fidelity project for a live example.