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");
}
}
}
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.
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?
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 ?
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.
My visual studio 2019 gives me error when I try to add this:
using Umbraco.Web.Security.Identity;
but "using Umbraco.Web.Security" is no problem. not the Identity?
I have installed these two nuget packages:
Install-Package IdentityModel -Version 3.0.0
Install-Package Microsoft.Owin.Security.OpenIdConnect -Version 3.1.0
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
AdAuthorize.cs
FilterConfig.cs
Global.cs
xSurfaceController.cs
YourLoginPageController.cs
Changes in Web.config
and
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
Thanks! Good info :-)
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?
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?
I figured it out but thanks anyway!
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?
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 ?
So I take it there is no way to totally hide the AAD login page?
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.
So your logout function logs them out of AAD and Umbraco?
My visual studio 2019 gives me error when I try to add this:
using Umbraco.Web.Security.Identity;
but "using Umbraco.Web.Security" is no problem. not the Identity?
I have installed these two nuget packages: Install-Package IdentityModel -Version 3.0.0 Install-Package Microsoft.Owin.Security.OpenIdConnect -Version 3.1.0
Its Umbraco8.
am I missing something?
Best regards
Hi,
Is the code tested on Umbraco 8 ?
is working on a reply...