U4-7032 - 7.3.0 Custom backoffice login provider requires documentation + simpler implementation extensibility

Created by Kevin Giszewski 27 Aug 2015, 16:27:07 Updated by Asbjørn Riis-Knudsen 03 Oct 2016, 13:07:02

Relates to: U4-7190

Relates to: U4-7907

Relates to: U4-4808

Membership providers are no longer used for authentication, this is done by ASP.Net Identity.

We require documentation on how to create a custom authentication - used purely to check a username and password against a custom data source.

Currently to do this a custom UserManager is required, we should make this a lot simpler to override which could easily be done by specifying a callback so a custom UserManager class is not required (though this would of course still work... as per below). We also need to modify the ctor for the current UserManager so that it's easier for developers to inherit from this class.

1 Attachments

Download Umbraco OWIN LDAP Authentication.dll

Comments

Kevin Giszewski 27 Aug 2015, 16:30:55

Curious if this line in my provider is the issue:

var backofficeUser = ApplicationContext.Current.Services.UserService.GetProfileByUserName(username);

If the User/Pwd is valid, a check to see if the user exists in Umbraco is necessary. Otherwise anyone with a valid user/pwd is allowed in. i.e. check authorization not just authentication.


Kevin Giszewski 27 Aug 2015, 16:40:23

Post upgrade, the custom user membership provider is no longer working. I've re-enabled it and get the following exception:

 2015-08-27 12:42:13,480 [P6868/D16/T27] ERROR Umbraco.Core.Security.AuthenticationExtensions - The current identity cannot be converted to Umbraco.Core.Security.UmbracoBackOfficeIdentity
System.InvalidOperationException: Cannot create a Umbraco.Core.Security.UmbracoBackOfficeIdentity from System.Security.Claims.ClaimsIdentity since the required claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier is missing
   at Umbraco.Core.Security.UmbracoBackOfficeIdentity.FromClaimsIdentity(ClaimsIdentity identity)
   at Umbraco.Core.Security.AuthenticationExtensions.GetCurrentIdentity(HttpContextBase http, Boolean authenticateRequestIfNotFound)


Kevin Giszewski 27 Aug 2015, 16:46:53

Also despite switching back to the custom provider, the users configured in the backoffice still validate (and they didn't before).


Kevin Giszewski 31 Aug 2015, 12:08:41

Ok, tried this on a fresh install of 7.3RC and it fails as well.

The method never gets called: public override bool ValidateUser(string username, string password){}

It got called prior to this. It's as-if the override is failing since the public override int MaxInvalidPasswordAttempts() does get called still.

Only thing in the log at the moment is this: Umbraco.Core.Security.BackOfficeSignInManager login failed message.


Kevin Giszewski 31 Aug 2015, 15:12:31

I want to clarify that there are two issues. If you are using a custom provider when you try to upgrade, it'll fail. If you try to use a clean install, it doesn't call the ValidateUser() method.

Still combing through core source to see what the issue might be :)


Kevin Giszewski 15 Sep 2015, 09:17:43

Bump. Just wondering if this is going to be a breaking change for 7.3?


Kevin Giszewski 15 Sep 2015, 09:27:36

Looks like it's related to this? http://issues.umbraco.org/issue/U4-4808


Kevin Giszewski 29 Sep 2015, 14:49:11

Bump.


Kevin Giszewski 29 Sep 2015, 14:49:45

Since this appears to be a breaking change, is there any documentation available?


Shannon Deminick 29 Sep 2015, 14:52:45

I need to look into this, there's probably a work around


Kevin Giszewski 29 Sep 2015, 15:05:00

Awesome thank you. I image some of the 'enterprise' type users are using custom providers for active directory, LDAP, etc and may have similar experiences.


Kevin Giszewski 30 Sep 2015, 19:46:30

Just adding some info after spending some time on this.

The BackOfficeSignInManager is what is actually being used, but I don't know how the plumbing is working despite crawling through code. https://github.com/umbraco/Umbraco-CMS/blob/b0fb892b16e42291fe1152d3cf9ac6e2f3cf9477/src/Umbraco.Core/Security/BackOfficeSignInManager.cs

There is a lot of code that just doesn't get hit anymore which confounds me since it's referenced for web services, etc.

Both the UsersMembershipProvider and my provider have their constructors fire.

I suppose the final solution will be something to do with implementing some OWIN code.

I'll pick up on this at some point but would like some docs when they're available on what the new proper way to do this is.


Shannon Deminick 30 Sep 2015, 20:24:42

Yes I do need to document a few things. The reality is that we use asp net identity now, not membership providers... Though they are still there if membership APIs are used. Identity is fully customizable, you will need to extend the current implementation. I will reply with some info tomorrow to get you started.


Kevin Giszewski 30 Sep 2015, 22:32:46

Awesome thanks. Sounds like a good opportunity for me to learn something new.


Shannon Deminick 01 Oct 2015, 11:58:36

Hey Kevin,

So Membership providers can still be used where the membership provider APIs are used, however the Membership provider authentication logic is not used for ASP.Net Identity. As you've pointed out, this is done by BackOfficeSignInManager. This is an ASP.Net Identity implementation of SignInManager which uses the UserManager which uses the UserStore. We have our own implementations of these: BackOfficeUserManager and BackOfficeUserStore.

These are wired up during OWIN startup as marked in your web.config appSetting: owin:appStartup which defaults to: UmbracoDefaultOwinStartup. You can override this startup class to implement your own logic and override basically anything with ASP.Net Identity. We haven't release v1.0 of Identity Extensions yet but we will soon. In the meantime, here's the repo:

https://github.com/umbraco/UmbracoIdentityExtensions

In this release, we will install two script files that will allow you to more easily customize the Identity implementation:

UmbracoStandardOwinStartup:

https://github.com/umbraco/UmbracoIdentityExtensions/blob/master/src/Umbraco.IdentityExtensions/App_Start/UmbracoStandardOwinStartup.cs.pp

UmbracoCustomOwinStartup:

