Skip to content

Instantly share code, notes, and snippets.

@Shazwazza
Last active December 4, 2020 10:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Shazwazza/2fbbbe6567a2b0509f5215af8ba9ab37 to your computer and use it in GitHub Desktop.
Save Shazwazza/2fbbbe6567a2b0509f5215af8ba9ab37 to your computer and use it in GitHub Desktop.
Umbraco 2FA integration demo
<div ng-controller="My2FA.LoginController">
<div id="twoFactorlogin" class="umb-modalcolumn umb-dialog" ng-cloak>
<div class="form">
<h1>2FA</h1>
<div>
<p>Enter 2FA Code</p>
<form method="POST" name="twoFactorSendForm" ng-submit="send(provider)" ng-if="step=='send'">
<div class="control-group">
<select ng-model="provider" ng-show="providers.length > 0">
<option ng-repeat="provider in providers" value="{{provider}}">{{provider}}</option>
</select>
</div>
<button type="submit" class="btn">Send</button>
</form>
<form method="POST" name="twoFactorCodeForm" ng-submit="validate(provider, code)" ng-if="step=='code'">
<div class="control-group">
<input type="text" name="code" class="input-xlarge" placeholder="2fa code" ng-model="code" />
</div>
<button type="submit" class="btn">Validate</button>
</form>
</div>
</div>
</div>
</div>
angular.module("umbraco").controller("My2FA.LoginController",
function ($scope, $cookies, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService) {
$scope.code = "";
$scope.provider = "";
$scope.providers = [];
$scope.step = "send";
authResource.get2FAProviders()
.then(function (data) {
$scope.providers = data;
});
$scope.send = function (provider) {
$scope.provider = provider;
authResource.send2FACode(provider)
.then(function () {
$scope.step = "code";
});
};
$scope.validate = function (provider, code) {
$scope.code = code;
authResource.verify2FACode(provider, code)
.then(function (data) {
userService.setAuthenticationSuccessful(data);
$scope.submit(true);
});
};
});
/// <summary>
/// A custom Umbraco OWIN startup class to replace the Umbraco UserManager to enable 2FA
/// </summary>
public class CustomOwinStartup : UmbracoDefaultOwinStartup
{
/// <summary>
/// Completely override this method - do not call it's base implementation we'll wire up everything manually
/// </summary>
/// <param name="app"></param>
protected override void ConfigureServices(IAppBuilder app)
{
app.SetUmbracoLoggerFactory();
var applicationContext = ApplicationContext.Current;
//Here's where we assign a custom UserManager called MyBackOfficeUserManager
app.ConfigureUserManagerForUmbracoBackOffice<MyBackOfficeUserManager, BackOfficeIdentityUser>(
applicationContext,
(options, context) =>
{
var membershipProvider = Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider();
//Create the custom MyBackOfficeUserManager
var userManager = MyBackOfficeUserManager.Create(options,
applicationContext.Services.UserService,
applicationContext.Services.ExternalLoginService,
membershipProvider);
return userManager;
});
}
/// <summary>
/// Override this method to enable the 2FA cookie middleware with Umbraco's 2FA cookie auth type
/// </summary>
/// <param name="app"></param>
/// <remarks>
/// This is required in order for 2FA to work since the validated username from validating credentials is stored
/// in a temporary cookie which is then fetched once the 2FA validation is done.
/// </remarks>
protected override void ConfigureMiddleware(IAppBuilder app)
{
app.UseTwoFactorSignInCookie(global::Umbraco.Core.Constants.Security.BackOfficeTwoFactorAuthenticationType, TimeSpan.FromMinutes(5));
base.ConfigureMiddleware(app);
}
}
/// <summary>
/// Subclass the default BackOfficeUserManager and extend it to support 2FA
/// </summary>
public class MyBackOfficeUserStore : BackOfficeUserStore
{
public MyBackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider)
: base(userService, externalLoginService, usersMembershipProvider)
{
}
/// <summary>
/// Override to support setting whether two factor authentication is enabled for the user
/// </summary>
/// <param name="user"/><param name="enabled"/>
/// <returns/>
/// <remarks>
/// This Demo does not persist any data, so this method doesn't really have any effect, if you wish
/// to have 2FA setup per user, you'll need to persist that somewhere and to do that you'd need to override
/// the IUserStore.UpdateAsync method by explicitly implementing that interface's method, calling the base
/// class logic and then updating your 2FA storage for the user. Similarly you'd have to do that for
/// IUserStore.DeleteAsync and IUserStore.CreateAsync.
///
/// This method is NOT designed to persist data! It's just meant to assign it, just like this
/// </remarks>
public override Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled)
{
user.TwoFactorEnabled = enabled;
return Task.FromResult(0);
}
/// <summary>
/// Returns whether two factor authentication is enabled for the user
/// </summary>
/// <param name="user"/>
/// <returns/>
/// <remarks>
/// This Demo does not persist any data, so this method for this Demo always returns true.
/// If you want to have 2FA configured per user, you will need to store that information somewhere.
/// See the notes above in the SetTwoFactorEnabledAsync method.
/// </remarks>
public override Task<bool> GetTwoFactorEnabledAsync(BackOfficeIdentityUser user)
{
return Task.FromResult(true);
//If you persisted this data somewhere then you could either look it up now, or you could
//explicitly implement all IUserStore "Find*" methods, call their base implementation and then lookup
//your persisted value and assign to the TwoFactorEnabled property of the resulting BackOfficeIdentityUser user.
//return Task.FromResult(user.TwoFactorEnabled);
}
}
/// <summary>
/// Subclass the default BackOfficeUserManager and extend it to support 2FA
/// </summary>
public class MyBackOfficeUserManager : BackOfficeUserManager, IUmbracoBackOfficeTwoFactorOptions
{
public MyBackOfficeUserManager(IUserStore<BackOfficeIdentityUser, int> store)
: base(store)
{
}
/// <summary>
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
/// </summary>
/// <param name="options"></param>
/// <param name="userService"></param>
/// <param name="externalLoginService"></param>
/// <param name="membershipProvider"></param>
/// <returns></returns>
public static MyBackOfficeUserManager Create(
IdentityFactoryOptions<MyBackOfficeUserManager> options,
IUserService userService,
IExternalLoginService externalLoginService,
MembershipProviderBase membershipProvider)
{
if (options == null) throw new ArgumentNullException("options");
if (userService == null) throw new ArgumentNullException("userService");
if (externalLoginService == null) throw new ArgumentNullException("externalLoginService");
var manager = new MyBackOfficeUserManager(new MyBackOfficeUserStore(userService, externalLoginService, membershipProvider));
manager.InitUserManager(manager, membershipProvider, options.DataProtectionProvider);
//Here you can specify the 2FA providers that you want to implement,
//in this demo we are using the custom AcceptAnyCodeProvider - which literally accepts any code - do not actually use this!
var dataProtectionProvider = options.DataProtectionProvider;
manager.RegisterTwoFactorProvider("test", new AcceptAnyCodeProvider(dataProtectionProvider.Create("Testing")));
return manager;
}
/// <summary>
/// Override to return true
/// </summary>
public override bool SupportsUserTwoFactor
{
get { return true; }
}
/// <summary>
/// Return the view for the 2FA screen
/// </summary>
/// <param name="owinContext"></param>
/// <param name="umbracoContext"></param>
/// <param name="username"></param>
/// <returns></returns>
public string GetTwoFactorView(IOwinContext owinContext, UmbracoContext umbracoContext, string username)
{
return "../App_Plugins/2FA/2fa-login.html";
}
}
/// <summary>
/// Silly IUserTokenProvider for this Demo to be used for the 2FA provider, this will generate a code but not send it anywhere
/// (which is what the base class does), and then we override the ValidateAsync method to validate any code given - do not actually use this!
/// </summary>
public class AcceptAnyCodeProvider : DataProtectorTokenProvider<BackOfficeIdentityUser, int>, IUserTokenProvider<BackOfficeIdentityUser, int>
{
public AcceptAnyCodeProvider(IDataProtector protector)
: base(protector)
{
}
/// <summary>
/// Explicitly implement this interface method - which overrides the base class's implementation
/// </summary>
/// <param name="purpose"></param>
/// <param name="token"></param>
/// <param name="manager"></param>
/// <param name="user"></param>
/// <returns></returns>
Task<bool> IUserTokenProvider<BackOfficeIdentityUser, int>.ValidateAsync(string purpose, string token, UserManager<BackOfficeIdentityUser, int> manager, BackOfficeIdentityUser user)
{
return Task.FromResult(true);
}
}
@biapar
Copy link

