From 86021c50524c8456dde062dc32f7b03db57b75c3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Feb 2017 00:47:28 +1100 Subject: [PATCH] Adds remaining core methods to make 2FA providers work if you know how to wire it up --- .../Security/BackOfficeSignInManager.cs | 20 ++++- .../src/common/resources/auth.resource.js | 9 +- .../Editors/AuthenticationController.cs | 85 ++++++++++++++----- src/Umbraco.Web/Models/SendCodeViewModel.cs | 22 +++++ src/Umbraco.Web/Security/WebSecurity.cs | 6 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 6 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Web/Models/SendCodeViewModel.cs 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 @@ +