https://github.com/umbraco/UmbracoIdentityExtensions/blob/master/src/Umbraco.IdentityExtensions/App_Start/UmbracoCustomOwinStartup.cs.pp

These files contain some inline docs on how to acheive this. For example, the UmbracoCustomOwinStartup gives you an example of assigning a custom BackOfficeUserManager or BackOfficeUserStore instance. The UserManager basically wraps a lot of other underlying implementations. You can actually configure most of these implementations with setter properties (see the BackOfficeUserManager code).

That should get you started. There's a lot of docs I need to make, just need to find the time... will be soon though.


Kevin Giszewski 01 Oct 2015, 12:01:49

@Shandem Thanks for this. I'll try to get on this later this week or next.


Kevin Giszewski 09 Oct 2015, 17:11:52

So since we use CAS as our preferred login provider, I decided to try to use this package: https://github.com/noelbundick/Owin.Security.CAS

That resulted in me extending UmbracoDefaultOwinStartup and replacing the web.config reference.

My class:

using Microsoft.Owin.Security;
using Owin;
using Owin.Security.CAS;
using Umbraco.Web;

namespace v73
{
    public class CasOwinStartup : UmbracoDefaultOwinStartup
    {
        public override void Configuration(IAppBuilder app)
        {
            base.Configuration(app);

            app.SetDefaultSignInAsAuthenticationType("Net ID");

            var casOptions = new CasAuthenticationOptions()
            {
                AuthenticationType = "Net ID",
                Caption = "Login with Net ID",
                CasServerUrlBase = "https://login-test.foo"
            };
            
            app.UseCasAuthentication(casOptions);
        }
    }
}

I also tried not inheriting the class and writing my own.

That only works if I copy the methods from the original default class.

Either way, the CAS auth never happens. CAS auth should mean redirecting me to the CAS page and back after success. I still get the default Umbraco login with the same usual button.

I've even tried line by line to remove some of the base configuration method calls.

Truth be told, this is all new material for me.


Shannon Deminick 12 Oct 2015, 09:50:41

Hi Kev,

Yes i suggest you do some non umbraco setup of ASP.Net identity and look into the auth flow of how things are setup. I'm still unsure as to what you are trying to do. First you've said that you want to use a different data store for your users... now it looks like you are trying to install a custom OAuth provider?

If you want to use a custom OAath provider instead of a custom user store (which is probably nicer/easier) Then i suggest you look at the extension methods here: https://github.com/umbraco/UmbracoIdentityExtensions/tree/master/src/App_Start . For example, the Google one: https://github.com/umbraco/UmbracoIdentityExtensions/blob/master/src/App_Start/UmbracoGoogleAuthExtensions.cs

In this method, it creates the provider options and then the important parts: it calls: ForUmbracoBackOffice this ensures that the provider is 'tagged' to be used in the Umbraco back office. It also updates the auth-type (name) so that it's prefixed with "Umbraco." so that if someone were to use the same provider on the front-end it doesn't a have the same overlapping name (you could revert this prefix after you call ForUmbracoBackOffice if you want but not recommended unless your provider dynamically changes this name which is rare). It also sets an icon for the provider which is displayed on the login page.

So for example, if you create an extension method like that google one for your CaS provider, then you can just call your extension method in your own owin startup.

Additional notes:

  • SetDefaultSignInAsAuthenticationType will not do anything for the Umbraco back office... this is used by ASP.Net identity internally and would be used as the default provider for the front-end
  • The ASP.Net identity setup in Umbraco back office is configured to not interfere with front-end/default/standard ASP.Net identity implementations
  • By default an OAuth provider must be linked manually to a user's account. This means they would need a local login to Umbraco first, and then can link the OAuth provider in the back office for them to use. In some cases (probably like yours), your OAuth provider is a private provider (as opposed to a public provider like Facebook) and in which case it is much nicer to use your OAuth provider as the primary login source instead of having the local account as the primary login source. You can enable this by using SetExternalSignInAutoLinkOptions, see: http://issues.umbraco.org/issue/U4-6753


Brad Wickett 12 Oct 2015, 14:30:09

I'm having major problems with the new authentication in this release as well. Previously, I had written a custom membership provider and specified in web.config to use that as the UsersMembershipProvider. It was using the public override bool ValidateUser(string username, string password) to perform the authentication against our Active Directory. In the new version I have used your OWIN examples and specified an override for the app.ConfigureUserManagerForUmbracoBackOffice in the OWIN startup. The class I have designed inherits from MembershipProviderBase and I have all the proper methods in place. However, the only method that ever gets fired is the constructor. The public override bool ValidateUser(string username, string password) function is never called during authentication. It seems that even though I'm hooked into the user manager with my custom class, that something is wrong in 7.3.0 in that none of the override functions are being called from my class. I can upload the entire solution somewhere if that helps. This is a critical issue for us as we authenticate our users against AD and I cannot upgrade our production instance until this is working again.


Shannon Deminick 12 Oct 2015, 14:49:56

Sure you can upload your code here and set it to visible by HQ only and i can have a look.


Brad Wickett 12 Oct 2015, 14:58:50

The project seems to exceed the max allowed upload size. The VS 2015 project is 87MB when zipped. I'll try just copying the basics to some text files and attach those.


Shannon Deminick 12 Oct 2015, 15:00:52

Sure, all i need is the basic parts of what you are trying to do and can go from there.


Kevin Giszewski 12 Oct 2015, 15:02:04

@brad Sounds like the exact same issue as I have. AFAIK it's never going to call that method in 7.3.0

@Shandem I'll look over your notes. All I need is a method to be called like the original membership provider. Since I can't do that without writing an OWIN provider, I'm trying to use our alternative auth which is CAS (used at lots of major universities).


Shannon Deminick 12 Oct 2015, 15:03:35

Ok, well there are two different things here:

  • A user store -> this is not an Owin Provider
  • An OAuth provider -> This is like the 'log in with facebook' button

Those are two very different things.


Shannon Deminick 12 Oct 2015, 15:07:12

And yes, i will re-iterate.... The membership provider does not get used to validate a user.