biapar commented Jun 19, 2017

Very long code but simple to read...

@bmiddleton1976
Copy link

bmiddleton1976 commented Dec 4, 2020

I've found a fix for this now by using registering via application_starting and not owin as per the following:

https://github.com/ng-soft/umbraco-2fa-with-google-authenticator/blob/master/YubiKey2Factor/2FactorAuthentication/Middleware/TwoFactorEventHandler.cs

I'll leave this here in case anyone else stumbles upon it.

A real long shot here due to it's age, but I've implemented this in Umbraco 7.1.5, all works except I'm logged out after 1 minute of inactivity.

The following is logged by Umbraco:

Could not validate XSRF token
System.Web.Mvc.HttpAntiForgeryException (0x80004005): The provided anti-forgery token was meant for user "", but the current user is "current users email".

Having checked the following:

https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.WebPages/Helpers/AntiXsrf/TokenValidator.cs

It appears that the tokens being compared are X-UMB-XSRF-TOKEN in the header, and UMB-XSRF-TOKEN in the cookies. Both always match, so the tokens are always containing "" as the username. The user check is done using Identity, so when the token is created, the user has not yet been set in Identity.

I just wondered if you had experienced such issues / any thoughts on how we might resolve this? It all seems very similar to the issue with the offroad code plugin and as such I'm starting to think this is a wider issue with implementing 2FA in Umbraco.

Thanks in advance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment