diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs
index 6519f8a36b..bd943309d0 100644
--- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs
+++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs
@@ -137,11 +137,11 @@ namespace Umbraco.Core.Security
{
var id = Convert.ToString(user.Id);
if (await UserManager.GetTwoFactorEnabledAsync(user.Id)
- && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0
- && await AuthenticationManager.TwoFactorBrowserRememberedAsync(id) == false)
+ && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0)
{
var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
+ identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName));
AuthenticationManager.SignIn(identity);
return SignInStatus.RequiresVerification;
}
@@ -197,7 +197,7 @@ namespace Umbraco.Core.Security
}
///
- /// Get the user id that has been verified already or null.
+ /// Get the user id that has been verified already or -1.
///
///
///
@@ -212,5 +212,19 @@ namespace Umbraco.Core.Security
}
return -1;
}
+
+ ///
+ /// Get the username that has been verified already or null.
+ ///
+ ///
+ public async Task GetVerifiedUserNameAsync()
+ {
+ var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
+ if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserName()) == false)
+ {
+ return result.Identity.GetUserName();
+ }
+ return null;
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js
index ad29bd93f3..40f1dbb807 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js
@@ -30,13 +30,11 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
umbRequestHelper.getApiUrl(
"authenticationApiBaseUrl",
"PostSend2FACode"),
- {
- provider: provider
- }),
+ angular.toJson(provider)),
'Could not send code');
},
- verify2FACode: function (code) {
+ verify2FACode: function (provider, code) {
return umbRequestHelper.resourcePromise(
$http.post(
@@ -44,7 +42,8 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
"authenticationApiBaseUrl",
"PostVerify2FACode"),
{
- code: code
+ code: code,
+ provider: provider
}),
'Could not verify code');
},
diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs
index 09aa9b3279..0489fcbb70 100644
--- a/src/Umbraco.Web/Editors/AuthenticationController.cs
+++ b/src/Umbraco.Web/Editors/AuthenticationController.cs
@@ -139,18 +139,7 @@ namespace Umbraco.Web.Editors
//get the user
var user = Security.GetBackOfficeUser(loginModel.Username);
- var userDetail = Mapper.Map(user);
- //update the userDetail and set their remaining seconds
- userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds;
-
- //create a response with the userDetail object
- var response = Request.CreateResponse(HttpStatusCode.OK, userDetail);
-
- //ensure the user is set for the current request
- Request.SetPrincipalForRequest(user);
-
- return response;
-
+ return SetPrincipalAndReturnUserDetail(user);
case SignInStatus.RequiresVerification:
var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions;
@@ -246,9 +235,8 @@ namespace Umbraco.Web.Editors
var userId = await SignInManager.GetVerifiedUserIdAsync();
if (userId < 0)
{
- //TODO: Or just return not found?
- throw new HttpResponseException(
- Request.CreateValidationErrorResponse("No verified user found"));
+ Logger.Warn("Get2FAProviders :: No verified user found, returning 404");
+ throw new HttpResponseException(HttpStatusCode.NotFound);
}
var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId);
return userFactors;
@@ -257,25 +245,52 @@ namespace Umbraco.Web.Editors
[SetAngularAntiForgeryTokens]
public async Task PostSend2FACode([FromBody]string provider)
{
+ if (provider.IsNullOrWhiteSpace())
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+
+ var userId = await SignInManager.GetVerifiedUserIdAsync();
+ if (userId < 0)
+ {
+ Logger.Warn("Get2FAProviders :: No verified user found, returning 404");
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+ }
+
// Generate the token and send it
if (await SignInManager.SendTwoFactorCodeAsync(provider) == false)
{
- throw new HttpResponseException(
- Request.CreateValidationErrorResponse("Invalid code"));
+ return BadRequest("Invalid code");
}
return Ok();
}
[SetAngularAntiForgeryTokens]
- public async Task PostVerify2FACode([FromBody]string code)
+ public async Task PostVerify2FACode(Verify2FACodeModel model)
{
- // Generate the token and send it
- if (await SignInManager.SendTwoFactorCodeAsync(code) == false)
+ if (ModelState.IsValid == false)
{
- throw new HttpResponseException(
- Request.CreateValidationErrorResponse("Invalid code"));
+ return Request.CreateValidationErrorResponse(ModelState);
+ }
+
+ var userName = await SignInManager.GetVerifiedUserNameAsync();
+ if (userName == null)
+ {
+ Logger.Warn("Get2FAProviders :: No verified user found, returning 404");
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+ }
+
+ var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: true, rememberBrowser: false);
+ switch (result)
+ {
+ case SignInStatus.Success:
+ //get the user
+ var user = Security.GetBackOfficeUser(userName);
+ return SetPrincipalAndReturnUserDetail(user);
+ case SignInStatus.LockedOut:
+ return Request.CreateValidationErrorResponse("User is locked out");
+ case SignInStatus.Failure:
+ default:
+ return Request.CreateValidationErrorResponse("Invalid code");
}
- return Ok();
}
///
@@ -314,6 +329,30 @@ namespace Umbraco.Web.Editors
return Request.CreateResponse(HttpStatusCode.OK);
}
+
+
+ ///
+ /// This is used when the user is auth'd successfully and we need to return an OK with user details along with setting the current Principal in the request
+ ///
+ ///
+ ///
+ private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user)
+ {
+ if (user == null) throw new ArgumentNullException("user");
+
+ var userDetail = Mapper.Map(user);
+ //update the userDetail and set their remaining seconds
+ userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds;
+
+ //create a response with the userDetail object
+ var response = Request.CreateResponse(HttpStatusCode.OK, userDetail);
+
+ //ensure the user is set for the current request
+ Request.SetPrincipalForRequest(user);
+
+ return response;
+ }
+
private string ConstructCallbackUrl(int userId, string code)
{
// Get an mvc helper to get the url
diff --git a/src/Umbraco.Web/Models/SendCodeViewModel.cs b/src/Umbraco.Web/Models/SendCodeViewModel.cs
new file mode 100644
index 0000000000..31c6644089
--- /dev/null
+++ b/src/Umbraco.Web/Models/SendCodeViewModel.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models
+{
+ ///
+ /// Used for 2FA verification
+ ///
+ [DataContract(Name = "code", Namespace = "")]
+ public class Verify2FACodeModel
+ {
+ [Required]
+ [DataMember(Name = "code", IsRequired = true)]
+ public string Code { get; set; }
+
+ [Required]
+ [DataMember(Name = "provider", IsRequired = true)]
+ public string Provider { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs
index f0c0861059..cda9f04fad 100644
--- a/src/Umbraco.Web/Security/WebSecurity.cs
+++ b/src/Umbraco.Web/Security/WebSecurity.cs
@@ -189,13 +189,13 @@ namespace Umbraco.Web.Security
}
///
- /// Returns the back office IUser instance for the username specified
+ /// Gets (and creates if not found) the back office instance for the username specified
///
///
///
///
- /// This will return an Iuser instance no matter what membership provider is installed for the back office, it will automatically
- /// create any missing Iuser accounts if one is not found and a custom membership provider is being used.
+ /// This will return an instance no matter what membership provider is installed for the back office, it will automatically
+ /// create any missing accounts if one is not found and a custom membership provider or is being used.
///
internal IUser GetBackOfficeUser(string username)
{
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index c26311144e..ce16abaf5f 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -348,6 +348,7 @@
+