Brad Wickett 12 Oct 2015, 15:07:19

Here is an overview of what I had done:

  1. Create a new Visual Studio 2015 solution and use NuGet to include Umbraco 7.3.0.
  2. Used NuGet to add your sample OwinStartup.cs files.
  3. Modified the web.config file and set the owin:Startup to be the UmbracoStandardOwinStartup.
  4. Created a custom class called WsuCustomMembershipProvider.cs which contained the following code:

namespace UmbracoWSUMembershipProvider { using Umbraco.Core.Security; using System; using System.Web.Security;

public class wsuLDAP : MembershipProviderBase
{
    public override MembershipPasswordFormat PasswordFormat {
        get
        {
            return MembershipPasswordFormat.Hashed;
        }
    }

    public override bool DeleteUser(string username, bool deleteAllRelatedData)
    {
        throw new NotImplementedException();
    }

    public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
    {
        throw new NotImplementedException();
    }

    public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
    {
        throw new NotImplementedException();
    }

    public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
    {
        throw new NotImplementedException();
    }

    public override int GetNumberOfUsersOnline()
    {
        throw new NotImplementedException();
    }

    public override MembershipUser GetUser(string username, bool userIsOnline)
    {
        throw new NotImplementedException();
    }

    public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
    {
        throw new NotImplementedException();
    }

    public override string GetUserNameByEmail(string email)
    {
        throw new NotImplementedException();
    }

    public override bool UnlockUser(string userName)
    {
        throw new NotImplementedException();
    }

    public override void UpdateUser(MembershipUser user)
    {
        throw new NotImplementedException();
    }

    protected override bool PerformChangePassword(string username, string oldPassword, string newPassword)
    {
        throw new NotImplementedException();
    }

    protected override bool PerformChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
    {
        throw new NotImplementedException();
    }

    protected override MembershipUser PerformCreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
    {
        throw new NotImplementedException();
    }

    protected override string PerformGetPassword(string username, string answer)
    {
        throw new NotImplementedException();
    }

    protected override string PerformResetPassword(string username, string answer, string generatedPassword)
    {
        throw new NotImplementedException();
    }

    public override bool ValidateUser(string username, string password)
    {
        bool resp = false;

        return resp;
    }

}

}

  1. Modified the UmbracoStandardOwinStartup.cs file to call my override as:

         app.ConfigureUserManagerForUmbracoBackOffice(
             ApplicationContext.Current,
             new UmbracoWSUMembershipProvider.wsuLDAP()
         );
    

I have setup breakpoints on every single method in my override and they are not getting called at login. Any help would be greatly appreciated as I have never used the OWIN auth before and this is all new to me.


Shannon Deminick 12 Oct 2015, 15:08:32

Again... the membership provider is not used for authentication.


Kevin Giszewski 12 Oct 2015, 15:09:03

@Shandem Sorry :) I'm out of my element entirely.

As with Brad, my previous implementation was a single method overridden public override bool ValidateUser(string username, string password){}. Since v7.3.0 no longer calls this on login, the OWIN path I believe is my only recourse.


Brad Wickett 12 Oct 2015, 15:09:35

Basically, we just need a simple way to tell Umbraco to call our LDAP authentication to authenticate against Active Directory. We are not using Azure AD at this point, so I need to be able to call my authentication code which will authenticate our users against our local AD.


Kevin Giszewski 12 Oct 2015, 15:11:33

@wicketbr Exact same scenario here.


Brad Wickett 12 Oct 2015, 15:12:38

I think what we all need is a simple code solution example that shows how to intercept the authentication call and return our own boolean based on our local authentication. I want to use the Umbraco user store for everything else except the authentication portion when a user exists in our local AD. In my previous provider I would check to see if a local account authorization worked first by calling the built-in ValidateUser function. That way, we could still use local Umbraco auth for things like an Admin account. Then, if that failed I called my own Active Directory login and returned true or false depending on if that was a good login. If it was good I called System.Web.Security.FormsAuthentication.SetAuthCookie(username, true); and returned true.


Kevin Giszewski 12 Oct 2015, 15:15:59

@wicketbr Wow you're code sounds like a mirror of mine ;)


Brad Wickett 12 Oct 2015, 15:16:55

And I should mention that I cannot use the sample LDAP auth option I've seen as our system is in place in a very large university and we do not have all of our users in a single OU. The samples I've seen where you give an LDAP query location don't seem to allow me to specify the RootDSE. Plus, that example wants a username and password stored for a user that can access AD via LDAP and I cannot store credentials in our web.config like that.


Brad Wickett 12 Oct 2015, 15:18:26

Kevin, I have a feeling that a lot of people are using some sort of method like I am. I wouldn't mind switching over to using the built-in authentication in Umbraco if users were able to change their password without having to have access to the users area. However, I have almost a hundred editors and I would spend a lot of time with password issues instead of just authenticating against AD.


Brad Wickett 12 Oct 2015, 15:21:56

Shannon, is there a complete example of how to authenticate Umbraco against our own local auth mechanism using your new OWIN integration? A complete step-by-step guide would help a lot of us. Or, an option for end users to be able to change their own password could also be a less-than-desirable fix. As it stands right now I cannot upgrade from our current 7.2.x install to 7.3 or higher until I can figure out how to authenticate our users.


Kevin Giszewski 12 Oct 2015, 15:22:05

@wicketbr Same here :) As an alternative to LDAP we can use CAS for SSO but getting the backoffice to do that has been difficult. Therefore we've settled with LDAP for now. The new way looks promising for external auth providers but I can't figure out either (LDAP or CAS) at the moment in this version.


Gleb Kaplan 12 Oct 2015, 15:24:28

I support this request, the lack of documentation / examples on implementing custom authentication prevents us from upgrading to 7.3


Shannon Deminick 12 Oct 2015, 15:26:26

Yes i understand what you are both saying... I would love to invent all of the time in the world and document this. I know it is required and I will get to it but in the meantime you'll have to help me help you read as much as you can with what I'm trying to tell you.

See this comment above: http://issues.umbraco.org/issue/U4-7032#comment=67-23165

