We have an IIS server that uses an OpenAM Agent for providing SSO. We have the agent configured to protect the /umbraco path of our Umbraco instance. What we aren't sure about is how to get Umbraco to bypass the login form and use the current User in the HttpRequest? How would one programmatically log the current user into their Umbraco user account?
So after a good amount of research and trial-and-error, I have somewhat figured this out and thought I would share with the rest of the community in case anyone ever has a similar requirement.
I ended up rolling my own OwinMiddleware class that checks the OwinContext AuthenticationManager for a current User. If our OpenAM SSO agent successfully authenticates a user then it sets the current user. Then in my invoke method I try to determine if the user has an Umbraco account using the BackOfficeUserManager. If so then I use the BackOfficeSignInManager to sign the user into their Umbraco account.
Here are the classes that tie it all together:
ExampleOwinMiddleware.cs:
using Microsoft.Owin;
using System;
using System.Threading.Tasks;
using Umbraco.Core.Security;
using Umbraco.Web;
namespace Example.Owin {
public class ExampleOwinMiddleware : OwinMiddleware {
public ExampleOwinMiddleware(OwinMiddleware next) : base(next) { }
protected async Task AuthenticateRequestAsync(IOwinContext context) {
if (null == context) {
throw new ArgumentNullException("context");
}
var manager = context.GetBackOfficeSignInManager();
if (null == manager) {
return;
}
var name = context.Authentication.User.Identity.Name;
var user = await manager.UserManager.FindByNameAsync(name);
if (null == user) {
return;
}
await manager.SignInAsync(user, true, false);
}
public override async Task Invoke(IOwinContext context) {
if (RequiresAuthentication(context)) {
await AuthenticateRequestAsync(context);
}
await Next.Invoke(context);
}
protected bool RequiresAuthentication(IOwinContext context) {
if (null == context) {
throw new ArgumentNullException("context");
}
if (!context.Request.Path.StartsWithSegments(new PathString("/umbraco"))) {
return false;
}
if (null != UmbracoContext.Current && null != UmbracoContext.Current.Security.CurrentUser) {
return false;
}
return true;
}
}
}
ExampleOwinStartup.cs:
using Example.Owin;
using Microsoft.Owin;
using Owin;
using Umbraco.Core;
using Umbraco.Core.Security;
using Umbraco.Web;
using Umbraco.Web.Security.Identity;
[assembly: OwinStartup(typeof(ExampleOwinStartup))]
namespace Example.Owin {
public class ExampleOwinStartup : UmbracoDefaultOwinStartup {
public override void Configuration(IAppBuilder app) {
var provider = MembershipProviderExtensions.GetUsersMembershipProvider();
app.ConfigureUserManagerForUmbracoBackOffice(
ApplicationContext.Current, provider.AsUmbracoMembershipProvider()
);
app.UseUmbracoBackOfficeCookieAuthentication(ApplicationContext.Current)
.UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current)
.UseExampleAuthentication();
}
}
}
ExampleExtensions.cs:
using Owin;
namespace Example.Owin {
public static class ExampleExtensions {
public static IAppBuilder UseExampleAuthentication(this IAppBuilder app) {
return app.Use<ExampleOwinMiddleware>();
}
}
}
The one downside to this approach is that this logic tends to run for every path you navigate to within the back office. For some strange reason the UmbracoContext.Current property returns null or the CurrentUser object on the WebSecurity class (UmbracoContext.Current.Security) is null.
Is there a better way of getting the current Umbraco back-office user? Is there a better way to sign the user in? I am setting the persist flag to true on the SignInAsync method and I haven't tried the remember browser flag, would that help out?
That said, you should not be signing the user in on each request. The SignInManager essentially writes a cookie and then the user is authenticated based on that cookie on subsequent requests. What I think you should be doing is either have some custom middleware execute before our cookie auth middleware or you can extend our cookie auth middleware. In either case, what needs to happen is that you determine if valid umbraco auth ticket currently exists, if it does then your code does nothing. If not valid ticket exists, that's when your code executes and it might work to use your SignInManager approach.
If you want to extend our middleware, you might be able to achieve the same outcome with less code. You would essentially want to copy this method. Then you can register your own callback for OnValidateIdentity to perform your custom logic. In there you can check if the context.Identity is not null and if it's null, then you execute your logic. And then in your owin startup execute this copied method instead of the default UseUmbracoBackOfficeCookieAuthentication.
Otherwise if you want to have custom middleware, you'll want to have your middleware execute before ours. You need to know that middleware can be marked to run at certain stages, by default our middleware executes on the PipelineStage.Authenticate stage, see here, that is the default method you are executing. So you should register your middleware before ours and in your UseExampleAuthentication method you should assign the marker like this. That way your middleware should execute first. Then you would check if a back office auth ticket exists in the request using this (or since that is internal, you could use HttpContext.Current and the method above that one). If the ticket doesn't exist and it's a back office request, you would execute your logic. If you use custom middleware for this then ideally you should change your middleware to derive from AuthenticationMiddleware<T>, have a derived AuthenticationHandler to return the appropriate AuthenticationTicket with the UmbracoBackOfficeIdentity (ClaimsIdentity) and Owin will take care of re-assigning the User correctly. You might need to look through some of the katana source code to understand how this all works https://github.com/aspnet/AspNetKatana
For some strange reason the UmbracoContext.Current property returns null or the CurrentUser object on the WebSecurity class (UmbracoContext.Current.Security) is null.
At what stage are you talking about here? UmbracoContext.Current is assigned during the HttpModule.BeginRequest stage, if you are trying to access this in your middleware and it's null it's probably because your middleware is executing before our HttpModule. The CurrentUser will be null in your middleware because that is the point during the request that the user is assigned to the request. Since you are replacing our cookie auth middleware, it's your middlware's responsibility to assign the UmbracoBackOfficeIdentity to the request
Thank you, Shannon, this definitely got me on the right track. And you're correct the UmbracoContext.Current was returning null, because my middleware was executing before it was set. I can't thank you enough for taking the time to provide me with such an in-depth response.
OpenAM IIS Agent
We have an IIS server that uses an OpenAM Agent for providing SSO. We have the agent configured to protect the /umbraco path of our Umbraco instance. What we aren't sure about is how to get Umbraco to bypass the login form and use the current User in the HttpRequest? How would one programmatically log the current user into their Umbraco user account?
So after a good amount of research and trial-and-error, I have somewhat figured this out and thought I would share with the rest of the community in case anyone ever has a similar requirement.
I ended up rolling my own OwinMiddleware class that checks the OwinContext AuthenticationManager for a current User. If our OpenAM SSO agent successfully authenticates a user then it sets the current user. Then in my invoke method I try to determine if the user has an Umbraco account using the BackOfficeUserManager. If so then I use the BackOfficeSignInManager to sign the user into their Umbraco account.
Here are the classes that tie it all together:
ExampleOwinMiddleware.cs:
ExampleOwinStartup.cs:
ExampleExtensions.cs:
The one downside to this approach is that this logic tends to run for every path you navigate to within the back office. For some strange reason the UmbracoContext.Current property returns null or the CurrentUser object on the WebSecurity class (UmbracoContext.Current.Security) is null.
Is there a better way of getting the current Umbraco back-office user? Is there a better way to sign the user in? I am setting the persist flag to true on the SignInAsync method and I haven't tried the remember browser flag, would that help out?
Hi,
I'm not familiar with this OpenAM IIS Agent so can't be much help with regard to that specifically but hopefully can help you out a little bit.
For every back office request (apart from assets) you will need to have an
UmbracoBackOfficeIdentity
user assigned to the request, that is specifically how Umbraco checks for a logged in back office user (among other things). Here's the logic that our cookie manager uses to determine if the request should be authenticated by our own middleware: https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs#L61That said, you should not be signing the user in on each request. The SignInManager essentially writes a cookie and then the user is authenticated based on that cookie on subsequent requests. What I think you should be doing is either have some custom middleware execute before our cookie auth middleware or you can extend our cookie auth middleware. In either case, what needs to happen is that you determine if valid umbraco auth ticket currently exists, if it does then your code does nothing. If not valid ticket exists, that's when your code executes and it might work to use your SignInManager approach.
If you want to extend our middleware, you might be able to achieve the same outcome with less code. You would essentially want to copy this method. Then you can register your own callback for
OnValidateIdentity
to perform your custom logic. In there you can check if thecontext.Identity
is not null and if it's null, then you execute your logic. And then in your owin startup execute this copied method instead of the defaultUseUmbracoBackOfficeCookieAuthentication
.Otherwise if you want to have custom middleware, you'll want to have your middleware execute before ours. You need to know that middleware can be marked to run at certain stages, by default our middleware executes on the
PipelineStage.Authenticate
stage, see here, that is the default method you are executing. So you should register your middleware before ours and in yourUseExampleAuthentication
method you should assign the marker like this. That way your middleware should execute first. Then you would check if a back office auth ticket exists in the request using this (or since that is internal, you could useHttpContext.Current
and the method above that one). If the ticket doesn't exist and it's a back office request, you would execute your logic. If you use custom middleware for this then ideally you should change your middleware to derive fromAuthenticationMiddleware<T>
, have a derivedAuthenticationHandler
to return the appropriateAuthenticationTicket
with theUmbracoBackOfficeIdentity
(ClaimsIdentity) and Owin will take care of re-assigning theUser
correctly. You might need to look through some of the katana source code to understand how this all works https://github.com/aspnet/AspNetKatanaThis sounds a little bit like Azure Easy Auth, for some extra info my blog post about this might provide a little more info for you https://shazwazza.com/post/getting-umbraco-to-work-with-azure-easy-auth/
At what stage are you talking about here?
UmbracoContext.Current
is assigned during the HttpModule.BeginRequest stage, if you are trying to access this in your middleware and it's null it's probably because your middleware is executing before our HttpModule. The CurrentUser will be null in your middleware because that is the point during the request that the user is assigned to the request. Since you are replacing our cookie auth middleware, it's your middlware's responsibility to assign theUmbracoBackOfficeIdentity
to the requestThank you, Shannon, this definitely got me on the right track. And you're correct the
UmbracoContext.Current
was returning null, because my middleware was executing before it was set. I can't thank you enough for taking the time to provide me with such an in-depth response.is working on a reply...