Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Alejandro Ocampo 46 posts 278 karma points c-trib
    Jan 11, 2019 @ 14:03
    Alejandro Ocampo
    2

    Azure Active Directory Implementation for Umbraco

    Hi there,

    This post is just something I would like to share since I haven't found any solid guidance on the internet and I think it might be helpful to share it here as always feedback is appreciated if you see something is not secure or can be done better.

    The task was to implement Azure Active Directory (from now on AAD) to validate access to an Umbraco site (Umbraco users or members are NOT involved with this validation).

    This is what I did to achieve this:

    OwinStatup.cs

    using x.Configuration;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Microsoft.Owin;
    using Microsoft.Owin.Security;
    using Microsoft.Owin.Security.Cookies;
    using Microsoft.Owin.Security.OpenIdConnect;
    using Owin;
    using System.Globalization;
    using System.IdentityModel.Tokens;
    using System.Threading.Tasks;
    using Umbraco.Web;
    using Umbraco.Web.Security.Identity;
    
    [assembly: OwinStartup("OwinStartup", typeof(x.OwinStartup))]
    
    namespace x.Authentication
    {
        public class OwinStartup : UmbracoDefaultOwinStartup
        {
            private static readonly string _clientId = ConfigurationHelper.GetValue("azureAd:clientId");
            private static readonly string _tenantId = ConfigurationHelper.GetValue("azureAd:tenantId");
            private static readonly string _aActiveDirectoryInstance = ConfigurationHelper.GetValue("azureAd:AADInstance");
            private static readonly string _redirectUri = ConfigurationHelper.GetValue("azureAd:RedirectUri");
            private static readonly string _logOutUri = ConfigurationHelper.GetValue("azureAd:LogOutUri");
            private readonly string _authority = string.Format(CultureInfo.InvariantCulture, _aActiveDirectoryInstance, _tenantId);
    
            public override void Configuration(IAppBuilder app)
            {
                base.Configuration(app);
                ConfigureAuthentication(app);
                ConfigureWebApiAuthentication(app);
            }
    
            private void ConfigureAuthentication(IAppBuilder app)
            {
                app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
                app.UseCookieAuthentication(new CookieAuthenticationOptions());
    
                app.UseOpenIdConnectAuthentication(
                    new OpenIdConnectAuthenticationOptions
                    {
                        // Sets the ClientId, authority, RedirectUri as obtained from web.config
                        ClientId = _clientId,
                        Authority = _authority,
                        RedirectUri = _redirectUri,
                        // PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
                        PostLogoutRedirectUri = _redirectUri,
                        Scope = "openid profile",//OpenIdConnectScope.OpenIdProfile,
                        // ResponseType is set to request the id_token - which contains basic information about the signed-in user
                        ResponseType = "id_token", //OpenIdConnectResponseType.IdToken,
                        // ValidateIssuer set to false to allow personal and work accounts from any organization to sign in to your application
                        // To only allow users from a single organizations, set ValidateIssuer to true and 'tenant' setting in web.config to the tenant name
                        // To allow users from only a list of specific organizations, set ValidateIssuer to true and use ValidIssuers parameter
                        TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuer = false
                        },
                        // OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
                        Notifications = new OpenIdConnectAuthenticationNotifications
                        {
                            AuthenticationFailed = context =>
                            {
                                context.HandleResponse();
                                context.Response.Redirect("/your-login-page/?message=" + context.Exception.Message);
                                return Task.FromResult(0);
                            }
                        }
                    }
                );
    
                //https://issues.umbraco.org/issue/U4-7213
                //https://shazwazza.com/post/getting-umbraco-to-work-with-azure-easy-auth/
                app
                    .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.PostAuthenticate)
                    .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.PostAuthenticate)
                    .UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.Authorize);
            }
    
            /// <summary>
            /// https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v1-dotnet-webapi
            /// https://our.umbraco.com/forum/extending-umbraco-and-using-the-api/88746-secure-web-api-by-using-bearer-tokens-from-azure-ad
            /// </summary>
            /// <param name="app"></param>
            private void ConfigureWebApiAuthentication(IAppBuilder app)
            {
                var stsDiscoveryEndpoint = $"{_authority}/.well-known/openid-configuration";
                var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
                var config = configManager.GetConfigurationAsync().Result;
    
                app.UseWindowsAzureActiveDirectoryBearerAuthentication(new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                {
                    Tenant = _tenantId,
                    TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidAudience = _tenantId,
                        ValidIssuer = config.Issuer,
                        IssuerSigningTokens = config.SigningTokens.ToList(),
                        RequireSignedTokens = true
                    }
                });
            }
        }
    }
    

    AdAuthorize.cs

    using System.Web.Mvc;
    
    namespace x.Attributes
    {
        public class AdAuthorize : AuthorizeAttribute
        {
            // Set default Unauthorized Page Url here to login
            public string LogInUrl { get; set; } = "/your-login-page/";
    
            protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
            {
                if (filterContext.HttpContext.Request.IsAuthenticated)
                {
                    //do normal process
                    base.HandleUnauthorizedRequest(filterContext);
                }
                else
                {
                    filterContext.Result = new RedirectResult(LogInUrl);
                }
            }
        }
    }
    

    FilterConfig.cs

    using x.Attributes;
    using System.Web.Mvc;
    
    namespace x.Authentication
    {
        public class FilterConfig
        {
            public static void RegisterGlobalFilters(GlobalFilterCollection filters)
            {
                filters.Add(new AdAuthorize());
            }
        }
    }
    

    Global.cs

    namespace x.Web
    {
        public class Global : UmbracoApplication
        {
            protected override void OnApplicationStarting(object sender, EventArgs e)
            {
                FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            }
        }
    }
    

    xSurfaceController.cs

    using Microsoft.Owin.Security;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using Umbraco.Web.Mvc;
    
    namespace x.Controllers.Actions
    {
        public class xSurfaceController : SurfaceController
        {
            [AllowAnonymous]
            public void Login()
            {
                //Send an OpenID Connect sign -in request.
                if (Request.IsAuthenticated)
                {
                    Response.Redirect("/");
                }
                else
                {
                    HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" });
                }
            }
    
            public void LogOut()
            {
                // To sign out the user, you should issue an OpenIDConnect sign out request.
                if (Request.IsAuthenticated)
                {
                    var authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
                    HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
                    Request.GetOwinContext().Authentication.GetAuthenticationTypes();
                }
                else
                {
                    Response.Redirect("/your-login-page/");
                }
            }
        }
    }
    

    YourLoginPageController.cs

    using System.Web.Mvc;
    using Umbraco.Web.Models;
    using Umbraco.Web.Mvc;
    
    namespace x.Controllers.Hijacks
    {
        [AllowAnonymous]
        public class LoginPageController : RenderMvcController
        {
            public override ActionResult Index(RenderModel model)
            {
                return View("~/Views/YourLoginPage.cshtml");
            }
        }
    }
    

    Changes in Web.config

    <add key="owin:appStartup" value="OwinStartup" />
    <add key="AAD:AADInstance" value="https://login.microsoftonline.com/{0}" />   
    <add key="AAD:tenantId" value="ffff-ccccc-rrrrr-tttt-sdfsdfsdfsd" />
    <add key="AAD:clientId" value="ffff-ccccc-rrrrr-tttt-sdfsdfsdfsd" />
    <add key="AAD:RedirectUri" value="http://localhost:12345/" />
    <add key="AAD:LogOutUri" value="http://localhost:12345/your-login-page/" />
    

    and

    <authentication mode="None" />
    

    Using this global filter I'm securing the access to the whole site with the [AdAuthorize] attribute and I'm only allowing the login page with [AllowAnonymous] attribute.

    This was the only way I got to work as I wanted using AAD. The current behavior is:

    • if a user requesting a url is NOT authenticated it will get redirected to /your-login-page/.

    • if a user requesting a url IS authenticated it will let the user continue.

    Hope this helps!

    Thanks,

    Ale

  • John Bergman 429 posts 982 karma points
    Jan 12, 2019 @ 05:33
    John Bergman
    0

    Thanks! Good info :-)

  • Jonas Söderström 5 posts 74 karma points
    Apr 30, 2019 @ 09:01
    Jonas Söderström
    0

    Thank you Alejandro!

    Youre right that there is no solid guidance in this topic. Great that you share this post with the community.

    I have some trouble to get it to work. Would you be able to share the code for "YourLoginPage.cshtml" aswell?

  • Alejandro Ocampo 46 posts 278 karma points c-trib
    May 30, 2019 @ 15:59
    Alejandro Ocampo
    0

    Hi Jonas,

    I just updated the code to support webapi, please check again and see if this works for you.

    YourLoginPage.cshtml is just razor so you can put whatever you want. Just guessing here.... maybe you are asking for this?

        @Html.ActionLink("LoginText","LogInActionName","ControllerName", null, new { @class = "somefancyclass" })
    
  • Jonas Söderström 5 posts 74 karma points
    Jun 03, 2019 @ 11:02
    Jonas Söderström
    0

    I figured it out but thanks anyway!

  • Connie DeCinko 85 posts 212 karma points
    Jun 25, 2019 @ 16:43
    Connie DeCinko
    0

    Trying to follow this. Does your code allow a Member to log in to the website via a custom login page, authenticate to AAD and never see the AAD typical login page? Do they still have to grant permission to your site in AAD?

  • Alejandro Ocampo 46 posts 278 karma points c-trib
    Jun 25, 2019 @ 17:25
    Alejandro Ocampo
    0

    This is redirecting you to a login page yes but this login page just have one button that takes you to the normal AAD login screen. This was done to keep the user under the site context, so if an user hits the site for the first time they will see a login page that looks familiar to them before seeing AAD login page. Does that make sense ?

  • Connie DeCinko 85 posts 212 karma points
    Jun 25, 2019 @ 17:54
    Connie DeCinko
    0

    So I take it there is no way to totally hide the AAD login page?

  • Connie DeCinko 85 posts 212 karma points
    Jul 10, 2019 @ 17:53
    Connie DeCinko
    0

    Is there a copy of this in GitLab so we can see the complete picture? For example, a clean install with the default starter kit does not include the App_Start folder and files within.

  • Connie DeCinko 85 posts 212 karma points
    15 days ago
    Connie DeCinko
    0

    So your logout function logs them out of AAD and Umbraco?

Please Sign in or register to post replies

Write your reply to:

Draft