ASP.Net Identity isn't new, its a few years old now, something that we all must accept and learn.

So, i'll try to re-explain:

  • All things are done with implementations of the UserManager, Umbraco's is the BackOfficeUserManager
  • A UserManager wraps all things in the UserStore, Umbraco's is the BackOfficeUserStore.
  • You can replace both of these entirely if you wanted, or you can replace one or the other or just parts
  • User Store is the Data, User Manager is the thing that uses the data.
  • The SignInManager is the thing that currently does authentication which uses the User Manager to do some checks
  • The UserManager.CheckPasswordAsync is the method you want to override.

I promise tomorrow I will respond with an example of how to achieve this, unfortunately its the end of the day here and I have to head out.


Brad Wickett 12 Oct 2015, 15:32:42

@Shandem that would be great. If I can get an example of how to do this I will be glad to post an entire step-by-step solution for others to use.


Kevin Giszewski 12 Oct 2015, 15:34:02

@Shandem Thanks again Shannon. I don't want to come across as dense and I understand. This was just an unexpected breaking change for LDAP auth users (though it sounds like we have been utilizing the MemberShipProvider incorrectly all along).

I'm out of the office today and will spend some time working on a solution tomorrow which I will share of course.


Shannon Deminick 13 Oct 2015, 15:42:10

Hi all,

@wicketbr It would be amazingly fantastic if someone can make a start on docs for this. There's actually a task for me to do on the GitHub docs tracker: https://github.com/umbraco/UmbracoDocs/issues/256 . Of course that task will include quite a substantial amount of docs, even starting with simple documentation now is better than none ;)

Ok, for starters this thread is mainly about "I want to be able to customize the authentication logic for back office users". This is basically the entire reason behind putting Identity in the core. Membership is legacy (dead), it is not flexible and doesn't really support anything to do with a modern auth process. Unfortunately auth is an insanely complicated thing and the ASP.Net Identity team have done a reasonably good job of creating a very flexible API, unfortunately they haven't done a good job of documenting it.

The API is incredibly flexible so we won't get into all the nitty gritty details but I'll describe how you can implement a method to perform custom authentication. Here's the logic flow of how a password is validated:

  • The SignInManager.PasswordSignInAsync is used, Umbraco's implementation is BackOfficeSignInManager
  • PasswordSignInAsync Can call into a few methods: UserManager.FindByNameAsync(userName), UserManager.IsLockedOutAsync(user.Id), UserManager.CheckPasswordAsync(user, password), UserManager.VerifyPasswordAsync
  • The UserManager methods above are virtual. Umbraco's implementation is BackOfficeUserManager. Each of these methods calls in to the UserStore, Umbraco's implementation is BackOfficeUserStore.
  • The UserStore methods that could be called in the above 3 methods are: Store.FindByNameAsync(userName), Store.FindByIdAsync(userId), Store.GetLockoutEnabledAsync(user), Store.GetLockoutEndDateAsync(user), Store.GetPasswordHashAsync(user)
  • All of the UserStore methods are interface methods from many various interfaces such as: IUserPasswordStore, IUserLoginStore, IUserEmailStore, etc... Since these are interface methods, it is possible to override any of the BackOfficeUserStore methods by explicit implementation.

So there are clearly two points of extensibility here:

  • UserManager
  • UserStore

And by default the UserManager more or less just calls into the UserStore to retrieve data and work with it. So the modifications you'll need to make really depend on what you are trying to achieve! For example:

Do you just want to validate the username/password with your own data, but the actual Umbraco User data is still stored inside of the Umbraco data store?

Do you want to have the Umbraco user auth data stored in your own custom way? (i.e. LDAP) including all Identity information such as passwords, lock-out data, email, name & phone number information, etc... ?

If you only require option #1, then the solution is relatively easy:

//Create a custom UserManager
//NOTE!! - This ctor overload is only available in 7.3.1, unfortunately 
// to do the same in 7.3.0 there's a few hoops that you'd need to jump 
// through. You can see the 7.3.0 BackOfficeUserManager code here:
// https://github.com/umbraco/Umbraco-CMS/blob/d0c4b2ab7293f4ccacb6b9bcda237bd51be8a2b6/src/Umbraco.Core/Security/BackOfficeUserManager.cs
// So you can see you'd have to use the simple ctor and copy/paste the InitUserManager manager code
// into this class below and make sure it's called

public class CustomBackOfficeUserManager : BackOfficeUserManager
{
    public CustomBackOfficeUserManager(
        IUserStore<BackOfficeIdentityUser, int> store, 
        IdentityFactoryOptions<BackOfficeUserManager> options, 
        MembershipProviderBase membershipProvider) : 
        base(store, options, membershipProvider)
    {
    }

    /// <summary>
    /// Returns true if the password is valid for the user
    /// </summary>
    /// <param name="user"/><param name="password"/>
    /// <returns/>
    public override Task<bool> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
    {
        //This method normally will:
        // * Get the UserStore
        // * return the call to this.VerifyPasswordAsync with the UserStore instance

        //If you want to authenticate the user with some totally custom data, you can do that here:
        if (VerifyTheUsersCredentialsManually(user.Email, password))
        {
            return Task.FromResult(true);
        }
        return Task.FromResult(false);
    }
}

//Then in your owin startup, you can use this block of code for your 
// `ConfigureUserManagerForUmbracoBackOffice` method:

var appCtx = ApplicationContext.Current;
app.ConfigureUserManagerForUmbracoBackOffice<BackOfficeUserManager, BackOfficeIdentityUser>(
    appCtx,
    (options, context) =>
    {
        var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider();
        var store = new BackOfficeUserStore(
            appCtx.Services.UserService, 
            appCtx.Services.ExternalLoginService,
            membershipProvider);
        return new CustomBackOfficeUserManager(store, options, membershipProvider);
    });

Now, if you require option #2, then it's quite a lot of work because you'd need to implement your own UserStore. The UserStore is split into several smaller interfaces, you can see these here: https://github.com/umbraco/Umbraco-CMS/blob/d0c4b2ab7293f4ccacb6b9bcda237bd51be8a2b6/src/Umbraco.Core/Security/BackOfficeUserStore.cs#L16 So if you wanted to store some of this information outside of Umbraco, you could create a class that inherits from BackOfficeUserStore and then explicitly implement the interfaces that you'd like to handle ... or you could write an entire user store from scratch ... you could in fact write the entire UserManager + UserStore + SignInManager from scratch if you wanted.

Other things to note:

  • Updating a User in the back office - whether it's in the user editor, password changer, etc... will not make updates the the ASP.Net Identity provider
  • Updating a User in the back office will still use the membership provider settings described in the web.config for users
  • Currently it is very difficult to support both Membership provider settings and ASP.Net Identity settings simultaneously but this will become better with time ...
  • The initial ASP.Net Identity implementation in 7.3 is focused primarily on the Auth process, in later versions we will take Identity settings/configuration into account when updating user data.
  • The membership provider parameter that you have all seen in the ConfigureUserManagerForUmbracoBackOffice method is actually used to initialize the ASP.Net Identity implementation with the configured settings for the membership provider to support backwards compatibility, so settings such as MaxInvalidPasswordAttempts, MinRequiredNonAlphanumericCharacters, MinRequiredPasswordLength and the logic that the current membership provider uses for password hashing needs to be used as well - otherwise users couldn't log in at all.
  • The UserManager has public getter/setter properties to tweak it's settings, this is essentially what this code is doing: https://github.com/umbraco/Umbraco-CMS/blob/d0c4b2ab7293f4ccacb6b9bcda237bd51be8a2b6/src/Umbraco.Core/Security/BackOfficeUserManager.cs#L74 , so there's quite a few settings you can tweak there without actually implementing your own UserManager


Brad Wickett 13 Oct 2015, 16:25:10

Yes, I just want to do option 1. I want to validate the username and password against an outside source that I will call, but I want to use the Umbraco user store for everything else. I have some meetings this morning, but I'll take a look at trying to use your code example later today.


Kevin Giszewski 13 Oct 2015, 17:55:36

@Shandem Thank you so much for taking the time. I'm VERY close to making it work.

I'm building mine based off of 7.3.0.

I have a few classes:

using Owin;
using Umbraco.Core;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Web.Security.Identity;

namespace v73
{
    public class MyOwinStartup
    {
        public void Configuration(IAppBuilder app)
        {
            app.SetUmbracoLoggerFactory();

            var applicationContext = ApplicationContext.Current;

            app.ConfigureUserManagerForUmbracoBackOffice<MyUserManager, BackOfficeIdentityUser>(
                ApplicationContext.Current, 
                (options, context) =>
                    {
                        var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider();

                        var store = new BackOfficeUserStore(
                                    applicationContext.Services.UserService,
                                    applicationContext.Services.ExternalLoginService,
                                    membershipProvider);
                        
                        var myManager = new MyUserManager(store);

                        return MyUserManager.InitUserManager(myManager, membershipProvider, options);
                    });

            app.UseUmbracoBackOfficeCookieAuthentication(ApplicationContext.Current)
                .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current);
        }
    }
}

//the custom user manager

using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;

namespace v73
{
    public class MyUserManager : BackOfficeUserManager
    {
        public MyUserManager(IUserStore<BackOfficeIdentityUser, int> store)
            : base(store)
        {
        }

        public override Task<bool> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
        {
            //for now always validate; add LDAP logic here
            return Task.FromResult(true);
        }

        public static MyUserManager InitUserManager(MyUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions<MyUserManager> options)
        {
            // Configure validation logic for usernames
            manager.UserValidator = new UserValidator<BackOfficeIdentityUser, int>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = membershipProvider.MinRequiredPasswordLength,
                RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false
            };

            //use a custom hasher based on our membership provider
            manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider);

            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = new DataProtectorTokenProvider<BackOfficeIdentityUser, int>(dataProtectionProvider.Create("ASP.NET Identity"));
            }

            manager.UserLockoutEnabledByDefault = true;
            manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30);
            manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory();

            return manager;
        }
    }

    //INCLUDED THIS BECAUSE IT'S INTERNAL IN THE CORE
    internal class MembershipPasswordHasher : IPasswordHasher
    {
        private readonly MembershipProviderBase _provider;

        public MembershipPasswordHasher(MembershipProviderBase provider)
        {
            _provider = provider;
        }

        public string HashPassword(string password)
        {
            return _provider.HashPasswordForStorage(password);
        }

        public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            return _provider.VerifyPassword(providedPassword, hashedPassword)
                ? PasswordVerificationResult.Success
                : PasswordVerificationResult.Failed;
        }
    }
}

This compiles and I can confirm it all runs through the InitUserManager no problem. However when running, it throws this exception:

Value cannot be null.
Parameter name: userManager

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: userManager

Source Error: 

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace: 


[ArgumentNullException: Value cannot be null.
Parameter name: userManager]
   Microsoft.AspNet.Identity.Owin.SignInManager`2..ctor(UserManager`2 userManager, IAuthenticationManager authenticationManager) +108
   Umbraco.Core.Security.BackOfficeSignInManager..ctor(BackOfficeUserManager userManager, IAuthenticationManager authenticationManager, ILogger logger, IOwinRequest request) +26
   Umbraco.Core.Security.BackOfficeSignInManager.Create(IdentityFactoryOptions`1 options, IOwinContext context, ILogger logger) +116
   Microsoft.AspNet.Identity.Owin.<Invoke>d__0.MoveNext() +125
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13908768
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.AspNet.Identity.Owin.<Invoke>d__0.MoveNext() +450
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13908768
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.<RunApp>d__5.MoveNext() +202
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +13908768
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +61
   Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.<DoFinalWork>d__2.MoveNext() +193
   Microsoft.Owin.Host.SystemWeb.IntegratedPipeline.StageAsyncResult.End(IAsyncResult ar) +96
   System.Web.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +363
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +157

Still troubleshooting.


Kevin Giszewski 14 Oct 2015, 13:47:31

I got it working. The pitfall I fell into was I changed the first type passed like so:

app.ConfigureUserManagerForUmbracoBackOffice<MyUserManager, BackOfficeIdentityUser>...

and it should be (like Shannon's example):

app.ConfigureUserManagerForUmbracoBackOffice<BackOfficeUserManager, BackOfficeIdentityUser>.

Full working code for 7.3.0 follows:

Create a class for your OwinStartup:

using Owin;
using Umbraco.Core;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Web.Security.Identity;

namespace v73
{
    public class MyOwinStartup
    {
        public void Configuration(IAppBuilder app)
        {
            app.SetUmbracoLoggerFactory();

            var applicationContext = ApplicationContext.Current;

            app.ConfigureUserManagerForUmbracoBackOffice<BackOfficeUserManager, BackOfficeIdentityUser>(
                ApplicationContext.Current, 
                (options, context) =>
                    {
                        var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider();

                        var store = new BackOfficeUserStore(
                                    applicationContext.Services.UserService,
                                    applicationContext.Services.ExternalLoginService,
                                    membershipProvider);

                        return MyUserManager.InitUserManager(new MyUserManager(store), membershipProvider, options);
                    });

            app.UseUmbracoBackOfficeCookieAuthentication(ApplicationContext.Current)
                .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current);
        }
    }
}

Point your site to this class in your web.config appSettings:

<add key="owin:appStartup" value="v73.MyOwinStartup" />

Create your custom user manager:

using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;

namespace v73
{
    public class MyUserManager : BackOfficeUserManager
    {
        public MyUserManager(IUserStore<BackOfficeIdentityUser, int> store)
            : base(store)
        {
        }

        public override Task<bool> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
        {
            //for now always validate; add LDAP logic here
            return Task.FromResult(true);
        }

        public static MyUserManager InitUserManager(MyUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions<BackOfficeUserManager> options)
        {
            // Configure validation logic for usernames
            manager.UserValidator = new UserValidator<BackOfficeIdentityUser, int>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = membershipProvider.MinRequiredPasswordLength,
                RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false
                //TODO: Do we support the old regex match thing that membership providers used?
            };

            //use a custom hasher based on our membership provider
            //THIS IS AN INTERNAL METHOD WHICH I PULL OUT INTO A CLASS BELOW
            //THIS SHOULD NOT BE NECESSARY IN v7.3.1
            manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider);

            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = new DataProtectorTokenProvider<BackOfficeIdentityUser, int>(dataProtectionProvider.Create("ASP.NET Identity"));
            }

            manager.UserLockoutEnabledByDefault = true;
            manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts;
            //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked
            // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are
            // locked out or not.
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30);

            //custom identity factory for creating the identity object for which we auth against in the back office
            manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory();

            return manager;
        }
    }

    //INCLUDED THIS BECAUSE IT'S INTERNAL
    internal class MembershipPasswordHasher : IPasswordHasher
    {
        private readonly MembershipProviderBase _provider;

        public MembershipPasswordHasher(MembershipProviderBase provider)
        {
            _provider = provider;
        }

        public string HashPassword(string password)
        {
            return _provider.HashPasswordForStorage(password);
        }

        public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            return _provider.VerifyPassword(providedPassword, hashedPassword)
                ? PasswordVerificationResult.Success
                : PasswordVerificationResult.Failed;
        }
    }
}

@Shandem thank you so much for clearing this up for us. I let you close this (instead of me) in case you need this for tracking the new overload in v7.3.1.


Brad Wickett 14 Oct 2015, 13:49:12

So, will this all change again in 7.3.1?


Kevin Giszewski 14 Oct 2015, 13:54:26

@wicketbr Shannon will be adding an overload to the BackOfficeUserManager to reduce the redundant code (InitUserManager and the extra class I had to pull in). It'll reduce some code and make sure that if any changes are made to the InitUserManager method stay up to date since we'll be calling the native method.


Brad Wickett 14 Oct 2015, 21:00:11

@kgiszewski I've tried using your code, and if I set a breakpoint on the public MyUserManager I can see that firing. However, if I set a breakpoint in public override Task CheckPasswordAsync that never fires when I click Login from the Umbraco login form. Am I missing something else?


Brad Wickett 14 Oct 2015, 22:04:38

@kgiszewski Never mind, I did a fresh install and got it working. Now I'm going to attempt to integrate my LDAP auth first, then failover to the internal integration if the LDAP fails.

Thanks,

Brad


José A.García 15 Oct 2015, 06:46:58

@kgiszewski: I've been trying to use your former code but it seems it does not authenticate users against LDAP. Any valid user with any random password allows the user to be logged into the BackOffice. I suppose that's because the part of the code that you marked as "for now always validate; add LDAP logic here"

Can you please post your final code when you add that part also? We just need a plain simple Active Directory authentication using LDAP (nor Azure nor any other fancy way).

I'm not much into the Umbraco programming, since we've been using Umbraco only for some months. We had Active Directory authentication in place and it simply broke up when upgraded to 7.3. We've been trying to fix this since and your solution (though not finished yet) is the closest we have found to a solution.

Thanks and regards.

PS: Will this fix be integrated (not needed) in 7.3.1 or will we need it too then?


Kevin Giszewski 15 Oct 2015, 09:19:43

@jagbarcelo.1 The purpose of this bug report was to communicate to the HQ that a previous feature was not working in v7.3.0. It addresses the larger issue of 'where do I put my authentication code'? Furthermore I wouldn't be able to put in my exact working code for LDAP as it would expose too much information about my exact setup than I'm comfortable with.

Ideally you would already have the LDAP auth logic to drop in. I would recommend opening a thread on http://our.umbraco.org to get some specific help. I will caution you that it took quite a bit of effort to get LDAP working properly and may not be a trivial task. The LDAP portion that you seek isn't specific to Umbraco at this point and you can utilize additional resources beyond Umbraco. Also, authentication falls into security and you may want to consider hiring an Umbraco consultant assist you if you're not comfortable with Umbraco to that level.

Sorry I can't be of more help at this point, but my code above should create a nice extension point for you to get started. v7.3.1 only simplifies the code a bit and the general gist will still be needed.


Brad Wickett 15 Oct 2015, 13:44:07

@jagbarcelo.1 : You could try something like this. You need to make sure you are "using System.DirectoryServices; " and that you have included a reference to DirectoryServices in your project:

private bool ldapAuth(string Username, string Password) { bool resp = false; try { string ldapRoot = "your.ad.root"; // (i.e. ad.yourcompany.com) string domainAndUsername = ldapRoot + @"" + Username; DirectoryEntry entry = new DirectoryEntry("LDAP://" + ldapRoot, domainAndUsername, Password); try { object obj = entry.NativeObject; DirectorySearcher search = new DirectorySearcher(entry); search.Filter = "(SAMAccountName=" + Username + ")"; search.PropertiesToLoad.Add("cn"); System.DirectoryServices.SearchResult result = search.FindOne(); if (result != null) { // Login was successful resp = true; } } catch (Exception ex) { // Login was invalid, you can examine the Exception object here to see why if you want } } catch (Exception ex) { // Login was invalid, you can examine the Exception object here to see why if you want } return resp; }


Brad Wickett 15 Oct 2015, 15:05:06

@jagbarcelo.1 and @kgiszewski ,

I have put together a simple solution for LDAP Owin authentication that should be fairly easy to implement.

  1. Copy the attached "OWIN LDAP Authenticaton.dll" file to your bin folder. Make sure the file is named with the spaces, the download may replace the spaces with + characters.
  2. Modify the "owin:appStartup" key as shown below:

  1. Add a new key as shown below:

Just make sure to set the value to your actual ad root. That's it. The entire Visual Studio 2015 solution is too large to post here, so I've uploaded it to my website at the link below:

http://wickett.net/UserFiles/UmbracoOwinLdapAuthentication.zip (86.6 MB)

If you want to run and debug that instance of Umbraco you can login as the admin user with the following credentials:

username: admin@local password: test


Gleb Kaplan 15 Oct 2015, 15:48:09

Thank you all for your examples!

Now, I am trying to implement Kevin's example (with LDAP authentication similar to Brad's) and everything is working fine, however immediately after login to the backoffice, I get 401 error from ServerVariables HTTP call.

"401 You must login to view this resource.",

so it seems like the authentication token is not persisting.

What am I missing?


Brad Wickett 15 Oct 2015, 15:58:48

@Glebby , do you have users configured already in the Umbraco Users section with usernames that match your AD usernames?


Gleb Kaplan 15 Oct 2015, 16:06:42

Sure thing. The login itself works, as I after all can see proper response from PostLogin:

)]}', {"email":"XXXX@XXXX.XXX","locale":"en-GB","emailHash":"d483b8aa5951e972c91ee0ee0a618b27","userType":"admin","remainingAuthSeconds":1200.0,"startContentId":-1,"startMediaId":-1,"allowedSections":["content","courier","developer","forms","media","member","settings","users"],"id":0,"name":"Gleb Kaplan"}


Brad Wickett 15 Oct 2015, 18:19:27

I'm not sure on that one, as I've never seen a 401 from Umbraco on login. Can you post a screen shot? It could be an IIS message as opposed to something from Umbraco itself and might be related to your IIS security settings.


José A.García 16 Oct 2015, 10:38:23

@wicketbr , Thanks for your {{ldapAuth}} code. I managed to integrate it with @kgiszewski former code by simply changing the {{CheckPasswordAsync}} method: public override Task CheckPasswordAsync(BackOfficeIdentityUser user, string password) { //for now always validate; add LDAP logic here //return Task.FromResult(true);

bool ret = ldapAuth(user.Name, password); return Task.FromResult(ret); }

of course I also had to add {{using System.DirectoryServices;}} on top of the file and add the reference in {{web.config}}: ... ...

In our case, we have not experienced the 401 error @Glebby has talked about.


Gleb Kaplan 16 Oct 2015, 12:22:16

Nevermind, must be something wrong with my upgrade. I rolled back and performed the upgrade to 7.3 again, then added the custom authentication, everything is working like a charm. Thank you.


Amir Moradi 16 Oct 2015, 13:51:05

I have problem with my custom AD membership since I updated from 7.2.6 to 7.3. It is exact similar to http://issues.umbraco.org/issue/U4-7190, Is there any fix or workaround for this problem as well?


Amir Moradi 20 Oct 2015, 11:26:48

Thanks Kevin, Brad, and Shannon.


Shannon Deminick 22 Oct 2015, 16:38:50

Hi all,

for 7.3.1 I've made this process much easier to extend with a new interface called IBackOfficeUserPasswordChecker which you can implement (it's one method). Then you can set the default BackOfficeUserManager's property BackOfficeUserPasswordChecker to your own implementation. This means you don't have to extend the default implementation.

var applicationContext = ApplicationContext.Current;
app.ConfigureUserManagerForUmbracoBackOffice<BackOfficeUserManager, BackOfficeIdentityUser>(
    applicationContext,
    (options, context) =>
    {
        var membershipProvider = Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider();
        var store = new BackOfficeUserStore(
                    applicationContext.Services.UserService,
                    applicationContext.Services.ExternalLoginService,
                    membershipProvider);
        var userManager = new BackOfficeUserManager(store)
        {
            //Set your own custom IBackOfficeUserPasswordChecker
            BackOfficeUserPasswordChecker = new MyPasswordChecker()
        };
        return userManager;
    });


Kevin Giszewski 22 Oct 2015, 17:16:23

@Shandem Bravo. I think that'll cover a lot of the use-cases.


Brad Wickett 22 Oct 2015, 21:19:29

@Shandem With the new BackOfficeUserPasswordChecker in 7.3.1 will there still be a way to use the default if our external fails? In my case, I attempt to authenticate against the default built-in account first. Then, if that fails I check against our LDAP authentication. This way I can still use built-in accounts but can also use AD authentication via LDAP if the user authentication against the local account fails.


Kenneth Solberg 25 Oct 2015, 18:05:23

@kgiszewski I tried your solution but I still get Windows login prompt after Umbraco login screen when site is running Windows Authentication.

Edit: I also tried nightly from: https://ci.appveyor.com/project/SebastiaanJanssen/umbraco-cms-hs8dx/build/1488/artifacts

Same result - here's how to reproduce: Set up a 7.2.8 and switch to Windows Authentication, browse to machinename/umbraco and log in, oila - works. Do the same for a 7.3.0/7.3.1b1488 and you'll get a Windows login prompt -after- Umbraco login ... pretty standard intranet setup, so I guess this will affect quite a few.


Kevin Giszewski 26 Oct 2015, 12:05:22

@kenneth I'm not sure I'll be of much help to you on that one as I don't use Windows Auth. I know going from 7.2.8 to 7.3.0, the MembershipProvider ValidateUser method no longer fires. Not sure if you have any third party auth providers in use that rely on that method.


Brad Wickett 26 Oct 2015, 13:31:09

The solution I am using does not use Windows Authentication in IIS. If you are using that you would have to write a different solution. The solution I am using uses the username and password from the Umbraco login but authenticates that against our Active Directory using LDAP.


Shannon Deminick 26 Oct 2015, 13:33:03

@wicketbr very good point sir! I will update the solution to be able to easily fall back to the default password checker.

@kenneth Not sure about the windows auth/basic auth in this scenario, i would have to test this.


Shannon Deminick 26 Oct 2015, 14:21:03

PR for docs is here: https://github.com/umbraco/UmbracoDocs/pull/265

PR for code is here: https://github.com/umbraco/Umbraco-CMS/pull/835


Kevin Giszewski 26 Oct 2015, 14:28:31

@Shandem #h5yr!


Kenneth Solberg 27 Oct 2015, 06:54:03

@Shandem Ok, I created a separate issue for it here U4-7307 - TL;DR - not possible to log in to Umbraco when using Windows Authentication in 7.3.0.


Brian Powell 25 Nov 2015, 00:02:21

I published a project on CodePlex that should help anyone using any of the standard MembershipProvider-based user authentication setups: https://github.com/Bitmapped/UmbBackofficeMembershipProvider. I wrote this code to work with another CodePlex project of mine, Active Directory ASP.NET Provider (https://github.com/Bitmapped/AdAspNetProvider). That's the only one I've tested it with, but it should work with any other standard provider.

Drop the DLL and the DLL for Umbraco's Identity Extensions into the \bin folder. Configuration instructions are at https://github.com/Bitmapped/UmbBackofficeMembershipProvider and are straightforward.


Richard Hamilton 02 Sep 2016, 14:45:40

@bitmapped These links down't work. I have resolved one issue that this page relates to but also want to hook into the logout

UmbracoContext.Current.Security.ClearCurrentLogin();

How can I do this?


Brian Powell 02 Sep 2016, 14:56:41

@richard_hamilton@mail.com I updated my links in my earlier post. I migrated to Github at the beginning of the year.

I'm not aware of any hooks to ClearCurrentLogin() but I haven't looked for them either. That's beyond the scope of the login provider, which just checks to see if the username/password are valid.


Tom Bille 05 Sep 2016, 10:52:29

@bitmapped - Nice project you've published. I have my user-data in a different system and my problem is that I can't get to the custom CheckPasswordAsync method because the user-accounts sometimes isn't created in Umbraco yet. Is there a way to bypass Umbraco's initial find-the-user-in-the-umbraco-db stuff?


Brian Powell 06 Sep 2016, 12:14:25

@tom@tombille.dk I'm not immediately aware of bypassing the check but I haven't gone looking specifically for that either. Maybe you can set up your other system to automatically create an Umbraco account on user creation?


LAD team 06 Sep 2016, 14:16:49

@tom@tombille.dk We've done something like this in our implementation of Identity login.

We're using Active Directory to authenticate, and are hijacking Umbraco's subsystems to create an Umbraco user during the initial FindByNameAsync phase. To elaborate, we have a custom user manage that derives from BackOfficeUserManager. When the UserManager calls FindByNameAsync, it initially tries to find a user via the UserService using GetByUsername. If this returns null, we create a new BackOfficeIdentiyUser (filled in with details we retrieve from Active Directory and store it in the UserStore with Store.CreateAsync. We store this user without a password (Umbraco generates a dummy password).

We use a custom implementation of IBackOfficeUserPasswordChecker to actually perform the password authentication. The PasswordChecker implementation also synchronizes usertype and application access (as we specify this stuff based on AD groups via config).

I haven't published the code anywhere, but perhaps it's something we can look into. There's some specific libraries we use that don't make sense to release.


Brian Powell 06 Sep 2016, 14:37:22

@LTSB-Applications-Development@legis.wisconsin.gov I think that functionality is something a lot of users would be interested in. Do you have to custom compile Umbraco to make this work or is it a DLL you can just drop in?


LAD team 06 Sep 2016, 14:47:33

We haven't done any custom compilation of Umbraco for the sake of our own maintenance and sanity. This could be extracted into its own DLL that people could just drop in. It's pretty simple, but currently relies on some specifics that won't quite make sense in other environments. The implementation just overrides a few methods of Umbraco's own UserManager. In our CustomOwinStartup we're just calling Create on our custom UserManager and setting the password checker to our custom password checker.

You're right that it may be worthwhile to share this particular implementation. I would need to abstract out some of our AD details. I'm also not sure if it's worth including our config section implementation that maps AD groups to Umbraco user types, perhaps!


Tom Bille 07 Sep 2016, 06:05:21

@LTSB-Applications-Development@legis.wisconsin.gov That sounds awesome. I would love to see which overrides you have made and how you did it.


Priority: Normal

Type: Feature (request)

State: Fixed

Assignee:

Difficulty: Normal

Category: Architecture

Backwards Compatible: True

Fix Submitted:

Affected versions: 7.3.0

Due in version: 7.3.1

Sprint: Sprint 2

Story Points:

Cycle: