From 3f4f846cbb1bbfc4366642219d93569f28422fcc Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Wed, 24 Sep 2014 15:04:23 -0700 Subject: [PATCH] Add social auth and two factor - Merge Authentication into Core - Add social login support - Add two factor support - Rework options model for startup --- Identity.sln | 28 +- .../Controllers/AccountController.cs | 353 +++++++++++++++--- .../Controllers/HomeController.cs | 4 +- .../Controllers/ManageController.cs | 132 ++++--- .../Models/AccountViewModels.cs | 61 ++- .../Models/ManageViewModels.cs | 14 +- samples/IdentitySample.Mvc/Program.cs | 56 --- samples/IdentitySample.Mvc/Startup.cs | 170 +++++++-- .../Views/Account/ConfirmEmail.cshtml | 12 + .../Views/Account/DisplayEmail.cshtml | 13 + .../Account/ExternalLoginConfirmation.cshtml | 41 ++ .../Views/Account/ExternalLoginFailure.cshtml | 10 + .../Views/Account/ForgotPassword.cshtml | 34 ++ .../Account/ForgotPasswordConfirmation.cshtml | 19 + .../Views/Account/Login.cshtml | 36 +- .../Views/Account/Manage.cshtml | 21 -- .../Views/Account/Register.cshtml | 4 +- .../Views/Account/ResetPassword.cshtml | 47 +++ .../Account/ResetPasswordConfirmation.cshtml | 14 + .../Views/Account/SendCode.cshtml | 27 ++ .../Views/Account/VerifyCode.cshtml | 43 +++ .../Account/_ChangePasswordPartial.cshtml | 36 -- .../Views/Manage/Index.cshtml | 42 ++- .../Views/Manage/ManageLogins.cshtml | 2 +- .../Views/Manage/SetPassword.cshtml | 12 +- samples/IdentitySample.Mvc/project.json | 13 +- .../BuilderExtensions.cs | 30 -- .../HttpAuthenticationManager.cs | 71 ---- .../IdentityBuilderExtensions.cs | 19 - ...osoft.AspNet.Identity.Authentication.kproj | 20 - .../SecurityStampValidator.cs | 92 ----- .../project.json | 31 -- .../IdentityDbContext.cs | 20 +- ...dentityEntityFrameworkBuilderExtensions.cs | 54 +++ ...ityFrameworkServiceCollectionExtensions.cs | 31 +- .../RoleStore.cs | 30 +- .../UserStore.cs | 43 +-- .../BuilderExtensions.cs | 31 ++ .../ClaimsIdentityOptions.cs | 25 +- .../DataProtectionTokenProvider.cs | 177 +++++++++ .../EmailTokenProvider.cs | 43 +-- .../ExternalLoginInfo.cs | 13 + .../HttpContextExtensions.cs | 75 ++++ .../IAuthenticationManager.cs | 25 -- .../IPasswordValidator.cs | 4 +- .../ISecurityStampValidator.cs | 14 + .../IUserTokenProvider.cs | 10 +- .../IdentityBuilder.cs | 35 +- .../IdentityOptions.cs | 63 +++- src/Microsoft.AspNet.Identity/IdentityRole.cs | 20 +- .../IdentityServiceCollectionExtensions.cs | 44 ++- .../IdentityServices.cs | 35 +- src/Microsoft.AspNet.Identity/IdentityUser.cs | 16 +- .../LockoutOptions.cs | 13 +- .../PasswordOptions.cs | 19 +- .../PasswordValidator.cs | 2 +- .../PhoneNumberTokenProvider.cs | 33 +- .../Properties/AssemblyInfo.cs | 6 + .../Properties/Resources.Designer.cs | 120 +++++- src/Microsoft.AspNet.Identity/Resources.resx | 30 +- .../Rfc6238AuthenticationService.cs | 4 +- .../SecurityStampValidator.cs | 74 ++++ .../SignInManager.cs | 139 +++++-- .../SignInOptions.cs | 2 - .../TotpSecurityStampBasedTokenProvider.cs | 25 +- .../UserLoginInfo.cs | 21 +- src/Microsoft.AspNet.Identity/UserManager.cs | 123 +++--- src/Microsoft.AspNet.Identity/project.json | 3 + ....AspNet.Identity.Authentication.Test.kproj | 21 -- .../project.json | 33 -- .../HttpSignInTest.cs | 2 +- .../project.json | 1 - .../RoleStoreTest.cs | 2 +- .../TestIdentityFactory.cs | 4 +- .../SqlStoreTestBase.cs | 4 +- .../UserStoreGuidKeyTest.cs | 4 +- .../UserStoreTest.cs | 4 +- .../IdentityBuilderTest.cs | 6 +- .../IdentityOptionsTest.cs | 1 + .../PasswordValidatorTest.cs | 16 +- .../SecurityStampValidatorTest.cs | 57 +-- .../SignInManagerTest.cs} | 147 +++++--- .../UserManagerTest.cs | 33 +- test/Shared/MockHelpers.cs | 9 +- test/Shared/UserManagerTestBase.cs | 206 ++++------ 85 files changed, 2159 insertions(+), 1320 deletions(-) delete mode 100644 samples/IdentitySample.Mvc/Program.cs create mode 100644 samples/IdentitySample.Mvc/Views/Account/ConfirmEmail.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/DisplayEmail.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/ExternalLoginFailure.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/ForgotPasswordConfirmation.cshtml delete mode 100644 samples/IdentitySample.Mvc/Views/Account/Manage.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/ResetPasswordConfirmation.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/SendCode.cshtml create mode 100644 samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml delete mode 100644 samples/IdentitySample.Mvc/Views/Account/_ChangePasswordPartial.cshtml delete mode 100644 src/Microsoft.AspNet.Identity.Authentication/BuilderExtensions.cs delete mode 100644 src/Microsoft.AspNet.Identity.Authentication/HttpAuthenticationManager.cs delete mode 100644 src/Microsoft.AspNet.Identity.Authentication/IdentityBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNet.Identity.Authentication/Microsoft.AspNet.Identity.Authentication.kproj delete mode 100644 src/Microsoft.AspNet.Identity.Authentication/SecurityStampValidator.cs delete mode 100644 src/Microsoft.AspNet.Identity.Authentication/project.json create mode 100644 src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Identity/BuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs create mode 100644 src/Microsoft.AspNet.Identity/ExternalLoginInfo.cs create mode 100644 src/Microsoft.AspNet.Identity/HttpContextExtensions.cs delete mode 100644 src/Microsoft.AspNet.Identity/IAuthenticationManager.cs create mode 100644 src/Microsoft.AspNet.Identity/ISecurityStampValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Identity/SecurityStampValidator.cs delete mode 100644 test/Microsoft.AspNet.Identity.Authentication.Test/Microsoft.AspNet.Identity.Authentication.Test.kproj delete mode 100644 test/Microsoft.AspNet.Identity.Authentication.Test/project.json rename test/{Microsoft.AspNet.Identity.Authentication.Test => Microsoft.AspNet.Identity.Test}/SecurityStampValidatorTest.cs (76%) rename test/{Microsoft.AspNet.Identity.Authentication.Test/HttpSignInTest.cs => Microsoft.AspNet.Identity.Test/SignInManagerTest.cs} (80%) diff --git a/Identity.sln b/Identity.sln index bc462d16f5..0f887fcf61 100644 --- a/Identity.sln +++ b/Identity.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.21722.1 +VisualStudioVersion = 14.0.22013.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0F647068-6602-4E24-B1DC-8ED91481A50A}" EndProject @@ -11,12 +11,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity", EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity.SqlServer", "src\Microsoft.AspNet.Identity.SqlServer\Microsoft.AspNet.Identity.SqlServer.kproj", "{AD42BAFB-1993-4FAF-A280-3711A9F33E2F}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity.Authentication", "src\Microsoft.AspNet.Identity.Authentication\Microsoft.AspNet.Identity.Authentication.kproj", "{7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity.Test", "test\Microsoft.AspNet.Identity.Test\Microsoft.AspNet.Identity.Test.kproj", "{2CF3927B-19E4-4866-9BAA-2C131580E7C3}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity.Authentication.Test", "test\Microsoft.AspNet.Identity.Authentication.Test\Microsoft.AspNet.Identity.Authentication.Test.kproj", "{823453CC-5846-4D49-B343-15BC0074CA60}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity.InMemory.Test", "test\Microsoft.AspNet.Identity.InMemory.Test\Microsoft.AspNet.Identity.InMemory.Test.kproj", "{65161409-C4C4-4D63-A73B-231FCFF4D503}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Identity.SqlServer.Test", "test\Microsoft.AspNet.Identity.SqlServer.Test\Microsoft.AspNet.Identity.SqlServer.Test.kproj", "{B4C067C1-F934-493C-9DBC-19E8CA305613}" @@ -62,16 +58,6 @@ Global {AD42BAFB-1993-4FAF-A280-3711A9F33E2F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {AD42BAFB-1993-4FAF-A280-3711A9F33E2F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {AD42BAFB-1993-4FAF-A280-3711A9F33E2F}.Release|x86.ActiveCfg = Release|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Debug|x86.ActiveCfg = Debug|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Release|Any CPU.Build.0 = Release|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9}.Release|x86.ActiveCfg = Release|Any CPU {2CF3927B-19E4-4866-9BAA-2C131580E7C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2CF3927B-19E4-4866-9BAA-2C131580E7C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {2CF3927B-19E4-4866-9BAA-2C131580E7C3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -82,16 +68,6 @@ Global {2CF3927B-19E4-4866-9BAA-2C131580E7C3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {2CF3927B-19E4-4866-9BAA-2C131580E7C3}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2CF3927B-19E4-4866-9BAA-2C131580E7C3}.Release|x86.ActiveCfg = Release|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Debug|x86.ActiveCfg = Debug|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Release|Any CPU.Build.0 = Release|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {823453CC-5846-4D49-B343-15BC0074CA60}.Release|x86.ActiveCfg = Release|Any CPU {65161409-C4C4-4D63-A73B-231FCFF4D503}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {65161409-C4C4-4D63-A73B-231FCFF4D503}.Debug|Any CPU.Build.0 = Debug|Any CPU {65161409-C4C4-4D63-A73B-231FCFF4D503}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -139,9 +115,7 @@ Global GlobalSection(NestedProjects) = preSolution {1729302E-A58E-4652-B639-5B6B68DA2748} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {AD42BAFB-1993-4FAF-A280-3711A9F33E2F} = {0F647068-6602-4E24-B1DC-8ED91481A50A} - {7B4CFF5A-1948-45EC-B170-6EB7C039B2F9} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {2CF3927B-19E4-4866-9BAA-2C131580E7C3} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} - {823453CC-5846-4D49-B343-15BC0074CA60} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} {65161409-C4C4-4D63-A73B-231FCFF4D503} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} {B4C067C1-F934-493C-9DBC-19E8CA305613} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} {813B36FE-BBA5-4449-B157-6EBBA5ED02CA} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} diff --git a/samples/IdentitySample.Mvc/Controllers/AccountController.cs b/samples/IdentitySample.Mvc/Controllers/AccountController.cs index 1b17e8382e..9b4d655489 100644 --- a/samples/IdentitySample.Mvc/Controllers/AccountController.cs +++ b/samples/IdentitySample.Mvc/Controllers/AccountController.cs @@ -1,30 +1,30 @@ -using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Identity; using Microsoft.AspNet.Mvc; -using IdentitySample.Models; +using Microsoft.AspNet.Mvc.Rendering; +using System.Linq; +using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; namespace IdentitySample.Models { [Authorize] - public class AccountController : Controller + public class AccountController(UserManager userManager, SignInManager signInManager) + : Controller { - public AccountController(UserManager userManager, SignInManager signInManager) - { - UserManager = userManager; - SignInManager = signInManager; - } + public UserManager UserManager { get; } = userManager; - public UserManager UserManager { get; private set; } - - public SignInManager SignInManager { get; private set; } + public SignInManager SignInManager { get; } = signInManager; // // GET: /Account/Login + [HttpGet] [AllowAnonymous] public IActionResult Login(string returnUrl = null) { ViewBag.ReturnUrl = returnUrl; + ViewBag.LoginProviders = Context.GetExternalAuthenticationTypes().ToList(); return View(); } @@ -45,6 +45,8 @@ namespace IdentitySample.Models case SignInStatus.LockedOut: ModelState.AddModelError("", "User is locked out, try again later."); return View(model); + case SignInStatus.RequiresVerification: + return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); case SignInStatus.Failure: default: ModelState.AddModelError("", "Invalid username or password."); @@ -58,6 +60,7 @@ namespace IdentitySample.Models // // GET: /Account/Register + [HttpGet] [AllowAnonymous] public IActionResult Register() { @@ -73,54 +76,17 @@ namespace IdentitySample.Models { if (ModelState.IsValid) { - var user = new ApplicationUser { UserName = model.UserName }; + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { - await SignInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction("Index", "Home"); - } - else - { - AddErrors(result); - } - } - - // If we got this far, something failed, redisplay form - return View(model); - } - - // - // GET: /Account/Manage - public IActionResult Manage(ManageMessageId? message = null) - { - ViewBag.StatusMessage = - message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." - : message == ManageMessageId.Error ? "An error has occurred." - : ""; - ViewBag.ReturnUrl = Url.Action("Manage"); - return View(); - } - - // - // POST: /Account/Manage - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Manage(ManageUserViewModel model) - { - ViewBag.ReturnUrl = Url.Action("Manage"); - if (ModelState.IsValid) - { - var user = await GetCurrentUserAsync(); - var result = await UserManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); - if (result.Succeeded) - { - return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); - } - else - { - AddErrors(result); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Context.Request.Scheme); + await UserManager.SendEmailAsync(user, "Confirm your account", "Please confirm your account by clicking this link: link"); + ViewBag.Link = callbackUrl; + return View("DisplayEmail"); } + AddErrors(result); } // If we got this far, something failed, redisplay form @@ -137,6 +103,277 @@ namespace IdentitySample.Models return RedirectToAction("Index", "Home"); } + // + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public IActionResult ExternalLogin(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider + var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }); + var properties = Context.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return new ChallengeResult(provider, properties); + } + + // + // GET: /Account/ExternalLoginCallback + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl = null) + { + var info = await Context.GetExternalLoginInfo(); + if (info == null) + { + return RedirectToAction("Login"); + } + + // Sign in the user with this external login provider if the user already has a login + var result = await SignInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, + isPersistent: false); + switch (result) + { + case SignInStatus.Success: + return RedirectToLocal(returnUrl); + case SignInStatus.LockedOut: + return View("Lockout"); + case SignInStatus.RequiresVerification: + return RedirectToAction("SendCode", new { ReturnUrl = returnUrl }); + case SignInStatus.Failure: + default: + // If the user does not have an account, then prompt the user to create an account + ViewBag.ReturnUrl = returnUrl; + ViewBag.LoginProvider = info.LoginProvider; + // REVIEW: handle case where email not in claims? + var email = info.ExternalIdentity.FindFirstValue(ClaimTypes.Email); + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); + } + } + + // + // POST: /Account/ExternalLoginConfirmation + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null) + { + if (User.Identity.IsAuthenticated) + { + return RedirectToAction("Index", "Manage"); + } + + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await Context.GetExternalLoginInfo(); + if (info == null) + { + return View("ExternalLoginFailure"); + } + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + var result = await UserManager.CreateAsync(user); + if (result.Succeeded) + { + result = await UserManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + await SignInManager.SignInAsync(user, isPersistent: false); + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } + + ViewBag.ReturnUrl = returnUrl; + return View(model); + } + + // REVIEW: We should make this a POST + // + // GET: /Account/ConfirmEmail + [HttpGet] + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return View("Error"); + } + var user = await UserManager.FindByIdAsync(userId); + if (user == null) + { + return View("Error"); + } + var result = await UserManager.ConfirmEmailAsync(user, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + // + // GET: /Account/ForgotPassword + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPassword() + { + return View(); + } + + // + // POST: /Account/ForgotPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ForgotPassword(ForgotPasswordViewModel model) + { + if (ModelState.IsValid) + { + var user = await UserManager.FindByNameAsync(model.Email); + if (user == null || !(await UserManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return View("ForgotPasswordConfirmation"); + } + + var code = await UserManager.GeneratePasswordResetTokenAsync(user); + var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Context.Request.Scheme); + await UserManager.SendEmailAsync(user, "Reset Password", "Please reset your password by clicking here: link"); + ViewBag.Link = callbackUrl; + return View("ForgotPasswordConfirmation"); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/ForgotPasswordConfirmation + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/ResetPassword + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPassword(string code = null) + { + return code == null ? View("Error") : View(); + } + + // + // POST: /Account/ResetPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ResetPassword(ResetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await UserManager.FindByNameAsync(model.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToAction("ResetPasswordConfirmation", "Account"); + } + var result = await UserManager.ResetPasswordAsync(user, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction("ResetPasswordConfirmation", "Account"); + } + AddErrors(result); + return View(); + } + + // + // GET: /Account/ResetPasswordConfirmation + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/SendCode + [HttpGet] + [AllowAnonymous] + public async Task SendCode(string returnUrl = null, bool rememberMe = false) + { + var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(user); + var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList(); + return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/SendCode + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task SendCode(SendCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(); + } + + // Generate the token and send it + if (!await SignInManager.SendTwoFactorCodeAsync(model.SelectedProvider)) + { + return View("Error"); + } + return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); + } + + // + // GET: /Account/VerifyCode + [HttpGet] + [AllowAnonymous] + public async Task VerifyCode(string provider, bool rememberMe, string returnUrl = null) + { + var user = await SignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + // Remove before production + ViewBag.Status = "For DEMO purposes the current " + provider + " code is: " + await UserManager.GenerateTwoFactorTokenAsync(user, provider); + return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/VerifyCode + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task VerifyCode(VerifyCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); + switch (result) + { + case SignInStatus.Success: + return RedirectToLocal(model.ReturnUrl); + case SignInStatus.LockedOut: + return View("Lockout"); + default: + ModelState.AddModelError("", "Invalid code."); + return View(model); + } + } + #region Helpers private void AddErrors(IdentityResult result) @@ -152,12 +389,6 @@ namespace IdentitySample.Models return await UserManager.FindByIdAsync(Context.User.Identity.GetUserId()); } - public enum ManageMessageId - { - ChangePasswordSuccess, - Error - } - private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) diff --git a/samples/IdentitySample.Mvc/Controllers/HomeController.cs b/samples/IdentitySample.Mvc/Controllers/HomeController.cs index 3d04fe37e5..3a831a25ed 100644 --- a/samples/IdentitySample.Mvc/Controllers/HomeController.cs +++ b/samples/IdentitySample.Mvc/Controllers/HomeController.cs @@ -4,12 +4,13 @@ namespace IdentitySample.Models { public class HomeController : Controller { + [HttpGet] public IActionResult Index() { return View(); } - [Authorize] + [HttpGet] public IActionResult About() { ViewBag.Message = "Your app description page."; @@ -17,6 +18,7 @@ namespace IdentitySample.Models return View(); } + [HttpGet] public IActionResult Contact() { ViewBag.Message = "Your contact page."; diff --git a/samples/IdentitySample.Mvc/Controllers/ManageController.cs b/samples/IdentitySample.Mvc/Controllers/ManageController.cs index 6cd4d65085..1f2ee76450 100644 --- a/samples/IdentitySample.Mvc/Controllers/ManageController.cs +++ b/samples/IdentitySample.Mvc/Controllers/ManageController.cs @@ -1,26 +1,24 @@ +using IdentitySample.Models; +using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Mvc; -using IdentitySample.Models; +using System.Linq; using System.Security.Principal; using System.Threading.Tasks; namespace IdentitySample { [Authorize] - public class ManageController : Controller + public class ManageController(UserManager userManager, SignInManager signInManager) + : Controller { - public ManageController(UserManager userManager, SignInManager signInManager) - { - UserManager = userManager; - SignInManager = signInManager; - } + public UserManager UserManager { get; } = userManager; - public UserManager UserManager { get; private set; } - - public SignInManager SignInManager { get; private set; } + public SignInManager SignInManager { get; } = signInManager; // // GET: /Account/Index + [HttpGet] public async Task Index(ManageMessageId? message = null) { ViewBag.StatusMessage = @@ -46,6 +44,7 @@ namespace IdentitySample // // GET: /Account/RemoveLogin + [HttpGet] public async Task RemoveLogin() { var user = await GetCurrentUserAsync(); @@ -92,7 +91,6 @@ namespace IdentitySample return View(model); } // Generate the token and send it -#if ASPNET50 var code = await UserManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), model.Number); if (UserManager.SmsService != null) { @@ -103,19 +101,19 @@ namespace IdentitySample }; await UserManager.SmsService.SendAsync(message); } -#endif return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number }); } // // POST: /Manage/RememberBrowser [HttpPost] + [ValidateAntiForgeryToken] public async Task RememberBrowser() { var user = await GetCurrentUserAsync(); if (user != null) { - await SignInManager.RememberTwoFactorClient(user); + await SignInManager.RememberTwoFactorClientAsync(user); await SignInManager.SignInAsync(user, isPersistent: false); } return RedirectToAction("Index", "Manage"); @@ -124,6 +122,7 @@ namespace IdentitySample // // POST: /Manage/ForgetBrowser [HttpPost] + [ValidateAntiForgeryToken] public async Task ForgetBrowser() { await SignInManager.ForgetTwoFactorClientAsync(); @@ -133,6 +132,7 @@ namespace IdentitySample // // POST: /Manage/EnableTFA [HttpPost] + [ValidateAntiForgeryToken] public async Task EnableTFA() { var user = await GetCurrentUserAsync(); @@ -148,6 +148,7 @@ namespace IdentitySample // // POST: /Manage/DisableTFA [HttpPost] + [ValidateAntiForgeryToken] public async Task DisableTFA() { var user = await GetCurrentUserAsync(); @@ -161,14 +162,13 @@ namespace IdentitySample // // GET: /Account/VerifyPhoneNumber + [HttpGet] public async Task VerifyPhoneNumber(string phoneNumber) { // This code allows you exercise the flow without actually sending codes // For production use please register a SMS provider in IdentityConfig and generate a code here. -#if ASPNET50 var code = await UserManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), phoneNumber); ViewBag.Status = "For DEMO purposes only, the current code is " + code; -#endif return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); } @@ -199,6 +199,7 @@ namespace IdentitySample // // GET: /Account/RemovePhoneNumber + [HttpGet] public async Task RemovePhoneNumber() { var user = await GetCurrentUserAsync(); @@ -216,6 +217,7 @@ namespace IdentitySample // // GET: /Manage/ChangePassword + [HttpGet] public IActionResult ChangePassword() { return View(); @@ -248,6 +250,7 @@ namespace IdentitySample // // GET: /Manage/SetPassword + [HttpGet] public IActionResult SetPassword() { return View(); @@ -279,55 +282,63 @@ namespace IdentitySample return RedirectToAction("Index", new { Message = ManageMessageId.Error }); } + //GET: /Account/Manage + [HttpGet] + public async Task ManageLogins(ManageMessageId? message = null) + { + ViewBag.StatusMessage = + message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + : message == ManageMessageId.AddLoginSuccess ? "The external login was added." + : message == ManageMessageId.Error ? "An error has occurred." + : ""; + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var userLogins = await UserManager.GetLoginsAsync(user); + var otherLogins = Context.GetExternalAuthenticationTypes().Where(auth => userLogins.All(ul => auth.AuthenticationType != ul.LoginProvider)).ToList(); + ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1; + return View(new ManageLoginsViewModel + { + CurrentLogins = userLogins, + OtherLogins = otherLogins + }); + } + // - // GET: /Account/Manage - //public async Task ManageLogins(ManageMessageId? message) - //{ - // ViewBag.StatusMessage = - // message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." - // : message == ManageMessageId.Error ? "An error has occurred." - // : ""; - // var user = await GetCurrentUserAsync(); - // if (user == null) - // { - // return View("Error"); - // } - // var userLogins = await UserManager.GetLoginsAsync(user); - //var otherLogins = AuthenticationManager.GetExternalAuthenticationTypes().Where(auth => userLogins.All(ul => auth.AuthenticationType != ul.LoginProvider)).ToList(); - //ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1; - //return View(new ManageLoginsViewModel - //{ - // CurrentLogins = userLogins, - // OtherLogins = otherLogins - //}); - //} + // POST: /Manage/LinkLogin + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult LinkLogin(string provider) + { + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Action("LinkLoginCallback", "Manage"); + var properties = Context.ConfigureExternalAuthenticationProperties(provider, redirectUrl, User.Identity.GetUserId()); + return new ChallengeResult(provider, properties); + } - //// - //// POST: /Manage/LinkLogin - //[HttpPost] - //[ValidateAntiForgeryToken] - //public IActionResult LinkLogin(string provider) - //{ - // // Request a redirect to the external login provider to link a login for the current user - // return new AccountController.ChallengeResult(provider, Url.Action("LinkLoginCallback", "Manage"), User.Identity.GetUserId()); - //} - - //// - //// GET: /Manage/LinkLoginCallback - //public async Task LinkLoginCallback() - //{ - // var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId()); - // if (loginInfo == null) - // { - // return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error }); - // } - // var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); - // return result.Succeeded ? RedirectToAction("ManageLogins") : RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error }); - //} + // + // GET: /Manage/LinkLoginCallback + [HttpGet] + public async Task LinkLoginCallback() + { + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var info = await Context.GetExternalLoginInfo(User.Identity.GetUserId()); + if (info == null) + { + return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error }); + } + var result = await UserManager.AddLoginAsync(user, info); + var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error; + return RedirectToAction("ManageLogins", new { Message = message }); + } #region Helpers - // Used for XSRF protection when adding external logins - //private const string XsrfKey = "XsrfId"; private void AddErrors(IdentityResult result) { @@ -350,6 +361,7 @@ namespace IdentitySample public enum ManageMessageId { AddPhoneSuccess, + AddLoginSuccess, ChangePasswordSuccess, SetTwoFactorSuccess, SetPasswordSuccess, diff --git a/samples/IdentitySample.Mvc/Models/AccountViewModels.cs b/samples/IdentitySample.Mvc/Models/AccountViewModels.cs index 0abd19a22e..4979304b7f 100644 --- a/samples/IdentitySample.Mvc/Models/AccountViewModels.cs +++ b/samples/IdentitySample.Mvc/Models/AccountViewModels.cs @@ -1,31 +1,66 @@ -using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.Rendering; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace IdentitySample.Models { public class ExternalLoginConfirmationViewModel { [Required] - [Display(Name = "User name")] - public string UserName { get; set; } + [Display(Name = "Email")] + public string Email { get; set; } } - public class ManageUserViewModel + public class SendCodeViewModel + { + public string SelectedProvider { get; set; } + public ICollection Providers { get; set; } + public string ReturnUrl { get; set; } + public bool RememberMe { get; set; } + } + + public class VerifyCodeViewModel { [Required] - [DataType(DataType.Password)] - [Display(Name = "Current password")] - public string OldPassword { get; set; } + public string Provider { get; set; } + + [Required] + [Display(Name = "Code")] + public string Code { get; set; } + public string ReturnUrl { get; set; } + + [Display(Name = "Remember this browser?")] + public bool RememberBrowser { get; set; } + + public bool RememberMe { get; set; } + } + + public class ResetPasswordViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] - [Display(Name = "New password")] - public string NewPassword { get; set; } + [Display(Name = "Password")] + public string Password { get; set; } [DataType(DataType.Password)] - [Display(Name = "Confirm new password")] - [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } + + public class ForgotPasswordViewModel + { + [Required] + [Display(Name = "Email")] + public string Email { get; set; } } public class LoginViewModel @@ -46,8 +81,8 @@ namespace IdentitySample.Models public class RegisterViewModel { [Required] - [Display(Name = "User name")] - public string UserName { get; set; } + [Display(Name = "Email")] + public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] diff --git a/samples/IdentitySample.Mvc/Models/ManageViewModels.cs b/samples/IdentitySample.Mvc/Models/ManageViewModels.cs index 45b758aee6..75469967ca 100644 --- a/samples/IdentitySample.Mvc/Models/ManageViewModels.cs +++ b/samples/IdentitySample.Mvc/Models/ManageViewModels.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Identity; using Microsoft.AspNet.Mvc.Rendering; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -14,11 +15,11 @@ namespace IdentitySample.Models public bool BrowserRemembered { get; set; } } - //public class ManageLoginsViewModel - //{ - // public IList CurrentLogins { get; set; } - // public IList OtherLogins { get; set; } - //} + public class ManageLoginsViewModel + { + public IList CurrentLogins { get; set; } + public IList OtherLogins { get; set; } + } public class FactorViewModel { @@ -84,5 +85,4 @@ namespace IdentitySample.Models public string SelectedProvider { get; set; } public ICollection Providers { get; set; } } - } \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/Program.cs b/samples/IdentitySample.Mvc/Program.cs deleted file mode 100644 index 33600d411f..0000000000 --- a/samples/IdentitySample.Mvc/Program.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.Framework.ConfigurationModel; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.DependencyInjection.Fallback; -using Microsoft.AspNet.Hosting; -using System; -using System.Threading.Tasks; - -namespace MusicStore -{ - /// - /// This demonstrates how the application can be launched in a K console application. - /// k run command in the application folder will invoke this. - /// - public class Program - { - private readonly IServiceProvider _hostServiceProvider; - - public Program(IServiceProvider hostServiceProvider) - { - _hostServiceProvider = hostServiceProvider; - } - - public Task Main(string[] args) - { - //Add command line configuration source to read command line parameters. - var config = new Configuration(); - config.AddCommandLine(args); - - var serviceCollection = new ServiceCollection(); - serviceCollection.Add(HostingServices.GetDefaultServices(config)); - var services = serviceCollection.BuildServiceProvider(_hostServiceProvider); - - var context = new HostingContext() - { - Services = services, - Configuration = config, - ServerName = "Microsoft.AspNet.Server.WebListener", - ApplicationName = "MusicStore" - }; - - var engine = services.GetService(); - if (engine == null) - { - throw new Exception("TODO: IHostingEngine service not available exception"); - } - - using (engine.Start(context)) - { - Console.WriteLine("Started the server.."); - Console.WriteLine("Press any key to stop the server"); - Console.ReadLine(); - } - return Task.FromResult(0); - } - } -} \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/Startup.cs b/samples/IdentitySample.Mvc/Startup.cs index 2b550e0e83..916969aa4e 100644 --- a/samples/IdentitySample.Mvc/Startup.cs +++ b/samples/IdentitySample.Mvc/Startup.cs @@ -1,30 +1,94 @@ +using System; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Diagnostics; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Authentication; using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.Security.Facebook; +using Microsoft.AspNet.Security.Google; +using Microsoft.AspNet.Security.Twitter; using Microsoft.Data.Entity; using Microsoft.Framework.ConfigurationModel; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.OptionsModel; using IdentitySample.Models; -using System; namespace IdentitySamples { - public class Startup + public static class UseExt { + + /** + * TODO: Middleware constructors need to take IOptionsAccessor + + * Move options setup into a different method? + + * Cookie options need to be different, named service/option instances? i.e. Singleton Named Options + + SetupNamedOption("ApplicationCookie", options => { }) + UseCookieAuthentication("ApplicationCookie") + SetupNamedOption("ExternalCookie", options => { }) + UseCookieAuthentication("ApplicationCookie") + + // Overloads which use default/no name + SetupOption(options => { }) + UseGoogleAuthentication() + + */ + + public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + //return builder.UseGoogleAuthentication(b => + // b.ApplicationServices.GetService>().Options); + } + + + + public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder builder, Func func) + { + return builder.UseGoogleAuthentication(func(builder)); + } + + public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder builder) + { + // This should go inside of the middleware delegate + return builder.UseFacebookAuthentication(b => + b.ApplicationServices.GetService>().Options); + } + + public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder builder, Func func) + { + return builder.UseFacebookAuthentication(func(builder)); + } + + public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder builder) + { + return builder.UseTwitterAuthentication(b => + b.ApplicationServices.GetService>().Options); + } + + public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder builder, Func func) + { + return builder.UseTwitterAuthentication(func(builder)); + } + } + + public partial class Startup() { + { + /* + * Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1' is found in both the registered sources, + * then the later source will win. By this way a Local config can be overridden by a different setting while deployed remotely. + */ + Configuration = new Configuration() + .AddJsonFile("LocalConfig.json") + .AddEnvironmentVariables(); //All environment variables in the process's context flow in as configuration values. + } + + public IConfiguration Configuration { get; private set; } + public void Configure(IApplicationBuilder app) { - /* Adding IConfiguration as a service in the IoC to avoid instantiating Configuration again. - * Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1' is found in both the registered sources, - * then the later source will win. By this way a Local config can be overridden by a different setting while deployed remotely. - */ - var configuration = new Configuration(); - configuration.AddJsonFile("LocalConfig.json"); - configuration.AddEnvironmentVariables(); //All environment variables in the process's context flow in as configuration values. - app.UseServices(services => { // Add EF services to the services container @@ -34,21 +98,52 @@ namespace IdentitySamples // Configure DbContext services.SetupOptions(options => { - options.DefaultAdminUserName = configuration.Get("DefaultAdminUsername"); - options.DefaultAdminPassword = configuration.Get("DefaultAdminPassword"); - options.UseSqlServer(configuration.Get("Data:IdentityConnection:ConnectionString")); + options.DefaultAdminUserName = Configuration.Get("DefaultAdminUsername"); + options.DefaultAdminPassword = Configuration.Get("DefaultAdminPassword"); + options.UseSqlServer(Configuration.Get("Data:IdentityConnection:ConnectionString")); }); // Add Identity services to the services container - services.AddIdentitySqlServer() - .AddAuthentication() - .SetupOptions(options => - { - options.Password.RequireDigit = false; - options.Password.RequireLowercase = false; - options.Password.RequireUppercase = false; - options.Password.RequireNonLetterOrDigit = false; - }); + services.AddDefaultIdentity(Configuration); + + // move this into add identity along with the + //service.SetupOptions(options => options.SignInAsAuthenticationType = "External") + + services.SetupOptions(options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonLetterOrDigit = false; + options.SecurityStampValidationInterval = TimeSpan.Zero; + }); + services.SetupOptions(options => + { + options.ClientId = "514485782433-fr3ml6sq0imvhi8a7qir0nb46oumtgn9.apps.googleusercontent.com"; + options.ClientSecret = "V2nDD9SkFbvLTqAUBWBBxYAL"; + }); + services.AddInstance(new GoogleAuthenticationOptions + { + ClientId = "514485782433-fr3ml6sq0imvhi8a7qir0nb46oumtgn9.apps.googleusercontent.com", + ClientSecret = "V2nDD9SkFbvLTqAUBWBBxYAL" + }); + services.SetupOptions(options => + { + options.AppId = "901611409868059"; + options.AppSecret = "4aa3c530297b1dcebc8860334b39668b"; + }); + + services.SetupOptions(options => + { + options.AppId = "901611409868059"; + options.AppSecret = "4aa3c530297b1dcebc8860334b39668b"; + }); + + services.SetupOptions(options => + { + options.ConsumerKey = "BSdJJ0CrDuvEhpkchnukXZBUv"; + options.ConsumerSecret = "xKUNuKhsRdHD03eLn67xhPAyE1wFFEndFo1X2UJaK2m1jdAxf4"; + }); // Add MVC services to the services container services.AddMvc(); @@ -62,19 +157,13 @@ namespace IdentitySamples // Add static files to the request pipeline app.UseStaticFiles(); + // Setup identity cookie middleware // Add cookie-based authentication to the request pipeline - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationType = ClaimsIdentityOptions.DefaultAuthenticationType, - LoginPath = new PathString("/Account/Login"), - Notifications = new CookieAuthenticationNotifications - { - OnValidateIdentity = SecurityStampValidator.OnValidateIdentity( - validateInterval: TimeSpan.FromMinutes(0)) - } - }); + app.UseIdentity(); - app.UseTwoFactorSignInCookies(); + app.UseGoogleAuthentication(); + app.UseFacebookAuthentication(); + app.UseTwitterAuthentication(); // Add MVC to the request pipeline app.UseMvc(routes => @@ -85,8 +174,15 @@ namespace IdentitySamples defaults: new { controller = "Home", action = "Index" }); }); - //Populates the MusicStore sample data + //Populates the Admin user and role SampleData.InitializeIdentityDatabaseAsync(app.ApplicationServices).Wait(); } + + // TODO: Move services here + public IServiceProvider ConfigureServices(ServiceCollection services) + { + return services.BuildServiceProvider(); + } + } } \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/Views/Account/ConfirmEmail.cshtml b/samples/IdentitySample.Mvc/Views/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000000..d8aab1c1d7 --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ConfirmEmail.cshtml @@ -0,0 +1,12 @@ +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Confirm Email"; +} + +

@ViewBag.Title.

+
+

+ Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) +

+
diff --git a/samples/IdentitySample.Mvc/Views/Account/DisplayEmail.cshtml b/samples/IdentitySample.Mvc/Views/Account/DisplayEmail.cshtml new file mode 100644 index 0000000000..1aa9a79f35 --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/DisplayEmail.cshtml @@ -0,0 +1,13 @@ +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "DEMO purpose Email Link"; +} +

@ViewBag.Title.

+

+ Please check your email and confirm your email address. +

+

+ For DEMO only: You can click this link to confirm the email: link + Please change this code to register an email service in IdentityConfig to send an email. +

diff --git a/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml b/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml new file mode 100644 index 0000000000..d3ae58b2a8 --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ExternalLoginConfirmation.cshtml @@ -0,0 +1,41 @@ +@model IdentitySample.Models.ExternalLoginConfirmationViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Register"; +} +

@ViewBag.Title.

+

Associate your @ViewBag.LoginProvider account.

+ +@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() + +

Association Form

+
+ @Html.ValidationSummary(true, "", new { @class = "text-danger" }) +

+ You've successfully authenticated with @ViewBag.LoginProvider. + Please enter a user name for this site below and click the Register button to finish + logging in. +

+
+ @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) + @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} diff --git a/samples/IdentitySample.Mvc/Views/Account/ExternalLoginFailure.cshtml b/samples/IdentitySample.Mvc/Views/Account/ExternalLoginFailure.cshtml new file mode 100644 index 0000000000..88071934ab --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ExternalLoginFailure.cshtml @@ -0,0 +1,10 @@ +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Login Failure"; +} + +
+

@ViewBag.Title.

+

Unsuccessful login with service.

+
diff --git a/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml b/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml new file mode 100644 index 0000000000..55970949b5 --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ForgotPassword.cshtml @@ -0,0 +1,34 @@ +@model IdentitySample.Models.ForgotPasswordViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Forgot your password?"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() +

Enter your email.

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) +
+ @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} diff --git a/samples/IdentitySample.Mvc/Views/Account/ForgotPasswordConfirmation.cshtml b/samples/IdentitySample.Mvc/Views/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000000..a4b98f1cff --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,19 @@ +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Forgot Password Confirmation"; +} + +
+

@ViewBag.Title.

+
+
+

+ Please check your email to reset your password. +

+

+ For DEMO only: You can click this link to reset password: link + Please change this code to register an email service in IdentityConfig to send an email. +

+
+ diff --git a/samples/IdentitySample.Mvc/Views/Account/Login.cshtml b/samples/IdentitySample.Mvc/Views/Account/Login.cshtml index 7dd9dc483e..ec3269be4b 100644 --- a/samples/IdentitySample.Mvc/Views/Account/Login.cshtml +++ b/samples/IdentitySample.Mvc/Views/Account/Login.cshtml @@ -1,4 +1,7 @@ @model IdentitySample.Models.LoginViewModel +@using System.Collections.Generic +@using Microsoft.AspNet.Http +@using Microsoft.AspNet.Http.Security @{ //TODO: Until we have a way to specify the layout page at application level. @@ -44,8 +47,39 @@

- @Html.ActionLink("Register", "Register") if you don't have a local account. + @Html.ActionLink("Register a new user?", "Register")

+

+ @Html.ActionLink("Forget your password?", "ForgotPassword") +

+ } + + +
+
+

Use another service to log in.

+
+ @{ + if (ViewBag.LoginProviders.Count == 0) { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else { + using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = ViewBag.ReturnUrl })) { + @Html.AntiForgeryToken() +
+

+ @foreach (AuthenticationDescription p in ViewBag.LoginProviders) { + + } +

+
+ } + } }
diff --git a/samples/IdentitySample.Mvc/Views/Account/Manage.cshtml b/samples/IdentitySample.Mvc/Views/Account/Manage.cshtml deleted file mode 100644 index fef49f927d..0000000000 --- a/samples/IdentitySample.Mvc/Views/Account/Manage.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@{ - //TODO: Until we have a way to specify the layout page at application level. - Layout = "/Views/Shared/_Layout.cshtml"; - ViewBag.Title = "Manage Account"; -} - -

@ViewBag.Title.

-

@ViewBag.StatusMessage

- -
-
- @await Html.PartialAsync("_ChangePasswordPartial") -
-
- -@section Scripts { - @*TODO : Until script helpers are available, adding script references manually*@ - @*@Scripts.Render("~/bundles/jqueryval")*@ - - -} \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/Views/Account/Register.cshtml b/samples/IdentitySample.Mvc/Views/Account/Register.cshtml index b282cb6a8c..a81dc30cd9 100644 --- a/samples/IdentitySample.Mvc/Views/Account/Register.cshtml +++ b/samples/IdentitySample.Mvc/Views/Account/Register.cshtml @@ -14,9 +14,9 @@
@Html.ValidationSummary()
- @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) + @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) + @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
diff --git a/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml b/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml new file mode 100644 index 0000000000..b5e0f2d9a0 --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ResetPassword.cshtml @@ -0,0 +1,47 @@ +@model IdentitySample.Models.ResetPasswordViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Reset password"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() +

Reset your password.

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) + @Html.HiddenFor(model => model.Code) +
+ @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) +
+
+
+ @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) +
+
+
+ @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} diff --git a/samples/IdentitySample.Mvc/Views/Account/ResetPasswordConfirmation.cshtml b/samples/IdentitySample.Mvc/Views/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000000..7f0a2ada6f --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,14 @@ +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Reset password confirmation"; +} + +
+

@ViewBag.Title.

+
+
+

+ Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) +

+
diff --git a/samples/IdentitySample.Mvc/Views/Account/SendCode.cshtml b/samples/IdentitySample.Mvc/Views/Account/SendCode.cshtml new file mode 100644 index 0000000000..cb2b351bed --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/SendCode.cshtml @@ -0,0 +1,27 @@ +@model IdentitySample.Models.SendCodeViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Send Verification Code"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("SendCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { + @Html.AntiForgeryToken() + @Html.Hidden("rememberMe", @Model.RememberMe) +
+
+ Two Factor Authentication Provider: + @Html.DropDownListFor(model => model.SelectedProvider, Model.Providers) + +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} diff --git a/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml b/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml new file mode 100644 index 0000000000..d4f8d9eb7a --- /dev/null +++ b/samples/IdentitySample.Mvc/Views/Account/VerifyCode.cshtml @@ -0,0 +1,43 @@ +@model IdentitySample.Models.VerifyCodeViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Enter Verification Code"; +} + +

@ViewBag.Title.

+ +@using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { + @Html.AntiForgeryToken() + @Html.ValidationSummary("", new { @class = "text-danger" }) + @Html.Hidden("provider", @Model.Provider) + @Html.Hidden("rememberMe", @Model.RememberMe) +

@ViewBag.Status

+
+
+ @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) +
+
+
+
+
+ @Html.CheckBoxFor(m => m.RememberBrowser) + @Html.LabelFor(m => m.RememberBrowser) +
+
+
+
+
+ +
+
+} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} diff --git a/samples/IdentitySample.Mvc/Views/Account/_ChangePasswordPartial.cshtml b/samples/IdentitySample.Mvc/Views/Account/_ChangePasswordPartial.cshtml deleted file mode 100644 index 0935c369ea..0000000000 --- a/samples/IdentitySample.Mvc/Views/Account/_ChangePasswordPartial.cshtml +++ /dev/null @@ -1,36 +0,0 @@ -@using System.Security.Principal -@model IdentitySample.Models.ManageUserViewModel - -

You're logged in as @User.Identity.GetUserName().

- -@using (Html.BeginForm("Manage", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) -{ - @Html.AntiForgeryToken() -

Change Password Form

-
- @Html.ValidationSummary() -
- @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" }) -
- @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" }) -
-
-
- @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) -
- @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) -
-
-
- @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) -
- @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) -
-
- -
-
- -
-
-} \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/Views/Manage/Index.cshtml b/samples/IdentitySample.Mvc/Views/Manage/Index.cshtml index d6d774d9d1..2f1b455817 100644 --- a/samples/IdentitySample.Mvc/Views/Manage/Index.cshtml +++ b/samples/IdentitySample.Mvc/Views/Manage/Index.cshtml @@ -10,14 +10,8 @@

- @if (Model.HasPassword) - { - @Html.ActionLink("Change your password", "ChangePassword") - } - else - { - @Html.ActionLink("Pick a password", "SetPassword") - } + @(Model.HasPassword ? Html.ActionLink("Change your password", "ChangePassword") + : Html.ActionLink("Set your password", "SetPassword"))

Phone Number: @(Model.PhoneNumber ?? "None") [ @@ -39,39 +33,47 @@

@if (Model.TwoFactor) { -
-

- Two factor is currently enabled: - -

-
+ using (Html.BeginForm("DisableTFA", "Manage", FormMethod.Post, new {@class = "form-horizontal", role = "form"})) + { + @Html.AntiForgeryToken() +

+ Two factor is currently enabled: + +

+ } } else { -
+ using (Html.BeginForm("EnableTFA", "Manage", FormMethod.Post, new {@class = "form-horizontal", role = "form"})) + { + @Html.AntiForgeryToken()

Two factor is currently disabled:

-
+ } } @if (Model.BrowserRemembered) { -
+ using (Html.BeginForm("ForgetBrowser", "Manage", FormMethod.Post, new {@class = "form-horizontal", role = "form"})) + { + @Html.AntiForgeryToken()

Browser is curently remembered for two factor:

-
+ } } else { -
+ using (Html.BeginForm("RememberBrowser", "Manage", FormMethod.Post, new {@class = "form-horizontal", role = "form"})) + { + @Html.AntiForgeryToken()

Browser is curently not remembered for two factor:

-
+ } }
diff --git a/samples/IdentitySample.Mvc/Views/Manage/ManageLogins.cshtml b/samples/IdentitySample.Mvc/Views/Manage/ManageLogins.cshtml index 06c49f6a7d..dd24e9c07a 100644 --- a/samples/IdentitySample.Mvc/Views/Manage/ManageLogins.cshtml +++ b/samples/IdentitySample.Mvc/Views/Manage/ManageLogins.cshtml @@ -1,5 +1,5 @@ @model IdentitySample.Models.ManageLoginsViewModel -@using Microsoft.Owin.Security +@using Microsoft.AspNet.Http.Security @{ //TODO: Until we have a way to specify the layout page at application level. Layout = "/Views/Shared/_Layout.cshtml"; diff --git a/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml b/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml index 91fd71b7bc..d863249884 100644 --- a/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml +++ b/samples/IdentitySample.Mvc/Views/Manage/SetPassword.cshtml @@ -1,4 +1,9 @@ @model IdentitySample.Models.SetPasswordViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Set Password"; +}

You do not have a local username/password for this site. Add a local @@ -9,7 +14,7 @@ { @Html.AntiForgeryToken() -

Create Local Login

+

Set your password


@Html.ValidationSummary("", new { @class = "text-danger" })
@@ -31,5 +36,8 @@
} @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + } \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/project.json b/samples/IdentitySample.Mvc/project.json index 8b0206cb57..5614632180 100644 --- a/samples/IdentitySample.Mvc/project.json +++ b/samples/IdentitySample.Mvc/project.json @@ -5,21 +5,28 @@ "description": "Identity sample MVC application on K", "version": "1.0.0-*", "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Server.IIS": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.Core": "6.0.0-*", + "Microsoft.AspNet.Mvc.ModelBinding": "6.0.0-*", + "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", + "Microsoft.AspNet.Routing": "1.0.0-*", "Microsoft.AspNet.Server.WebListener": "1.0.0-*", "Microsoft.AspNet.Diagnostics": "1.0.0-*", "Microsoft.AspNet.Identity.SqlServer": "3.0.0-*", - "Microsoft.AspNet.Identity.Authentication": "3.0.0-*", "Microsoft.AspNet.Security.Cookies": "1.0.0-*", + "Microsoft.AspNet.Security.Facebook": "1.0.0-*", + "Microsoft.AspNet.Security.Google": "1.0.0-*", + "Microsoft.AspNet.Security.Twitter": "1.0.0-*", "Microsoft.AspNet.StaticFiles": "1.0.0-*", "EntityFramework.SqlServer": "7.0.0-*", "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*", "Microsoft.Framework.OptionsModel": "1.0.0-*" }, "commands": { - "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5002", - "run": "run server.urls=http://localhost:5003" + "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:41532", + "run": "run server.urls=http://localhost:41532" }, "frameworks": { "aspnet50": { diff --git a/src/Microsoft.AspNet.Identity.Authentication/BuilderExtensions.cs b/src/Microsoft.AspNet.Identity.Authentication/BuilderExtensions.cs deleted file mode 100644 index 892513ed58..0000000000 --- a/src/Microsoft.AspNet.Identity.Authentication/BuilderExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Identity.Authentication; -using Microsoft.AspNet.Security.Cookies; - -namespace Microsoft.AspNet.Builder -{ - /// - /// Startup extensions - /// - public static class BuilderExtensions - { - public static IApplicationBuilder UseTwoFactorSignInCookies(this IApplicationBuilder builder) - { - // TODO: expose some way for them to customize these cookie lifetimes? - builder.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationType = HttpAuthenticationManager.TwoFactorRememberedAuthenticationType, - AuthenticationMode = Security.AuthenticationMode.Passive - }); - builder.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationType = HttpAuthenticationManager.TwoFactorUserIdAuthenticationType, - AuthenticationMode = Security.AuthenticationMode.Passive - }); - return builder; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.Authentication/HttpAuthenticationManager.cs b/src/Microsoft.AspNet.Identity.Authentication/HttpAuthenticationManager.cs deleted file mode 100644 index a0c742138c..0000000000 --- a/src/Microsoft.AspNet.Identity.Authentication/HttpAuthenticationManager.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Security; -using Microsoft.Framework.DependencyInjection; - -namespace Microsoft.AspNet.Identity.Authentication -{ - public class HttpAuthenticationManager : IAuthenticationManager - { - public static readonly string TwoFactorUserIdAuthenticationType = "Microsoft.AspNet.Identity.TwoFactor.UserId"; - public static readonly string TwoFactorRememberedAuthenticationType = "Microsoft.AspNet.Identity.TwoFactor.Remembered"; - - public HttpAuthenticationManager(IContextAccessor contextAccessor) - { - Context = contextAccessor.Value; - } - - public HttpContext Context { get; private set; } - - public void ForgetClient() - { - Context.Response.SignOut(TwoFactorRememberedAuthenticationType); - } - - public async Task IsClientRememeberedAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) - { - var result = - await Context.AuthenticateAsync(TwoFactorRememberedAuthenticationType); - return (result != null && result.Identity != null && result.Identity.Name == userId); - } - - public void RememberClient(string userId) - { - var rememberBrowserIdentity = new ClaimsIdentity(TwoFactorRememberedAuthenticationType); - rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); - Context.Response.SignIn(rememberBrowserIdentity); - } - - public async Task RetrieveUserId() - { - var result = await Context.AuthenticateAsync(TwoFactorUserIdAuthenticationType); - if (result != null && result.Identity != null) - { - return result.Identity.Name; - } - return null; - } - - public void SignIn(ClaimsIdentity identity, bool isPersistent) - { - Context.Response.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity); - } - public void SignOut(string authenticationType) - { - Context.Response.SignOut(authenticationType); - } - - public Task StoreUserId(string userId) - { - var userIdentity = new ClaimsIdentity(TwoFactorUserIdAuthenticationType); - userIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); - Context.Response.SignIn(userIdentity); - return Task.FromResult(0); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.Authentication/IdentityBuilderExtensions.cs b/src/Microsoft.AspNet.Identity.Authentication/IdentityBuilderExtensions.cs deleted file mode 100644 index 2233f32b90..0000000000 --- a/src/Microsoft.AspNet.Identity.Authentication/IdentityBuilderExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Authentication; - -namespace Microsoft.Framework.DependencyInjection -{ - public static class IdentityBuilderExtensions - { - public static IdentityBuilder AddAuthentication(this IdentityBuilder builder) - where TUser : class - where TRole : class - { - builder.Services.AddScoped(); - return builder; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.Authentication/Microsoft.AspNet.Identity.Authentication.kproj b/src/Microsoft.AspNet.Identity.Authentication/Microsoft.AspNet.Identity.Authentication.kproj deleted file mode 100644 index 4e4f49ce02..0000000000 --- a/src/Microsoft.AspNet.Identity.Authentication/Microsoft.AspNet.Identity.Authentication.kproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - 12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 7b4cff5a-1948-45ec-b170-6eb7c039b2f9 - Library - - - - - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.Authentication/SecurityStampValidator.cs b/src/Microsoft.AspNet.Identity.Authentication/SecurityStampValidator.cs deleted file mode 100644 index e52bfa6903..0000000000 --- a/src/Microsoft.AspNet.Identity.Authentication/SecurityStampValidator.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading.Tasks; -using Microsoft.AspNet.Security.Cookies; -using Microsoft.Framework.DependencyInjection; - -namespace Microsoft.AspNet.Identity.Authentication -{ - /// - /// Static helper class used to configure a CookieAuthenticationProvider to validate a cookie against a user's security - /// stamp - /// - public static class SecurityStampValidator - { - /// - /// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security - /// stamp after validateInterval - /// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new - /// ClaimsIdentity - /// - /// - /// - /// - /// - public static Func OnValidateIdentity( - TimeSpan validateInterval) - where TUser : class - { - return OnValidateIdentity(validateInterval, id => id.GetUserId()); - } - - /// - /// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security - /// stamp after validateInterval - /// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new - /// ClaimsIdentity - /// - /// - /// - /// - /// - /// - /// - public static Func OnValidateIdentity( - TimeSpan validateInterval, - Func getUserIdCallback) - where TUser : class - { - return async context => - { - var currentUtc = DateTimeOffset.UtcNow; - if (context.Options != null && context.Options.SystemClock != null) - { - currentUtc = context.Options.SystemClock.UtcNow; - } - var issuedUtc = context.Properties.IssuedUtc; - - // Only validate if enough time has elapsed - var validate = (issuedUtc == null); - if (issuedUtc != null) - { - var timeElapsed = currentUtc.Subtract(issuedUtc.Value); - validate = timeElapsed > validateInterval; - } - if (validate) - { - var manager = context.HttpContext.RequestServices.GetService>(); - var userId = getUserIdCallback(context.Identity); - var user = await manager.ValidateSecurityStampAsync(context.Identity, userId); - if (user != null) - { - bool isPersistent = false; - if (context.Properties != null) - { - isPersistent = context.Properties.IsPersistent; - } - await manager.SignInAsync(user, isPersistent); - } - else - { - context.RejectIdentity(); - manager.SignOut(); - } - } - }; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.Authentication/project.json b/src/Microsoft.AspNet.Identity.Authentication/project.json deleted file mode 100644 index 5d9fa40a86..0000000000 --- a/src/Microsoft.AspNet.Identity.Authentication/project.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "3.0.0-*", - "dependencies": { - "Microsoft.AspNet.Http" : "1.0.0-*", - "Microsoft.AspNet.Identity" : "", - "Microsoft.AspNet.Security.Cookies" : "1.0.0-*", - "Microsoft.Framework.DependencyInjection" : "1.0.0-*", - "System.Security.Claims" : "1.0.0-*" - }, - "frameworks": { - "aspnet50": {}, - "aspnetcore50": { - "dependencies": { - "System.Collections": "4.0.10.0", - "System.ComponentModel": "4.0.0.0", - "System.Diagnostics.Debug": "4.0.10.0", - "System.Diagnostics.Tools": "4.0.0.0", - "System.Globalization": "4.0.10.0", - "System.Linq": "4.0.0.0", - "System.Linq.Expressions": "4.0.0.0", - "System.Reflection": "4.0.10.0", - "System.Resources.ResourceManager": "4.0.0.0", - "System.Runtime": "4.0.20.0", - "System.Runtime.Extensions": "4.0.10.0", - "System.Security.Principal": "4.0.0.0", - "System.Text.Encoding": "4.0.10.0", - "System.Threading.Tasks": "4.0.10.0" - } - } - } -} diff --git a/src/Microsoft.AspNet.Identity.SqlServer/IdentityDbContext.cs b/src/Microsoft.AspNet.Identity.SqlServer/IdentityDbContext.cs index 3ec10bd4a5..b7cc4123c7 100644 --- a/src/Microsoft.AspNet.Identity.SqlServer/IdentityDbContext.cs +++ b/src/Microsoft.AspNet.Identity.SqlServer/IdentityDbContext.cs @@ -11,8 +11,6 @@ namespace Microsoft.AspNet.Identity.SqlServer IdentityDbContext { public IdentityDbContext() { } - public IdentityDbContext(IServiceProvider serviceProvider) : base(serviceProvider) { } - public IdentityDbContext(DbContextOptions options) : base(options) { } public IdentityDbContext(IServiceProvider serviceProvider, DbContextOptions options) : base(serviceProvider, options) { } } @@ -21,8 +19,6 @@ namespace Microsoft.AspNet.Identity.SqlServer where TUser : IdentityUser { public IdentityDbContext() { } - public IdentityDbContext(IServiceProvider serviceProvider) : base(serviceProvider) { } - public IdentityDbContext(DbContextOptions options) : base(options) { } public IdentityDbContext(IServiceProvider serviceProvider, DbContextOptions options) : base(serviceProvider, options) { } } @@ -39,8 +35,6 @@ namespace Microsoft.AspNet.Identity.SqlServer public DbSet> RoleClaims { get; set; } public IdentityDbContext() { } - public IdentityDbContext(IServiceProvider serviceProvider) : base(serviceProvider) { } - public IdentityDbContext(DbContextOptions options) : base(options) { } public IdentityDbContext(IServiceProvider serviceProvider, DbContextOptions options) : base(serviceProvider, options) { } protected override void OnModelCreating(ModelBuilder builder) @@ -48,26 +42,26 @@ namespace Microsoft.AspNet.Identity.SqlServer builder.Entity(b => { b.Key(u => u.Id); - b.Property(u => u.UserName); b.ToTable("AspNetUsers"); }); builder.Entity(b => { b.Key(r => r.Id); - b.Property(r => r.Name); b.ToTable("AspNetRoles"); }); builder.Entity>(b => { b.Key(uc => uc.Id); + b.ManyToOne().ForeignKey(uc => uc.UserId); b.ToTable("AspNetUserClaims"); }); builder.Entity>(b => { - b.Key(uc => uc.Id); + b.Key(rc => rc.Id); + b.ManyToOne().ForeignKey(rc => rc.RoleId); b.ToTable("AspNetRoleClaims"); }); @@ -76,8 +70,8 @@ namespace Microsoft.AspNet.Identity.SqlServer var userClaimType = builder.Model.GetEntityType(typeof(IdentityUserClaim)); var roleClaimType = builder.Model.GetEntityType(typeof(IdentityRoleClaim)); var userRoleType = builder.Model.GetEntityType(typeof(IdentityUserRole)); - var ucfk = userClaimType.GetOrAddForeignKey(userType.GetPrimaryKey(), new[] { userClaimType.GetProperty("UserId") }); - userType.AddNavigation(new Navigation(ucfk, "Claims", false)); + //var ucfk = userClaimType.GetOrAddForeignKey(userType.GetPrimaryKey(), new[] { userClaimType.GetProperty("UserId") }); + //userType.AddNavigation(new Navigation(ucfk, "Claims", false)); //userClaimType.AddNavigation(new Navigation(ucfk, "User", true)); //var urfk = userRoleType.GetOrAddForeignKey(userType.GetPrimaryKey(), new[] { userRoleType.GetProperty("UserId") }); //userType.AddNavigation(new Navigation(urfk, "Roles", false)); @@ -99,10 +93,10 @@ namespace Microsoft.AspNet.Identity.SqlServer builder.Entity>(b => { - b.Key(l => new { l.LoginProvider, l.ProviderKey, l.UserId }); + b.Key(l => new { l.LoginProvider, l.ProviderKey }); + b.ManyToOne().ForeignKey(uc => uc.UserId); b.ToTable("AspNetUserLogins"); }); - //.ForeignKeys(fk => fk.ForeignKey(f => f.UserId)) } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkBuilderExtensions.cs b/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkBuilderExtensions.cs new file mode 100644 index 0000000000..36d3c404d2 --- /dev/null +++ b/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkBuilderExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.SqlServer; +using Microsoft.Data.Entity; +using System; + +namespace Microsoft.Framework.DependencyInjection +{ + public static class IdentityEntityFrameworkBuilderExtensions + { + public static IdentityBuilder AddEntityFramework(this IdentityBuilder builder) + { + return builder.AddEntityFramework(); + } + + public static IdentityBuilder AddEntityFramework(this IdentityBuilder builder) + where TContext : DbContext + { + return builder.AddEntityFramework(); + } + + public static IdentityBuilder AddEntityFramework(this IdentityBuilder builder) + where TUser : IdentityUser, new() + where TContext : DbContext + { + return builder.AddEntityFramework(); + } + + public static IdentityBuilder AddEntityFramework(this IdentityBuilder builder) + where TUser : IdentityUser, new() + where TRole : IdentityRole, new() + where TContext : DbContext + { + builder.Services.AddScoped, UserStore>(); + builder.Services.AddScoped, RoleStore>(); + builder.Services.AddScoped(); + return builder; + } + + public static IdentityBuilder AddEntityFramework(this IdentityBuilder builder) + where TUser : IdentityUser, new() + where TRole : IdentityRole, new() + where TContext : DbContext + where TKey : IEquatable + { + builder.Services.AddScoped, UserStore>(); + builder.Services.AddScoped, RoleStore>(); + builder.Services.AddScoped(); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkServiceCollectionExtensions.cs index 1ecfe159c6..2cbd46319d 100644 --- a/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Identity.SqlServer/IdentityEntityFrameworkServiceCollectionExtensions.cs @@ -4,31 +4,54 @@ using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.SqlServer; using Microsoft.Data.Entity; +using Microsoft.Framework.ConfigurationModel; using System; namespace Microsoft.Framework.DependencyInjection { public static class IdentityEntityFrameworkServiceCollectionExtensions { - public static IdentityBuilder AddIdentitySqlServer(this ServiceCollection services) + public static IdentityBuilder AddIdentitySqlServer(this IServiceCollection services) { return services.AddIdentitySqlServer(); } - public static IdentityBuilder AddIdentitySqlServer(this ServiceCollection services) + public static IdentityBuilder AddIdentitySqlServer(this IServiceCollection services) where TContext : DbContext { return services.AddIdentitySqlServer(); } - public static IdentityBuilder AddIdentitySqlServer(this ServiceCollection services) + public static IdentityBuilder AddDefaultIdentity(this IServiceCollection services, IConfiguration config) + where TUser : IdentityUser, new() + where TRole : IdentityRole, new() + where TContext : DbContext + { + return services.AddDefaultIdentity(config) + .AddEntityFramework(); + } + + + public static IdentityBuilder AddIdentitySqlServer(this IServiceCollection services) where TUser : IdentityUser, new() where TContext : DbContext { return services.AddIdentitySqlServer(); } - public static IdentityBuilder AddIdentitySqlServer(this ServiceCollection services) + public static IdentityBuilder AddSqlServer(this IServiceCollection services) + where TUser : IdentityUser, new() + where TRole : IdentityRole, new() + where TContext : DbContext + { + var builder = services.AddIdentity(); + services.AddScoped, UserStore>(); + services.AddScoped, RoleStore>(); + services.AddScoped(); + return builder; + } + + public static IdentityBuilder AddIdentitySqlServer(this IServiceCollection services) where TUser : IdentityUser, new() where TRole : IdentityRole, new() where TContext : DbContext diff --git a/src/Microsoft.AspNet.Identity.SqlServer/RoleStore.cs b/src/Microsoft.AspNet.Identity.SqlServer/RoleStore.cs index bdda426f64..26db3d805c 100644 --- a/src/Microsoft.AspNet.Identity.SqlServer/RoleStore.cs +++ b/src/Microsoft.AspNet.Identity.SqlServer/RoleStore.cs @@ -12,43 +12,39 @@ using Microsoft.Data.Entity; namespace Microsoft.AspNet.Identity.SqlServer { - public class RoleStore : RoleStore where TRole : IdentityRole - { - public RoleStore(DbContext context) : base(context) { } - } + public class RoleStore(DbContext context) : RoleStore(context) + where TRole : IdentityRole + { } - public class RoleStore : RoleStore + public class RoleStore(TContext context) : RoleStore(context) where TRole : IdentityRole where TContext : DbContext - { - public RoleStore(TContext context) : base(context) { } - } + { } - public class RoleStore : - IQueryableRoleStore, + public class RoleStore(TContext context) : + IQueryableRoleStore, IRoleClaimStore where TRole : IdentityRole where TKey : IEquatable where TContext : DbContext { - private bool _disposed; - - public RoleStore(TContext context) + // Primary constructor { if (context == null) { throw new ArgumentNullException("context"); } - Context = context; - AutoSaveChanges = true; } - public TContext Context { get; private set; } + private bool _disposed; + + + public TContext Context { get; } = context; /// /// If true will call SaveChanges after CreateAsync/UpdateAsync/DeleteAsync /// - public bool AutoSaveChanges { get; set; } + public bool AutoSaveChanges { get; set; } = true; private async Task SaveChanges(CancellationToken cancellationToken) { diff --git a/src/Microsoft.AspNet.Identity.SqlServer/UserStore.cs b/src/Microsoft.AspNet.Identity.SqlServer/UserStore.cs index 4d10dbb126..40ecb269e8 100644 --- a/src/Microsoft.AspNet.Identity.SqlServer/UserStore.cs +++ b/src/Microsoft.AspNet.Identity.SqlServer/UserStore.cs @@ -13,25 +13,20 @@ using Microsoft.Data.Entity; namespace Microsoft.AspNet.Identity.SqlServer { - public class UserStore : UserStore - { - public UserStore(DbContext context) : base(context) { } - } + public class UserStore(DbContext context) : UserStore(context) + { } - public class UserStore : UserStore where TUser : IdentityUser, new() - { - public UserStore(DbContext context) : base(context) { } - } + public class UserStore(DbContext context) : UserStore(context) + where TUser : IdentityUser, new() + { } - public class UserStore : UserStore + public class UserStore(TContext context) : UserStore(context) where TUser : IdentityUser, new() where TRole : IdentityRole, new() where TContext : DbContext - { - public UserStore(TContext context) : base(context) { } - } + { } - public class UserStore : + public class UserStore(TContext context) : IUserLoginStore, IUserRoleStore, IUserClaimStore, @@ -47,24 +42,22 @@ namespace Microsoft.AspNet.Identity.SqlServer where TContext : DbContext where TKey : IEquatable { - private bool _disposed; - - public UserStore(TContext context) + // Primary constructor { if (context == null) { throw new ArgumentNullException("context"); } - Context = context; - AutoSaveChanges = true; } - public TContext Context { get; private set; } + private bool _disposed; + + public TContext Context { get; } = context; /// /// If true will call SaveChanges after CreateAsync/UpdateAsync/DeleteAsync /// - public bool AutoSaveChanges { get; set; } + public bool AutoSaveChanges { get; set; } = true; private Task SaveChanges(CancellationToken cancellationToken) { @@ -507,7 +500,7 @@ namespace Microsoft.AspNet.Identity.SqlServer return Task.FromResult(0); } - public virtual Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual async Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -516,9 +509,11 @@ namespace Microsoft.AspNet.Identity.SqlServer throw new ArgumentNullException("user"); } // todo: ensure logins loaded - IList result = user.Logins - .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToList(); - return Task.FromResult(result); + //IList result = user.Logins + // .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToList(); + var userId = user.Id; + return await UserLogins.Where(l => l.UserId.Equals(userId)) + .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(); } public async virtual Task FindByLoginAsync(string loginProvider, string providerKey, diff --git a/src/Microsoft.AspNet.Identity/BuilderExtensions.cs b/src/Microsoft.AspNet.Identity/BuilderExtensions.cs new file mode 100644 index 0000000000..d8caba9028 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/BuilderExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Identity; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Startup extensions + /// + public static class BuilderExtensions + { + public static IApplicationBuilder UseIdentity(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException("app"); + } + var options = app.ApplicationServices.GetService>().Options; + app.UseCookieAuthentication(options.ApplicationCookie); + app.SetDefaultSignInAsAuthenticationType(options.DefaultSignInAsAuthenticationType); + app.UseCookieAuthentication(options.ExternalCookie); + app.UseCookieAuthentication(options.TwoFactorRememberMeCookie); + app.UseCookieAuthentication(options.TwoFactorUserIdCookie); + return app; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs b/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs index 3ef007b107..165faf5eac 100644 --- a/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs +++ b/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs @@ -7,41 +7,32 @@ namespace Microsoft.AspNet.Identity { public class ClaimsIdentityOptions { - /// - /// ClaimType used for the security stamp by default - /// public static readonly string DefaultSecurityStampClaimType = "AspNet.Identity.SecurityStamp"; public static readonly string DefaultAuthenticationType = typeof(ClaimsIdentityOptions).Namespace + ".Application"; + public static readonly string DefaultExternalLoginAuthenticationType = typeof(ClaimsIdentityOptions).Namespace + ".ExternalLogin"; + public static readonly string DefaultTwoFactorRememberMeAuthenticationType = typeof(ClaimsIdentityOptions).Namespace + ".TwoFactorRememberMe"; + public static readonly string DefaultTwoFactorUserIdAuthenticationType = typeof(ClaimsIdentityOptions).Namespace + ".TwoFactorUserId"; - public ClaimsIdentityOptions() - { - AuthenticationType = DefaultAuthenticationType; - RoleClaimType = ClaimTypes.Role; - SecurityStampClaimType = DefaultSecurityStampClaimType; - UserIdClaimType = ClaimTypes.NameIdentifier; - UserNameClaimType = ClaimTypes.Name; - } - - public string AuthenticationType { get; set; } + public string AuthenticationType { get; set; } = DefaultAuthenticationType; /// /// Claim type used for role claims /// - public string RoleClaimType { get; set; } + public string RoleClaimType { get; set; } = ClaimTypes.Role; /// /// Claim type used for the user name /// - public string UserNameClaimType { get; set; } + public string UserNameClaimType { get; set; } = ClaimTypes.Name; /// /// Claim type used for the user id /// - public string UserIdClaimType { get; set; } + public string UserIdClaimType { get; set; } = ClaimTypes.NameIdentifier; /// /// Claim type used for the user security stamp /// - public string SecurityStampClaimType { get; set; } + public string SecurityStampClaimType { get; set; } = DefaultSecurityStampClaimType; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs b/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs new file mode 100644 index 0000000000..d29f234bff --- /dev/null +++ b/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs @@ -0,0 +1,177 @@ +using Microsoft.AspNet.Security.DataProtection; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + public class DataProtectionTokenProviderOptions + { + public string Name { get; set; } = "DataProtection"; + public TimeSpan TokenLifespan { get; set; } = TimeSpan.FromDays(1); + + } + + /// + /// Token provider that uses an IDataProtector to generate encrypted tokens based off of the security stamp + /// + public class DataProtectorTokenProvider(DataProtectionTokenProviderOptions options, IDataProtector protector) : IUserTokenProvider where TUser : class + { + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + if (protector == null) + { + throw new ArgumentNullException(nameof(protector)); + } + } + + public DataProtectionTokenProviderOptions Options { get; } = options; + public IDataProtector Protector { get; } = protector; + + public string Name { get { return Options.Name; } } + + /// + /// Generate a protected string for a user + /// + /// + /// + /// + /// + public async Task GenerateAsync(string purpose, UserManager manager, TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + var ms = new MemoryStream(); + var userId = await manager.GetUserIdAsync(user, cancellationToken); + using (var writer = ms.CreateWriter()) + { + writer.Write(DateTimeOffset.UtcNow); + writer.Write(userId); + writer.Write(purpose ?? ""); + string stamp = null; + if (manager.SupportsUserSecurityStamp) + { + stamp = await manager.GetSecurityStampAsync(user); + } + writer.Write(stamp ?? ""); + } + var protectedBytes = Protector.Protect(ms.ToArray()); + return Convert.ToBase64String(protectedBytes); + } + + /// + /// Return false if the token is not valid + /// + /// + /// + /// + /// + /// + public async Task ValidateAsync(string purpose, string token, UserManager manager, TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + var unprotectedData = Protector.Unprotect(Convert.FromBase64String(token)); + var ms = new MemoryStream(unprotectedData); + using (var reader = ms.CreateReader()) + { + var creationTime = reader.ReadDateTimeOffset(); + var expirationTime = creationTime + Options.TokenLifespan; + if (expirationTime < DateTimeOffset.UtcNow) + { + return false; + } + + var userId = reader.ReadString(); + var actualUserId = await manager.GetUserIdAsync(user, cancellationToken); + if (userId != actualUserId) + { + return false; + } + var purp = reader.ReadString(); + if (!string.Equals(purp, purpose)) + { + return false; + } + var stamp = reader.ReadString(); + if (reader.PeekChar() != -1) + { + return false; + } + + if (manager.SupportsUserSecurityStamp) + { + return stamp == await manager.GetSecurityStampAsync(user); + } + return stamp == ""; + } + } + // ReSharper disable once EmptyGeneralCatchClause + catch + { + // Do not leak exception + } + return false; + } + + /// + /// Returns false because tokens are two long to be used for two factor + /// + /// + /// + /// + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + /// + /// This provider no-ops by default when asked to notify a user + /// + /// + /// + /// + /// + public Task NotifyAsync(string token, UserManager manager, TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + } + + // Based on Levi's authentication sample + internal static class StreamExtensions + { + internal static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); + + public static BinaryReader CreateReader(this Stream stream) + { + return new BinaryReader(stream, DefaultEncoding, true); + } + + public static BinaryWriter CreateWriter(this Stream stream) + { + return new BinaryWriter(stream, DefaultEncoding, true); + } + + public static DateTimeOffset ReadDateTimeOffset(this BinaryReader reader) + { + return new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); + } + + public static void Write(this BinaryWriter writer, DateTimeOffset value) + { + writer.Write(value.UtcTicks); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs b/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs index eb5219894e..800029b75b 100644 --- a/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/EmailTokenProvider.cs @@ -5,33 +5,30 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Identity { - /// - /// TokenProvider that generates tokens from the user's security stamp and notifies a user via their email - /// - /// - public class EmailTokenProvider : TotpSecurityStampBasedTokenProvider - where TUser : class + public class EmailTokenProviderOptions { - private string _body; - private string _subject; + public string Name { get; set; } = Resources.DefaultEmailTokenProviderName; - /// - /// Email subject used when a token notification is received - /// - public string Subject - { - get { return _subject ?? string.Empty; } - set { _subject = value; } - } + public string Subject { get; set; } = "Security Code"; /// /// Format string which will be used for the email body, it will be passed the token for the first parameter /// - public string BodyFormat - { - get { return _body ?? "{0}"; } - set { _body = value; } - } + public string BodyFormat { get; set; } = "Your security code is: {0}"; + } + + /// + /// TokenProvider that generates tokens from the user's security stamp and notifies a user via their email + /// + /// + public class EmailTokenProvider(EmailTokenProviderOptions options) : TotpSecurityStampBasedTokenProvider + where TUser : class + { + public EmailTokenProvider() : this(new EmailTokenProviderOptions()) { } + + public EmailTokenProviderOptions Options { get; } = options; + + public override string Name { get { return Options.Name; } } /// /// True if the user has an email set @@ -39,7 +36,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public override async Task IsValidProviderForUserAsync(UserManager manager, TUser user, + public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { var email = await manager.GetEmailAsync(user, cancellationToken); @@ -74,7 +71,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("manager"); } - return manager.SendEmailAsync(user, Subject, String.Format(CultureInfo.CurrentCulture, BodyFormat, token), cancellationToken); + return manager.SendEmailAsync(user, Options.Subject, String.Format(CultureInfo.CurrentCulture, Options.BodyFormat, token), cancellationToken); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/ExternalLoginInfo.cs b/src/Microsoft.AspNet.Identity/ExternalLoginInfo.cs new file mode 100644 index 0000000000..f324a75a90 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/ExternalLoginInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; + +namespace Microsoft.AspNet.Identity +{ + public class ExternalLoginInfo(ClaimsIdentity externalIdentity, string loginProvider, string providerKey, + string displayName) : UserLoginInfo(loginProvider, providerKey, displayName) + { + public ClaimsIdentity ExternalIdentity { get; set; } = externalIdentity; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/HttpContextExtensions.cs b/src/Microsoft.AspNet.Identity/HttpContextExtensions.cs new file mode 100644 index 0000000000..b05b5fa1cb --- /dev/null +++ b/src/Microsoft.AspNet.Identity/HttpContextExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Http.Security; +using System.Linq; +using System; +using System.Security.Principal; + +namespace Microsoft.AspNet.Http +{ + public static class HttpContextExtensions + { + private const string LoginProviderKey = "LoginProvider"; + private const string XsrfKey = "XsrfId"; + + public static IEnumerable GetExternalAuthenticationTypes(this HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + return context.GetAuthenticationTypes().Where(d => !string.IsNullOrEmpty(d.Caption)); + } + + public static async Task GetExternalLoginInfo(this HttpContext context, string expectedXsrf = null) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + // REVIEW: should we consider taking the external authentication type as an argument? + var auth = await context.AuthenticateAsync(ClaimsIdentityOptions.DefaultExternalLoginAuthenticationType); + if (auth == null || auth.Identity == null || auth.Properties.Dictionary == null || !auth.Properties.Dictionary.ContainsKey(LoginProviderKey)) + { + return null; + } + + if (expectedXsrf != null) + { + if (!auth.Properties.Dictionary.ContainsKey(XsrfKey)) + { + return null; + } + var userId = auth.Properties.Dictionary[XsrfKey] as string; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Identity.FindFirstValue(ClaimTypes.NameIdentifier); + var provider = auth.Properties.Dictionary[LoginProviderKey] as string; + if (providerKey == null || provider == null) + { + return null; + } + return new ExternalLoginInfo(auth.Identity, provider, providerKey, auth.Description.Caption); + } + + public static AuthenticationProperties ConfigureExternalAuthenticationProperties(this HttpContext context, string provider, string redirectUrl, string userId = null) + { + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Dictionary[LoginProviderKey] = provider; + if (userId != null) + { + properties.Dictionary[XsrfKey] = userId; + } + return properties; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IAuthenticationManager.cs b/src/Microsoft.AspNet.Identity/IAuthenticationManager.cs deleted file mode 100644 index c060d2b142..0000000000 --- a/src/Microsoft.AspNet.Identity/IAuthenticationManager.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Identity -{ - public interface IAuthenticationManager - { - void SignIn(ClaimsIdentity identity, bool isPersistent); - void SignOut(string authenticationType); - - // remember browser for two factor - void ForgetClient(); - void RememberClient(string userId); - Task IsClientRememeberedAsync(string userId, - CancellationToken cancellationToken = default(CancellationToken)); - - // half cookie - Task StoreUserId(string userId); - Task RetrieveUserId(); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IPasswordValidator.cs b/src/Microsoft.AspNet.Identity/IPasswordValidator.cs index 50669f4c18..4a3f652584 100644 --- a/src/Microsoft.AspNet.Identity/IPasswordValidator.cs +++ b/src/Microsoft.AspNet.Identity/IPasswordValidator.cs @@ -12,10 +12,10 @@ namespace Microsoft.AspNet.Identity public interface IPasswordValidator where TUser : class { /// - /// Validate the item + /// Validate the password for the user /// /// - Task ValidateAsync(string password, UserManager manager, + Task ValidateAsync(TUser user, string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/ISecurityStampValidator.cs b/src/Microsoft.AspNet.Identity/ISecurityStampValidator.cs new file mode 100644 index 0000000000..cd40753cb9 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/ISecurityStampValidator.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Security.Cookies; + +namespace Microsoft.AspNet.Identity +{ + public interface ISecurityStampValidator + { + Task Validate(CookieValidateIdentityContext context, ClaimsIdentity identity); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs b/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs index 5494c0062a..7b03c5c584 100644 --- a/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; @@ -12,6 +11,11 @@ namespace Microsoft.AspNet.Identity /// public interface IUserTokenProvider where TUser : class { + /// + /// Name of the token provider + /// + string Name { get; } + /// /// Generate a token for a user /// @@ -47,13 +51,13 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)); /// - /// Returns true if provider can be used for this user, i.e. could require a user to have an email + /// Returns true if provider can be used for this user to generate two factor tokens, i.e. could require a user to have an email /// /// /// /// /// - Task IsValidProviderForUserAsync(UserManager manager, TUser user, + Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityBuilder.cs b/src/Microsoft.AspNet.Identity/IdentityBuilder.cs index 65d9ffd71c..0719c05dd5 100644 --- a/src/Microsoft.AspNet.Identity/IdentityBuilder.cs +++ b/src/Microsoft.AspNet.Identity/IdentityBuilder.cs @@ -9,39 +9,44 @@ namespace Microsoft.AspNet.Identity { public class IdentityBuilder where TUser : class where TRole : class { - public ServiceCollection Services { get; private set; } + public IServiceCollection Services { get; private set; } - public IdentityBuilder(ServiceCollection services) + public IdentityBuilder(IServiceCollection services) { Services = services; } // Rename to Add - public IdentityBuilder AddInstance(Func func) + public IdentityBuilder AddInstance(T obj) { - Services.AddInstance(func()); + Services.AddInstance(obj); return this; } - public IdentityBuilder AddUserStore(Func> func) + public IdentityBuilder AddUserStore(IUserStore store) { - return AddInstance(func); + return AddInstance(store); } - public IdentityBuilder AddRoleStore(Func> func) + public IdentityBuilder AddRoleStore(IRoleStore store) { - return AddInstance(func); + return AddInstance(store); } - public IdentityBuilder AddPasswordValidator(Func> func) + public IdentityBuilder AddPasswordValidator(IPasswordValidator validator) { - return AddInstance(func); + return AddInstance(validator); } - public IdentityBuilder AddUserValidator(Func> func) + public IdentityBuilder AddUserValidator(IUserValidator validator) { - return AddInstance(func); + return AddInstance(validator); + } + + public IdentityBuilder AddTokenProvider(IUserTokenProvider tokenProvider) + { + return AddInstance(tokenProvider); } public IdentityBuilder SetupOptions(Action action, int order) @@ -66,11 +71,5 @@ namespace Microsoft.AspNet.Identity Services.AddScoped(); return this; } - - //public IdentityBuilder UseTwoFactorProviders(Func>> func) - //{ - // return Use(func); - //} } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityOptions.cs b/src/Microsoft.AspNet.Identity/IdentityOptions.cs index 620ee13aa2..86517657c6 100644 --- a/src/Microsoft.AspNet.Identity/IdentityOptions.cs +++ b/src/Microsoft.AspNet.Identity/IdentityOptions.cs @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security; +using Microsoft.AspNet.Security.Cookies; +using System; + namespace Microsoft.AspNet.Identity { /// @@ -8,24 +13,58 @@ namespace Microsoft.AspNet.Identity /// public class IdentityOptions { - public IdentityOptions() + public ClaimsIdentityOptions ClaimsIdentity { get; set; } = new ClaimsIdentityOptions(); + + public UserOptions User { get; set; } = new UserOptions(); + + public PasswordOptions Password { get; set; } = new PasswordOptions(); + + public LockoutOptions Lockout { get; set; } = new LockoutOptions(); + + public SignInOptions SignIn { get; set; } = new SignInOptions(); + + public TimeSpan SecurityStampValidationInterval { get; set; } = TimeSpan.FromMinutes(30); + + public string EmailConfirmationTokenProvider { get; set; } = Resources.DefaultTokenProvider; + + public string PasswordResetTokenProvider { get; set; } = Resources.DefaultTokenProvider; + + //public string ApplicationCookieAuthenticationType { get; set; } + //public string ExternalCookieAuthenticationType { get; set; } + //public string TwoFactorCookieAuthenticationType { get; set; } + //public string TwoFactorFactorCookieAuthenticationType { get; set; } + + public CookieAuthenticationOptions ApplicationCookie { get; set; } = new CookieAuthenticationOptions { - ClaimsIdentity = new ClaimsIdentityOptions(); - User = new UserOptions(); - Password = new PasswordOptions(); - Lockout = new LockoutOptions(); - SignIn = new SignInOptions(); - } + AuthenticationType = ClaimsIdentityOptions.DefaultAuthenticationType, + LoginPath = new PathString("/Account/Login"), + Notifications = new CookieAuthenticationNotifications + { + OnValidateIdentity = SecurityStampValidator.ValidateIdentityAsync + } + }; - public ClaimsIdentityOptions ClaimsIdentity { get; set; } + // Move to setups for named per cookie option - public UserOptions User { get; set; } + public string DefaultSignInAsAuthenticationType { get; set; } = ClaimsIdentityOptions.DefaultExternalLoginAuthenticationType; - public PasswordOptions Password { get; set; } + public CookieAuthenticationOptions ExternalCookie { get; set; } = new CookieAuthenticationOptions + { + AuthenticationType = ClaimsIdentityOptions.DefaultExternalLoginAuthenticationType, + AuthenticationMode = AuthenticationMode.Passive + }; - public LockoutOptions Lockout { get; set; } + public CookieAuthenticationOptions TwoFactorRememberMeCookie { get; set; } = new CookieAuthenticationOptions + { + AuthenticationType = ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType, + AuthenticationMode = AuthenticationMode.Passive + }; - public SignInOptions SignIn { get; set; } + public CookieAuthenticationOptions TwoFactorUserIdCookie { get; set; } = new CookieAuthenticationOptions + { + AuthenticationType = ClaimsIdentityOptions.DefaultTwoFactorUserIdAuthenticationType, + AuthenticationMode = AuthenticationMode.Passive + }; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityRole.cs b/src/Microsoft.AspNet.Identity/IdentityRole.cs index 0a2206a989..776ba46ac8 100644 --- a/src/Microsoft.AspNet.Identity/IdentityRole.cs +++ b/src/Microsoft.AspNet.Identity/IdentityRole.cs @@ -33,23 +33,13 @@ namespace Microsoft.AspNet.Identity /// Represents a Role entity /// /// - public class IdentityRole where TKey : IEquatable + public class IdentityRole() where TKey : IEquatable { - /// - /// Constructor - /// - public IdentityRole() - { - Users = new List>(); - Claims = new List>(); - } - /// /// Constructor /// /// - public IdentityRole(string roleName) - : this() + public IdentityRole(string roleName) : this() { Name = roleName; } @@ -57,12 +47,12 @@ namespace Microsoft.AspNet.Identity /// /// Navigation property for users in the role /// - public virtual ICollection> Users { get; private set; } + public virtual ICollection> Users { get; private set; } = new List>(); /// /// Navigation property for claims in the role /// - public virtual ICollection> Claims { get; private set; } + public virtual ICollection> Claims { get; private set; } = new List>(); /// /// Role id @@ -74,4 +64,4 @@ namespace Microsoft.AspNet.Identity /// public virtual string Name { get; set; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs index b09e4e6e1e..41784b2955 100644 --- a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs @@ -4,52 +4,64 @@ using System; using Microsoft.AspNet.Identity; using Microsoft.Framework.ConfigurationModel; +using Microsoft.AspNet.Security.DataProtection; namespace Microsoft.Framework.DependencyInjection { public static class IdentityServiceCollectionExtensions { - public static IdentityBuilder AddIdentity(this ServiceCollection services, + public static IdentityBuilder AddIdentity(this IServiceCollection services, IConfiguration identityConfig) { services.SetupOptions(identityConfig); return services.AddIdentity(); } - public static IdentityBuilder AddIdentity(this ServiceCollection services) + public static IdentityBuilder AddIdentity(this IServiceCollection services) { return services.AddIdentity(); } - public static IdentityBuilder AddIdentity(this ServiceCollection services, - IConfiguration identityConfig) + public static IdentityBuilder AddIdentity(this IServiceCollection services, + IConfiguration identityConfig = null) where TUser : class where TRole : class { - services.SetupOptions(identityConfig); - return services.AddIdentity(); - } - - public static IdentityBuilder AddIdentity(this ServiceCollection services) - where TUser : class - where TRole : class - { - services.Add(IdentityServices.GetDefaultUserServices()); - services.Add(IdentityServices.GetDefaultRoleServices()); + if (identityConfig != null) + { + services.SetupOptions(identityConfig); + } + services.Add(IdentityServices.GetDefaultServices(identityConfig)); services.AddScoped>(); services.AddScoped>(); + services.AddScoped>(); services.AddScoped>(); services.AddScoped, ClaimsIdentityFactory>(); return new IdentityBuilder(services); } - public static IdentityBuilder AddIdentity(this ServiceCollection services) + public static IdentityBuilder AddDefaultIdentity(this IServiceCollection services, IConfiguration config = null) + where TUser : class + where TRole : class + { + return services.AddIdentity(config) + .AddTokenProvider(new DataProtectorTokenProvider( + new DataProtectionTokenProviderOptions + { + Name = Resources.DefaultTokenProvider, + }, + DataProtectionProvider.CreateFromDpapi().CreateProtector("ASP.NET Identity"))) + .AddTokenProvider(new PhoneNumberTokenProvider()) + .AddTokenProvider(new EmailTokenProvider()); + } + + public static IdentityBuilder AddIdentity(this IServiceCollection services) where TUser : class { return services.AddIdentity(); } - public static IdentityBuilder AddIdentity(this ServiceCollection services, + public static IdentityBuilder AddIdentity(this IServiceCollection services, IConfiguration identityConfig) where TUser : class { diff --git a/src/Microsoft.AspNet.Identity/IdentityServices.cs b/src/Microsoft.AspNet.Identity/IdentityServices.cs index aa4ded7eda..d632a9aba2 100644 --- a/src/Microsoft.AspNet.Identity/IdentityServices.cs +++ b/src/Microsoft.AspNet.Identity/IdentityServices.cs @@ -13,34 +13,23 @@ namespace Microsoft.AspNet.Identity /// public class IdentityServices { - public static IEnumerable GetDefaultUserServices() where TUser : class + public static IEnumerable GetDefaultServices(IConfiguration config = null) + where TUser : class where TRole : class { - return GetDefaultUserServices(new Configuration()); - } - - public static IEnumerable GetDefaultUserServices(IConfiguration configuration) - where TUser : class - { - var describe = new ServiceDescriber(configuration); - + ServiceDescriber describe; + if (config == null) + { + describe = new ServiceDescriber(); + } + else + { + describe = new ServiceDescriber(config); + } yield return describe.Transient, UserValidator>(); yield return describe.Transient, PasswordValidator>(); yield return describe.Transient, PasswordHasher>(); yield return describe.Transient(); - - // TODO: rationalize email/sms/usertoken services - } - - public static IEnumerable GetDefaultRoleServices() where TRole : class - { - return GetDefaultRoleServices(new Configuration()); - } - - public static IEnumerable GetDefaultRoleServices(IConfiguration configuration) - where TRole : class - { - var describe = new ServiceDescriber(configuration); - yield return describe.Instance>(new RoleValidator()); + yield return describe.Transient, RoleValidator>(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityUser.cs b/src/Microsoft.AspNet.Identity/IdentityUser.cs index b841e05020..effa19ccb4 100644 --- a/src/Microsoft.AspNet.Identity/IdentityUser.cs +++ b/src/Microsoft.AspNet.Identity/IdentityUser.cs @@ -19,16 +19,8 @@ namespace Microsoft.AspNet.Identity } } - public class IdentityUser - where TKey : IEquatable + public class IdentityUser() where TKey : IEquatable { - public IdentityUser() - { - Claims = new List>(); - Roles = new List>(); - Logins = new List>(); - } - public IdentityUser(string userName) : this() { UserName = userName; @@ -91,16 +83,16 @@ namespace Microsoft.AspNet.Identity /// /// Roles for the user /// - public virtual ICollection> Roles { get; private set; } + public virtual ICollection> Roles { get; } = new List>(); /// /// Claims for the user /// - public virtual ICollection> Claims { get; private set; } + public virtual ICollection> Claims { get; } = new List>(); /// /// Associated logins for the user /// - public virtual ICollection> Logins { get; private set; } + public virtual ICollection> Logins { get; } = new List>(); } } diff --git a/src/Microsoft.AspNet.Identity/LockoutOptions.cs b/src/Microsoft.AspNet.Identity/LockoutOptions.cs index 82ce883a11..1edc0cd8bb 100644 --- a/src/Microsoft.AspNet.Identity/LockoutOptions.cs +++ b/src/Microsoft.AspNet.Identity/LockoutOptions.cs @@ -7,26 +7,19 @@ namespace Microsoft.AspNet.Identity { public class LockoutOptions { - public LockoutOptions() - { - DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); - EnabledByDefault = false; - MaxFailedAccessAttempts = 5; - } - /// /// If true, will enable user lockout when users are created /// - public bool EnabledByDefault { get; set; } + public bool EnabledByDefault { get; set; } = false; /// /// Number of access attempts allowed for a user before lockout (if enabled) /// - public int MaxFailedAccessAttempts { get; set; } + public int MaxFailedAccessAttempts { get; set; } = 5; /// /// Default amount of time an user is locked out for after MaxFailedAccessAttempsBeforeLockout is reached /// - public TimeSpan DefaultLockoutTimeSpan { get; set; } + public TimeSpan DefaultLockoutTimeSpan { get; set; } = TimeSpan.FromMinutes(5); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PasswordOptions.cs b/src/Microsoft.AspNet.Identity/PasswordOptions.cs index 3498d08831..dd5a170c35 100644 --- a/src/Microsoft.AspNet.Identity/PasswordOptions.cs +++ b/src/Microsoft.AspNet.Identity/PasswordOptions.cs @@ -5,38 +5,29 @@ namespace Microsoft.AspNet.Identity { public class PasswordOptions { - public PasswordOptions() - { - RequireDigit = true; - RequireLowercase = true; - RequireNonLetterOrDigit = true; - RequireUppercase = true; - RequiredLength = 6; - } - /// /// Minimum required length /// - public int RequiredLength { get; set; } + public int RequiredLength { get; set; } = 6; /// /// Require a non letter or digit character /// - public bool RequireNonLetterOrDigit { get; set; } + public bool RequireNonLetterOrDigit { get; set; } = true; /// /// Require a lower case letter ('a' - 'z') /// - public bool RequireLowercase { get; set; } + public bool RequireLowercase { get; set; } = true; /// /// Require an upper case letter ('A' - 'Z') /// - public bool RequireUppercase { get; set; } + public bool RequireUppercase { get; set; } = true; /// /// Require a digit ('0' - '9') /// - public bool RequireDigit { get; set; } + public bool RequireDigit { get; set; } = true; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PasswordValidator.cs b/src/Microsoft.AspNet.Identity/PasswordValidator.cs index 497da1ed08..813048bf5e 100644 --- a/src/Microsoft.AspNet.Identity/PasswordValidator.cs +++ b/src/Microsoft.AspNet.Identity/PasswordValidator.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual Task ValidateAsync(string password, UserManager manager, + public virtual Task ValidateAsync(TUser user, string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)) { if (password == null) diff --git a/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs b/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs index 100c51586d..a1a1c74ac7 100644 --- a/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/PhoneNumberTokenProvider.cs @@ -5,23 +5,28 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Identity { - /// - /// TokenProvider that generates tokens from the user's security stamp and notifies a user via their phone number - /// - /// - public class PhoneNumberTokenProvider : TotpSecurityStampBasedTokenProvider - where TUser : class + public class PhoneNumberTokenProviderOptions { - private string _body; + public string Name { get; set; } = Resources.DefaultPhoneNumberTokenProviderName; /// /// Message contents which should contain a format string which the token will be the only argument /// - public string MessageFormat - { - get { return _body ?? "{0}"; } - set { _body = value; } - } + public string MessageFormat { get; set; } = "Your security code is: {0}"; + } + + /// + /// TokenProvider that generates tokens from the user's security stamp and notifies a user via their phone number + /// + /// + public class PhoneNumberTokenProvider(PhoneNumberTokenProviderOptions options) : TotpSecurityStampBasedTokenProvider + where TUser : class + { + public PhoneNumberTokenProvider() : this(new PhoneNumberTokenProviderOptions()) { } + + public PhoneNumberTokenProviderOptions Options { get; } = options; + + public override string Name { get { return Options.Name; } } /// /// Returns true if the user has a phone number set @@ -29,7 +34,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public override async Task IsValidProviderForUserAsync(UserManager manager, TUser user, + public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { if (manager == null) @@ -72,7 +77,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("manager"); } - return manager.SendSmsAsync(user, String.Format(CultureInfo.CurrentCulture, MessageFormat, token), cancellationToken); + return manager.SendSmsAsync(user, String.Format(CultureInfo.CurrentCulture, Options.MessageFormat, token), cancellationToken); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Identity/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9b09bcab00 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNet.Identity.Test")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs index 785e06b674..4bf5983229 100644 --- a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs @@ -10,6 +10,54 @@ namespace Microsoft.AspNet.Identity private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Identity.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// Your security code is: {0} + /// + internal static string DefaultEmailTokenProviderBodyFormat + { + get { return GetString("DefaultEmailTokenProviderBodyFormat"); } + } + + /// + /// Your security code is: {0} + /// + internal static string FormatDefaultEmailTokenProviderBodyFormat(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DefaultEmailTokenProviderBodyFormat"), p0); + } + + /// + /// Email + /// + internal static string DefaultEmailTokenProviderName + { + get { return GetString("DefaultEmailTokenProviderName"); } + } + + /// + /// Email + /// + internal static string FormatDefaultEmailTokenProviderName() + { + return GetString("DefaultEmailTokenProviderName"); + } + + /// + /// Security Code + /// + internal static string DefaultEmailTokenProviderSubject + { + get { return GetString("DefaultEmailTokenProviderSubject"); } + } + + /// + /// Security Code + /// + internal static string FormatDefaultEmailTokenProviderSubject() + { + return GetString("DefaultEmailTokenProviderSubject"); + } + /// /// An unknown failure has occured. /// @@ -26,6 +74,54 @@ namespace Microsoft.AspNet.Identity return GetString("DefaultError"); } + /// + /// Your security code is: {0} + /// + internal static string DefaultPhoneNumberTokenProviderMessageFormat + { + get { return GetString("DefaultPhoneNumberTokenProviderMessageFormat"); } + } + + /// + /// Your security code is: {0} + /// + internal static string FormatDefaultPhoneNumberTokenProviderMessageFormat(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DefaultPhoneNumberTokenProviderMessageFormat"), p0); + } + + /// + /// Phone + /// + internal static string DefaultPhoneNumberTokenProviderName + { + get { return GetString("DefaultPhoneNumberTokenProviderName"); } + } + + /// + /// Phone + /// + internal static string FormatDefaultPhoneNumberTokenProviderName() + { + return GetString("DefaultPhoneNumberTokenProviderName"); + } + + /// + /// DefaultTokenProvider + /// + internal static string DefaultTokenProvider + { + get { return GetString("DefaultTokenProvider"); } + } + + /// + /// DefaultTokenProvider + /// + internal static string FormatDefaultTokenProvider() + { + return GetString("DefaultTokenProvider"); + } + /// /// Email '{0}' is already taken. /// @@ -139,7 +235,7 @@ namespace Microsoft.AspNet.Identity } /// - /// No IUserTokenProvider is registered. + /// No IUserTokenProvider named '{0}' is registered. /// internal static string NoTokenProvider { @@ -147,27 +243,11 @@ namespace Microsoft.AspNet.Identity } /// - /// No IUserTokenProvider is registered. + /// No IUserTokenProvider named '{0}' is registered. /// - internal static string FormatNoTokenProvider() + internal static string FormatNoTokenProvider(object p0) { - return GetString("NoTokenProvider"); - } - - /// - /// No IUserTwoFactorProvider for '{0}' is registered. - /// - internal static string NoTwoFactorProvider - { - get { return GetString("NoTwoFactorProvider"); } - } - - /// - /// No IUserTwoFactorProvider for '{0}' is registered. - /// - internal static string FormatNoTwoFactorProvider(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("NoTwoFactorProvider"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("NoTokenProvider"), p0); } /// diff --git a/src/Microsoft.AspNet.Identity/Resources.resx b/src/Microsoft.AspNet.Identity/Resources.resx index d7f2489732..56f3d202d2 100644 --- a/src/Microsoft.AspNet.Identity/Resources.resx +++ b/src/Microsoft.AspNet.Identity/Resources.resx @@ -117,10 +117,34 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Your security code is: {0} + Default body format for the email + + + Email + Default name for the email token provider + + + Security Code + Default subject for the email + An unknown failure has occured. Default identity result error message + + Your security code is: {0} + Default message format for the phone number token provider + + + Phone + Default name for the phone number token provider + + + DefaultTokenProvider + Name of the default token provider + Email '{0}' is already taken. error for duplicate emails @@ -150,13 +174,9 @@ error when lockout is not enabled - No IUserTokenProvider is registered. + No IUserTokenProvider named '{0}' is registered. Error when there is no IUserTokenProvider - - No IUserTwoFactorProvider for '{0}' is registered. - Error when there is no provider found - Incorrect password. Error when a password doesn't match diff --git a/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs b/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs index 2a491f505a..7cbe783122 100644 --- a/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs +++ b/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Identity private static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) { // # of 0's = length of pin - const int mod = 1000000; + const int Mod = 1000000; // See https://tools.ietf.org/html/rfc4226 // We can add an optional modifier @@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Identity | (hash[offset + 2] & 0xff) << 8 | (hash[offset + 3] & 0xff); - return binaryCode % mod; + return binaryCode % Mod; } private static byte[] ApplyModifier(byte[] input, string modifier) diff --git a/src/Microsoft.AspNet.Identity/SecurityStampValidator.cs b/src/Microsoft.AspNet.Identity/SecurityStampValidator.cs new file mode 100644 index 0000000000..6b878d74d7 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/SecurityStampValidator.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Identity +{ + public class SecurityStampValidator : ISecurityStampValidator where TUser : class + { + /// + /// Rejects the identity if the stamp changes, and otherwise will sign in a new + /// ClaimsIdentity + /// + /// + public virtual async Task Validate(CookieValidateIdentityContext context, ClaimsIdentity identity) + { + var manager = context.HttpContext.RequestServices.GetService>(); + var userId = identity.GetUserId(); + var user = await manager.ValidateSecurityStampAsync(identity, userId); + if (user != null) + { + var isPersistent = false; + if (context.Properties != null) + { + isPersistent = context.Properties.IsPersistent; + } + await manager.SignInAsync(user, isPersistent); + } + else + { + context.RejectIdentity(); + manager.SignOut(); + } + } + } + + /// + /// Static helper class used to configure a CookieAuthenticationNotifications to validate a cookie against a user's security + /// stamp + /// + public static class SecurityStampValidator + { + public static Task ValidateIdentityAsync(CookieValidateIdentityContext context) + { + var currentUtc = DateTimeOffset.UtcNow; + if (context.Options != null && context.Options.SystemClock != null) + { + currentUtc = context.Options.SystemClock.UtcNow; + } + var issuedUtc = context.Properties.IssuedUtc; + + // Only validate if enough time has elapsed + var validate = (issuedUtc == null); + if (issuedUtc != null) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var identityOptions = context.HttpContext.RequestServices.GetService>().Options; + validate = timeElapsed > identityOptions.SecurityStampValidationInterval; + } + if (validate) + { + var validator = context.HttpContext.RequestServices.GetService(); + return validator.Validate(context, context.Identity); + } + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/SignInManager.cs b/src/Microsoft.AspNet.Identity/SignInManager.cs index 50d6893bec..576f357eae 100644 --- a/src/Microsoft.AspNet.Identity/SignInManager.cs +++ b/src/Microsoft.AspNet.Identity/SignInManager.cs @@ -6,6 +6,9 @@ using System.Security.Claims; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Identity @@ -16,33 +19,33 @@ namespace Microsoft.AspNet.Identity /// public class SignInManager where TUser : class { - public SignInManager(UserManager userManager, IAuthenticationManager authenticationManager, + public SignInManager(UserManager userManager, IContextAccessor contextAccessor, IClaimsIdentityFactory claimsFactory, IOptionsAccessor optionsAccessor) { if (userManager == null) { - throw new ArgumentNullException("userManager"); + throw new ArgumentNullException(nameof(userManager)); } - if (authenticationManager == null) + if (contextAccessor == null || contextAccessor.Value == null) { - throw new ArgumentNullException("authenticationManager"); + throw new ArgumentNullException(nameof(contextAccessor)); } if (claimsFactory == null) { - throw new ArgumentNullException("claimsFactory"); + throw new ArgumentNullException(nameof(claimsFactory)); } if (optionsAccessor == null || optionsAccessor.Options == null) { - throw new ArgumentNullException("optionsAccessor"); + throw new ArgumentNullException(nameof(optionsAccessor)); } UserManager = userManager; - AuthenticationManager = authenticationManager; + Context = contextAccessor.Value; ClaimsFactory = claimsFactory; Options = optionsAccessor.Options; } public UserManager UserManager { get; private set; } - public IAuthenticationManager AuthenticationManager { get; private set; } + public HttpContext Context { get; private set; } public IClaimsIdentityFactory ClaimsFactory { get; private set; } public IdentityOptions Options { get; private set; } @@ -50,7 +53,6 @@ namespace Microsoft.AspNet.Identity public virtual async Task CreateUserIdentityAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { - // REVIEW: should sign in manager take options instead of using the user manager instance? return await ClaimsFactory.CreateAsync(user, Options.ClaimsIdentity); } @@ -68,18 +70,22 @@ namespace Microsoft.AspNet.Identity // return true; //} - public virtual async Task SignInAsync(TUser user, bool isPersistent, + public virtual async Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null, CancellationToken cancellationToken = default(CancellationToken)) { var userIdentity = await CreateUserIdentityAsync(user); - AuthenticationManager.SignIn(userIdentity, isPersistent); + if (authenticationMethod != null) + { + userIdentity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod)); + } + Context.Response.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, userIdentity); } // TODO: Should this be async? public virtual void SignOut() { // REVIEW: need a new home for this option config? - AuthenticationManager.SignOut(Options.ClaimsIdentity.AuthenticationType); + Context.Response.SignOut(Options.ClaimsIdentity.AuthenticationType); } private async Task IsLockedOut(TUser user, CancellationToken token) @@ -138,16 +144,31 @@ namespace Microsoft.AspNet.Identity return SignInStatus.Failure; } + private static ClaimsIdentity CreateIdentity(TwoFactorAuthenticationInfo info) + { + if (info == null) + { + return null; + } + var identity = new ClaimsIdentity(ClaimsIdentityOptions.DefaultTwoFactorUserIdAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.Name, info.UserId)); + if (info.LoginProvider != null) + { + identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, info.LoginProvider)); + } + return identity; + } + public virtual async Task SendTwoFactorCodeAsync(string provider, CancellationToken cancellationToken = default(CancellationToken)) { - var userId = await AuthenticationManager.RetrieveUserId(); - if (userId == null) + var twoFactorInfo = await RetrieveTwoFactorInfoAsync(cancellationToken); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) { return false; } - var user = await UserManager.FindByIdAsync(userId, cancellationToken); + var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId, cancellationToken); if (user == null) { return false; @@ -162,31 +183,35 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { var userId = await UserManager.GetUserIdAsync(user, cancellationToken); - return await AuthenticationManager.IsClientRememeberedAsync(userId, cancellationToken); + var result = + await Context.AuthenticateAsync(ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType); + return (result != null && result.Identity != null && result.Identity.Name == userId); } - public virtual async Task RememberTwoFactorClient(TUser user, + public virtual async Task RememberTwoFactorClientAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { var userId = await UserManager.GetUserIdAsync(user, cancellationToken); - AuthenticationManager.RememberClient(userId); + var rememberBrowserIdentity = new ClaimsIdentity(ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType); + rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); + Context.Response.SignIn(new AuthenticationProperties { IsPersistent = true }, rememberBrowserIdentity); } public virtual Task ForgetTwoFactorClientAsync() { - AuthenticationManager.ForgetClient(); + Context.Response.SignOut(ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType); return Task.FromResult(0); } - public virtual async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, - CancellationToken cancellationToken = default(CancellationToken)) + public virtual async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, + bool rememberClient, CancellationToken cancellationToken = default(CancellationToken)) { - var userId = await AuthenticationManager.RetrieveUserId(); - if (userId == null) + var twoFactorInfo = await RetrieveTwoFactorInfoAsync(cancellationToken); + if (twoFactorInfo == null || twoFactorInfo.UserId == null) { return SignInStatus.Failure; } - var user = await UserManager.FindByIdAsync(userId, cancellationToken); + var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId, cancellationToken); if (user == null) { return SignInStatus.Failure; @@ -199,7 +224,11 @@ namespace Microsoft.AspNet.Identity { // When token is verified correctly, clear the access failed count used for lockout await UserManager.ResetAccessFailedCountAsync(user, cancellationToken); - await SignInAsync(user, isPersistent); + await SignInAsync(user, isPersistent, twoFactorInfo.LoginProvider, cancellationToken); + if (rememberClient) + { + await RememberTwoFactorClientAsync(user, cancellationToken); + } return SignInStatus.Success; } // If the token is incorrect, record the failure which also may cause the user to be locked out @@ -207,6 +236,23 @@ namespace Microsoft.AspNet.Identity return SignInStatus.Failure; } + /// + /// Returns the user who has started the two factor authentication process + /// + /// + /// + public virtual async Task GetTwoFactorAuthenticationUserAsync( + CancellationToken cancellationToken = default(CancellationToken)) + { + var info = await RetrieveTwoFactorInfoAsync(cancellationToken); + if (info == null) + { + return null; + } + + return await UserManager.FindByIdAsync(info.UserId, cancellationToken); + } + public async Task ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, CancellationToken cancellationToken = default(CancellationToken)) { @@ -219,24 +265,57 @@ namespace Microsoft.AspNet.Identity { return SignInStatus.LockedOut; } - return await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken); + return await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken, loginProvider); } private async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, - CancellationToken cancellationToken) + CancellationToken cancellationToken, string loginProvider = null) { - if (UserManager.SupportsUserTwoFactor && await UserManager.GetTwoFactorEnabledAsync(user)) + if (UserManager.SupportsUserTwoFactor && + await UserManager.GetTwoFactorEnabledAsync(user, cancellationToken) && + (await UserManager.GetValidTwoFactorProvidersAsync(user, cancellationToken)).Count > 0) { if (!await IsTwoFactorClientRememberedAsync(user, cancellationToken)) { // Store the userId for use after two factor check var userId = await UserManager.GetUserIdAsync(user, cancellationToken); - await AuthenticationManager.StoreUserId(userId); + Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider)); return SignInStatus.RequiresVerification; } } - await SignInAsync(user, isPersistent, cancellationToken); + await SignInAsync(user, isPersistent, loginProvider, cancellationToken); return SignInStatus.Success; } + + private async Task RetrieveTwoFactorInfoAsync(CancellationToken cancellationToken) + { + var result = await Context.AuthenticateAsync(ClaimsIdentityOptions.DefaultTwoFactorUserIdAuthenticationType); + if (result != null && result.Identity != null) + { + return new TwoFactorAuthenticationInfo + { + UserId = result.Identity.Name, + LoginProvider = result.Identity.FindFirstValue(ClaimTypes.AuthenticationMethod) + }; + } + return null; + } + + internal static ClaimsIdentity StoreTwoFactorInfo(string userId, string loginProvider) + { + var identity = new ClaimsIdentity(ClaimsIdentityOptions.DefaultTwoFactorUserIdAuthenticationType); + identity.AddClaim(new Claim(ClaimTypes.Name, userId)); + if (loginProvider != null) + { + identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); + } + return identity; + } + + internal class TwoFactorAuthenticationInfo + { + public string UserId { get; set; } + public string LoginProvider { get; set; } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/SignInOptions.cs b/src/Microsoft.AspNet.Identity/SignInOptions.cs index 6c20b64713..a488679615 100644 --- a/src/Microsoft.AspNet.Identity/SignInOptions.cs +++ b/src/Microsoft.AspNet.Identity/SignInOptions.cs @@ -5,8 +5,6 @@ namespace Microsoft.AspNet.Identity { public class SignInOptions { - public SignInOptions() { } - /// /// If set, requires a confirmed email to sign in /// diff --git a/src/Microsoft.AspNet.Identity/TotpSecurityStampBasedTokenProvider.cs b/src/Microsoft.AspNet.Identity/TotpSecurityStampBasedTokenProvider.cs index c032693585..19d6732b52 100644 --- a/src/Microsoft.AspNet.Identity/TotpSecurityStampBasedTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/TotpSecurityStampBasedTokenProvider.cs @@ -10,9 +10,11 @@ namespace Microsoft.AspNet.Identity /// /// /// - public class TotpSecurityStampBasedTokenProvider : IUserTokenProvider + public abstract class TotpSecurityStampBasedTokenProvider : IUserTokenProvider where TUser : class { + public abstract string Name { get; } + /// /// This token provider does not notify the user by default /// @@ -26,23 +28,6 @@ namespace Microsoft.AspNet.Identity return Task.FromResult(0); } - /// - /// Returns true if the provider can generate tokens for the user, by default this is equal to - /// manager.SupportsUserSecurityStamp - /// - /// - /// - /// - public virtual Task IsValidProviderForUserAsync(UserManager manager, TUser user, - CancellationToken cancellationToken = default(CancellationToken)) - { - if (manager == null) - { - throw new ArgumentNullException("manager"); - } - return Task.FromResult(manager.SupportsUserSecurityStamp); - } - /// /// Generate a token for the user using their security stamp /// @@ -101,8 +86,10 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("manager"); } - string userId = await manager.GetUserIdAsync(user); + var userId = await manager.GetUserIdAsync(user); return "Totp:" + purpose + ":" + userId; } + + public abstract Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserLoginInfo.cs b/src/Microsoft.AspNet.Identity/UserLoginInfo.cs index ecfde6f8dd..16c8e790d5 100644 --- a/src/Microsoft.AspNet.Identity/UserLoginInfo.cs +++ b/src/Microsoft.AspNet.Identity/UserLoginInfo.cs @@ -6,34 +6,21 @@ namespace Microsoft.AspNet.Identity /// /// Represents a linked login for a user (i.e. a local username/password or a facebook/google account /// - public sealed class UserLoginInfo + public class UserLoginInfo(string loginProvider, string providerKey, string displayName) { - /// - /// Constructor - /// - /// - /// - /// - public UserLoginInfo(string loginProvider, string providerKey, string displayName) - { - LoginProvider = loginProvider; - ProviderKey = providerKey; - ProviderDisplayName = displayName; - } - /// /// Provider for the linked login, i.e. Local, Facebook, Google, etc. /// - public string LoginProvider { get; set; } + public string LoginProvider { get; set; } = loginProvider; /// /// Key for the linked login at the provider /// - public string ProviderKey { get; set; } + public string ProviderKey { get; set; } = providerKey; /// /// Display name for the provider /// - public string ProviderDisplayName { get; set; } + public string ProviderDisplayName { get; set; } = displayName; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index a496e1bc8a..247410f2a6 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Framework.OptionsModel; +using Microsoft.AspNet.Security.DataProtection; namespace Microsoft.AspNet.Identity { @@ -19,7 +20,7 @@ namespace Microsoft.AspNet.Identity /// public class UserManager : IDisposable where TUser : class { - private readonly Dictionary> _factors = + private readonly Dictionary> _tokenProviders = new Dictionary>(); private TimeSpan _defaultLockout = TimeSpan.Zero; @@ -38,19 +39,20 @@ namespace Microsoft.AspNet.Identity /// public UserManager(IUserStore store, IOptionsAccessor optionsAccessor, IPasswordHasher passwordHasher, IUserValidator userValidator, - IPasswordValidator passwordValidator, IUserNameNormalizer userNameNormalizer) + IPasswordValidator passwordValidator, IUserNameNormalizer userNameNormalizer, + IEnumerable> tokenProviders) { if (store == null) { - throw new ArgumentNullException("store"); + throw new ArgumentNullException(nameof(store)); } if (optionsAccessor == null || optionsAccessor.Options == null) { - throw new ArgumentNullException("optionsAccessor"); + throw new ArgumentNullException(nameof(optionsAccessor)); } if (passwordHasher == null) { - throw new ArgumentNullException("passwordHasher"); + throw new ArgumentNullException(nameof(passwordHasher)); } Store = store; Options = optionsAccessor.Options; @@ -59,6 +61,13 @@ namespace Microsoft.AspNet.Identity PasswordValidator = passwordValidator; UserNameNormalizer = userNameNormalizer; // TODO: Email/Sms/Token services + + if (tokenProviders != null) { + foreach (var tokenProvider in tokenProviders) + { + RegisterTokenProvider(tokenProvider); + } + } } /// @@ -112,11 +121,6 @@ namespace Microsoft.AspNet.Identity /// public IIdentityMessageService SmsService { get; set; } - /// - /// Used for generating ResetPassword and Confirmation Tokens - /// - public IUserTokenProvider UserTokenProvider { get; set; } - public IdentityOptions Options { get @@ -271,14 +275,6 @@ namespace Microsoft.AspNet.Identity } } - /// - /// Dictionary mapping user two factor providers - /// - public IDictionary> TwoFactorProviders - { - get { return _factors; } - } - /// /// Dispose the store context /// @@ -451,7 +447,7 @@ namespace Microsoft.AspNet.Identity public virtual async Task UpdateNormalizedUserName(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { - string userName = await GetUserNameAsync(user, cancellationToken); + var userName = await GetUserNameAsync(user, cancellationToken); await Store.SetNormalizedUserNameAsync(user, NormalizeUserName(userName), cancellationToken); } @@ -497,7 +493,6 @@ namespace Microsoft.AspNet.Identity await UpdateNormalizedUserName(user, cancellationToken); } - /// /// Get the user's id /// @@ -651,7 +646,7 @@ namespace Microsoft.AspNet.Identity { if (PasswordValidator != null) { - var result = await PasswordValidator.ValidateAsync(newPassword, this, cancellationToken); + var result = await PasswordValidator.ValidateAsync(user, newPassword, this, cancellationToken); if (!result.Succeeded) { return result; @@ -736,7 +731,7 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - return await GenerateUserTokenAsync("ResetPassword", user, cancellationToken); + return await GenerateUserTokenAsync(user, Options.PasswordResetTokenProvider, "ResetPassword", cancellationToken); } /// @@ -756,7 +751,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } // Make sure the token is valid and the stamp matches - if (!await VerifyUserTokenAsync(user, "ResetPassword", token, cancellationToken)) + if (!await VerifyUserTokenAsync(user, Options.PasswordResetTokenProvider, "ResetPassword", token, cancellationToken)) { return IdentityResult.Failed(Resources.InvalidToken); } @@ -974,7 +969,6 @@ namespace Microsoft.AspNet.Identity return RemoveClaimsAsync(user, new Claim[] { claim }, cancellationToken); } - /// /// Remove a user claim /// @@ -1271,7 +1265,7 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - return GenerateUserTokenAsync("Confirmation", user, cancellationToken); + return GenerateUserTokenAsync(user, Options.EmailConfirmationTokenProvider, "Confirmation", cancellationToken); } /// @@ -1290,7 +1284,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - if (!await VerifyUserTokenAsync(user, "Confirmation", token, cancellationToken)) + if (!await VerifyUserTokenAsync(user, Options.EmailConfirmationTokenProvider, "Confirmation", token, cancellationToken)) { return IdentityResult.Failed(Resources.InvalidToken); } @@ -1461,20 +1455,24 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task VerifyUserTokenAsync(TUser user, string purpose, string token, + public virtual async Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, string token, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - if (UserTokenProvider == null) - { - throw new NotSupportedException(Resources.NoTokenProvider); - } if (user == null) { throw new ArgumentNullException("user"); } + if (tokenProvider == null) + { + throw new ArgumentNullException(nameof(tokenProvider)); + } + if (!_tokenProviders.ContainsKey(tokenProvider)) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.NoTokenProvider, tokenProvider)); + } // Make sure the token is valid - return await UserTokenProvider.ValidateAsync(purpose, token, this, user, cancellationToken); + return await _tokenProviders[tokenProvider].ValidateAsync(purpose, token, this, user, cancellationToken); } /// @@ -1484,38 +1482,38 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task GenerateUserTokenAsync(string purpose, TUser user, + public virtual async Task GenerateUserTokenAsync(TUser user, string tokenProvider, string purpose, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - if (UserTokenProvider == null) - { - throw new NotSupportedException(Resources.NoTokenProvider); - } if (user == null) { throw new ArgumentNullException("user"); } - return await UserTokenProvider.GenerateAsync(purpose, this, user, cancellationToken); + if (tokenProvider == null) + { + throw new ArgumentNullException(nameof(tokenProvider)); + } + if (!_tokenProviders.ContainsKey(tokenProvider)) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.NoTokenProvider, tokenProvider)); + } + return await _tokenProviders[tokenProvider].GenerateAsync(purpose, this, user, cancellationToken); } /// - /// Register a user two factor provider + /// Register a user token provider /// /// /// - public virtual void RegisterTwoFactorProvider(string twoFactorProvider, IUserTokenProvider provider) + public virtual void RegisterTokenProvider(IUserTokenProvider provider) { ThrowIfDisposed(); - if (twoFactorProvider == null) - { - throw new ArgumentNullException("twoFactorProvider"); - } if (provider == null) { throw new ArgumentNullException("provider"); } - TwoFactorProviders[twoFactorProvider] = provider; + _tokenProviders[provider.Name] = provider; } /// @@ -1533,9 +1531,9 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } var results = new List(); - foreach (var f in TwoFactorProviders) + foreach (var f in _tokenProviders) { - if (await f.Value.IsValidProviderForUserAsync(this, user, cancellationToken)) + if (await f.Value.CanGenerateTwoFactorTokenAsync(this, user, cancellationToken)) { results.Add(f.Key); } @@ -1551,7 +1549,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task VerifyTwoFactorTokenAsync(TUser user, string twoFactorProvider, string token, + public virtual async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -1559,14 +1557,13 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - if (!_factors.ContainsKey(twoFactorProvider)) + if (!_tokenProviders.ContainsKey(tokenProvider)) { throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, - Resources.NoTwoFactorProvider, twoFactorProvider)); + Resources.NoTokenProvider, tokenProvider)); } // Make sure the token is valid - var provider = _factors[twoFactorProvider]; - return await provider.ValidateAsync(twoFactorProvider, token, this, user, cancellationToken); + return await _tokenProviders[tokenProvider].ValidateAsync("TwoFactor", token, this, user, cancellationToken); } /// @@ -1576,7 +1573,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task GenerateTwoFactorTokenAsync(TUser user, string twoFactorProvider, + public virtual async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -1584,23 +1581,23 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - if (!_factors.ContainsKey(twoFactorProvider)) + if (!_tokenProviders.ContainsKey(tokenProvider)) { throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, - Resources.NoTwoFactorProvider, twoFactorProvider)); + Resources.NoTokenProvider, tokenProvider)); } - return await _factors[twoFactorProvider].GenerateAsync(twoFactorProvider, this, user, cancellationToken); + return await _tokenProviders[tokenProvider].GenerateAsync("TwoFactor", this, user, cancellationToken); } /// /// Notify a user with a token from a specific user factor provider /// /// - /// + /// /// /// /// - public virtual async Task NotifyTwoFactorTokenAsync(TUser user, string twoFactorProvider, + public virtual async Task NotifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -1608,12 +1605,16 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - if (!_factors.ContainsKey(twoFactorProvider)) + if (tokenProvider == null) + { + throw new ArgumentNullException(nameof(tokenProvider)); + } + if (!_tokenProviders.ContainsKey(tokenProvider)) { throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, - Resources.NoTwoFactorProvider, twoFactorProvider)); + Resources.NoTokenProvider, tokenProvider)); } - await _factors[twoFactorProvider].NotifyAsync(token, this, user, cancellationToken); + await _tokenProviders[tokenProvider].NotifyAsync(token, this, user, cancellationToken); return IdentityResult.Success; } diff --git a/src/Microsoft.AspNet.Identity/project.json b/src/Microsoft.AspNet.Identity/project.json index ba96e28097..6c44cd8eac 100644 --- a/src/Microsoft.AspNet.Identity/project.json +++ b/src/Microsoft.AspNet.Identity/project.json @@ -1,6 +1,9 @@ { "version": "3.0.0-*", "dependencies": { + "Microsoft.AspNet.Http" : "1.0.0-*", + "Microsoft.AspNet.Security" : "1.0.0-*", + "Microsoft.AspNet.Security.Cookies" : "1.0.0-*", "Microsoft.Framework.ConfigurationModel": "1.0.0-*", "Microsoft.Framework.DependencyInjection" : "1.0.0-*", "Microsoft.Framework.OptionsModel": "1.0.0-*", diff --git a/test/Microsoft.AspNet.Identity.Authentication.Test/Microsoft.AspNet.Identity.Authentication.Test.kproj b/test/Microsoft.AspNet.Identity.Authentication.Test/Microsoft.AspNet.Identity.Authentication.Test.kproj deleted file mode 100644 index 316dfaf8fd..0000000000 --- a/test/Microsoft.AspNet.Identity.Authentication.Test/Microsoft.AspNet.Identity.Authentication.Test.kproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 823453cc-5846-4d49-b343-15bc0074ca60 - Library - net45 - - - - - - - 2.0 - - - \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Authentication.Test/project.json b/test/Microsoft.AspNet.Identity.Authentication.Test/project.json deleted file mode 100644 index 50211cbdf9..0000000000 --- a/test/Microsoft.AspNet.Identity.Authentication.Test/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "dependencies": { - "Microsoft.AspNet.FeatureModel" : "1.0.0-*", - "Microsoft.AspNet.Http" : "1.0.0-*", - "Microsoft.AspNet.HttpFeature" : "1.0.0-*", - "Microsoft.AspNet.Identity" : "", - "Microsoft.AspNet.Identity.Authentication" : "", - "Microsoft.AspNet.PipelineCore" : "1.0.0-*", - "Microsoft.AspNet.RequestContainer" : "1.0.0-*", - "Microsoft.AspNet.Security" : "1.0.0-*", - "Microsoft.AspNet.Security.Cookies" : "1.0.0-*", - "Microsoft.AspNet.Testing" : "1.0.0-*", - "Microsoft.Framework.ConfigurationModel": "1.0.0-*", - "Microsoft.Framework.DependencyInjection" : "1.0.0-*", - "Microsoft.Framework.OptionsModel" : "1.0.0-*", - "Microsoft.Framework.Logging" : "1.0.0-*", - "System.Security.Claims" : "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" - }, - "code": "**\\*.cs;..\\Shared\\*.cs", - "frameworks": { - "aspnet50": { - "dependencies": { - "Moq" : "4.2.1312.1622", - "System.Runtime": "", - "System.Collections": "" - } - } - }, - "commands": { - "test": "Xunit.KRunner" - } -} diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs index caceeb4fb1..0e18d83eed 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test app.UseServices(services => { services.AddInstance(contextAccessor.Object); - services.AddIdentity().AddInMemory().AddAuthentication(); + services.AddIdentity().AddInMemory(); }); // Act diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/project.json b/test/Microsoft.AspNet.Identity.InMemory.Test/project.json index e4b1a44a89..0b01f0adb6 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/project.json +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/project.json @@ -2,7 +2,6 @@ "dependencies": { "Microsoft.AspNet.Http" : "1.0.0-*", "Microsoft.AspNet.Identity" : "", - "Microsoft.AspNet.Identity.Authentication" : "", "Microsoft.AspNet.PipelineCore" : "1.0.0-*", "Microsoft.AspNet.RequestContainer" : "1.0.0-*", "Microsoft.AspNet.Security" : "1.0.0-*", diff --git a/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/RoleStoreTest.cs b/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/RoleStoreTest.cs index 8437a7a6ac..ad8cc5d332 100644 --- a/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/RoleStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/RoleStoreTest.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.InMemory.Test var services = new ServiceCollection(); services.AddEntityFramework().AddInMemoryStore(); var store = new RoleStore(new InMemoryContext()); - services.AddIdentity().AddRoleStore(() => store); + services.AddIdentity().AddRoleStore(store); var provider = services.BuildServiceProvider(); var manager = provider.GetService>(); Assert.NotNull(manager); diff --git a/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/TestIdentityFactory.cs b/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/TestIdentityFactory.cs index d4395312b6..1bac21ef3a 100644 --- a/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/TestIdentityFactory.cs +++ b/test/Microsoft.AspNet.Identity.SqlServer.InMemory.Test/TestIdentityFactory.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.InMemory.Test public static UserManager CreateManager(InMemoryContext context) { - return MockHelpers.CreateManager(() => new InMemoryUserStore(context)); + return MockHelpers.CreateManager(new InMemoryUserStore(context)); } public static UserManager CreateManager() @@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.InMemory.Test public static RoleManager CreateRoleManager(InMemoryContext context) { var services = new ServiceCollection(); - services.AddIdentity().AddRoleStore(() => new RoleStore(context)); + services.AddIdentity().AddRoleStore(new RoleStore(context)); return services.BuildServiceProvider().GetService>(); } diff --git a/test/Microsoft.AspNet.Identity.SqlServer.Test/SqlStoreTestBase.cs b/test/Microsoft.AspNet.Identity.SqlServer.Test/SqlStoreTestBase.cs index eacc95d6ad..050c37a069 100644 --- a/test/Microsoft.AspNet.Identity.SqlServer.Test/SqlStoreTestBase.cs +++ b/test/Microsoft.AspNet.Identity.SqlServer.Test/SqlStoreTestBase.cs @@ -84,7 +84,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.Test { context = CreateTestContext(); } - return MockHelpers.CreateManager(() => new UserStore((ApplicationDbContext)context)); + return MockHelpers.CreateManager(new UserStore((ApplicationDbContext)context)); } protected override RoleManager CreateRoleManager(object context = null) @@ -94,7 +94,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.Test context = CreateTestContext(); } var services = new ServiceCollection(); - services.AddIdentity().AddRoleStore(() => new RoleStore((ApplicationDbContext)context)); + services.AddIdentity().AddRoleStore(new RoleStore((ApplicationDbContext)context)); return services.BuildServiceProvider().GetService>(); } diff --git a/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreGuidKeyTest.cs b/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreGuidKeyTest.cs index 38fbaebb60..04a591f4b6 100644 --- a/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreGuidKeyTest.cs +++ b/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreGuidKeyTest.cs @@ -66,7 +66,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.Test { context = CreateTestContext(); } - return MockHelpers.CreateManager(() => new ApplicationUserStore((ApplicationDbContext)context)); + return MockHelpers.CreateManager(new ApplicationUserStore((ApplicationDbContext)context)); } protected override RoleManager CreateRoleManager(object context) @@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.Test context = CreateTestContext(); } var services = new ServiceCollection(); - services.AddIdentity().AddRoleStore(() => new ApplicationRoleStore((ApplicationDbContext)context)); + services.AddIdentity().AddRoleStore(new ApplicationRoleStore((ApplicationDbContext)context)); return services.BuildServiceProvider().GetService>(); } } diff --git a/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreTest.cs b/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreTest.cs index 4bd79f6296..b1f0bbf1bb 100644 --- a/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.SqlServer.Test/UserStoreTest.cs @@ -172,7 +172,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.Test public static UserManager CreateManager(DbContext context) { - return MockHelpers.CreateManager(() => new UserStore(context)); + return MockHelpers.CreateManager(new UserStore(context)); } protected override UserManager CreateManager(object context = null) @@ -187,7 +187,7 @@ namespace Microsoft.AspNet.Identity.SqlServer.Test public static RoleManager CreateRoleManager(IdentityDbContext context) { var services = new ServiceCollection(); - services.AddIdentity().AddRoleStore(() => new RoleStore(context)); + services.AddIdentity().AddRoleStore(new RoleStore(context)); return services.BuildServiceProvider().GetService>(); } diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs index e68f9b4ffe..41820382dc 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Identity.Test { var services = new ServiceCollection(); var validator = new UserValidator(); - services.AddIdentity().AddUserValidator(() => validator); + services.AddIdentity().AddUserValidator(validator); Assert.Equal(validator, services.BuildServiceProvider().GetService>()); } @@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Identity.Test { var services = new ServiceCollection(); var validator = new PasswordValidator(); - services.AddIdentity().AddPasswordValidator(() => validator); + services.AddIdentity().AddPasswordValidator(validator); Assert.Equal(validator, services.BuildServiceProvider().GetService>()); } @@ -54,7 +54,7 @@ namespace Microsoft.AspNet.Identity.Test private static void CanOverride(TService instance) { var services = new ServiceCollection(); - services.AddIdentity().AddInstance(() => instance); + services.AddIdentity().AddInstance(instance); Assert.Equal(instance, services.BuildServiceProvider().GetService()); } diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs index 240992d95d..960ed8fbac 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs @@ -91,6 +91,7 @@ namespace Microsoft.AspNet.Identity.Test public class PasswordsNegativeLengthSetup : IOptionsSetup { public int Order { get { return 0; } } + public string Name { get; set; } public void Setup(IdentityOptions options) { options.Password.RequiredLength = -1; diff --git a/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs index c373eb9eb8..e735bf4b93 100644 --- a/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs @@ -29,8 +29,8 @@ namespace Microsoft.AspNet.Identity.Test // Act // Assert - await Assert.ThrowsAsync("password", () => validator.ValidateAsync(null, null)); - await Assert.ThrowsAsync("manager", () => validator.ValidateAsync("foo", null)); + await Assert.ThrowsAsync("password", () => validator.ValidateAsync(null, null, null)); + await Assert.ThrowsAsync("manager", () => validator.ValidateAsync(null, "foo", null)); } @@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Options.Password.RequireNonLetterOrDigit = false; manager.Options.Password.RequireLowercase = false; manager.Options.Password.RequireDigit = false; - IdentityResultAssert.IsFailure(await valid.ValidateAsync(input, manager), error); + IdentityResultAssert.IsFailure(await valid.ValidateAsync(null, input, manager), error); } [Theory] @@ -61,7 +61,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Options.Password.RequireNonLetterOrDigit = false; manager.Options.Password.RequireLowercase = false; manager.Options.Password.RequireDigit = false; - IdentityResultAssert.IsSuccess(await valid.ValidateAsync(input, manager)); + IdentityResultAssert.IsSuccess(await valid.ValidateAsync(null, input, manager)); } [Theory] @@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Options.Password.RequireLowercase = false; manager.Options.Password.RequireDigit = false; manager.Options.Password.RequiredLength = 0; - IdentityResultAssert.IsFailure(await valid.ValidateAsync(input, manager), + IdentityResultAssert.IsFailure(await valid.ValidateAsync(null, input, manager), "Passwords must have at least one non letter and non digit character."); } @@ -93,7 +93,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Options.Password.RequireLowercase = false; manager.Options.Password.RequireDigit = false; manager.Options.Password.RequiredLength = 0; - IdentityResultAssert.IsSuccess(await valid.ValidateAsync(input, manager)); + IdentityResultAssert.IsSuccess(await valid.ValidateAsync(null, input, manager)); } [Theory] @@ -135,11 +135,11 @@ namespace Microsoft.AspNet.Identity.Test } if (errors.Count == 0) { - IdentityResultAssert.IsSuccess(await valid.ValidateAsync(input, manager)); + IdentityResultAssert.IsSuccess(await valid.ValidateAsync(null, input, manager)); } else { - IdentityResultAssert.IsFailure(await valid.ValidateAsync(input, manager), string.Join(" ", errors)); + IdentityResultAssert.IsFailure(await valid.ValidateAsync(null, input, manager), string.Join(" ", errors)); } } } diff --git a/test/Microsoft.AspNet.Identity.Authentication.Test/SecurityStampValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs similarity index 76% rename from test/Microsoft.AspNet.Identity.Authentication.Test/SecurityStampValidatorTest.cs rename to test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs index 336e1461ba..fccacb0c3a 100644 --- a/test/Microsoft.AspNet.Identity.Authentication.Test/SecurityStampValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.Identity.Test; using Microsoft.AspNet.Security; using Microsoft.AspNet.Security.Cookies; using Microsoft.Framework.DependencyInjection; @@ -16,7 +15,7 @@ using Microsoft.Framework.OptionsModel; using Moq; using Xunit; -namespace Microsoft.AspNet.Identity.Authentication.Test +namespace Microsoft.AspNet.Identity.Test { public class SecurityStampTest { @@ -28,7 +27,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var id = new ClaimsIdentity(ClaimsIdentityOptions.DefaultAuthenticationType); var ticket = new AuthenticationTicket(id, new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow }); var context = new CookieValidateIdentityContext(httpContext.Object, ticket, new CookieAuthenticationOptions()); - await Assert.ThrowsAsync(() => SecurityStampValidator.OnValidateIdentity(TimeSpan.Zero).Invoke(context)); + await Assert.ThrowsAsync(() => SecurityStampValidator.ValidateIdentityAsync(context)); } [Theory] @@ -37,19 +36,22 @@ namespace Microsoft.AspNet.Identity.Authentication.Test public async Task OnValidateIdentityTestSuccess(bool isPersistent) { var user = new IdentityUser("test"); - var httpContext = new Mock(); var userManager = MockHelpers.MockUserManager(); - var authManager = new Mock(); var claimsManager = new Mock>(); - var identityOptions = new IdentityOptions(); + var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.Zero }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); + var httpContext = new Mock(); + var contextAccessor = new Mock>(); + contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - authManager.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).ReturnsAsync(user).Verifiable(); - signInManager.Setup(s => s.SignInAsync(user, isPersistent, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + signInManager.Setup(s => s.SignInAsync(user, isPersistent, null, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); var services = new ServiceCollection(); + services.AddInstance(options.Object); services.AddInstance(signInManager.Object); + services.AddInstance(new SecurityStampValidator()); httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); var id = new ClaimsIdentity(ClaimsIdentityOptions.DefaultAuthenticationType); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); @@ -60,7 +62,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test Assert.NotNull(context.Options); Assert.NotNull(context.Identity); await - SecurityStampValidator.OnValidateIdentity(TimeSpan.Zero).Invoke(context); + SecurityStampValidator.ValidateIdentityAsync(context); Assert.NotNull(context.Identity); signInManager.VerifyAll(); } @@ -69,18 +71,21 @@ namespace Microsoft.AspNet.Identity.Authentication.Test public async Task OnValidateIdentityRejectsWhenValidateSecurityStampFails() { var user = new IdentityUser("test"); - var httpContext = new Mock(); var userManager = MockHelpers.MockUserManager(); - var authManager = new Mock(); var claimsManager = new Mock>(); - var identityOptions = new IdentityOptions(); + var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.Zero }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); + var httpContext = new Mock(); + var contextAccessor = new Mock>(); + contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - authManager.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).ReturnsAsync(null).Verifiable(); var services = new ServiceCollection(); + services.AddInstance(options.Object); services.AddInstance(signInManager.Object); + services.AddInstance(new SecurityStampValidator()); httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); var id = new ClaimsIdentity(ClaimsIdentityOptions.DefaultAuthenticationType); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); @@ -91,7 +96,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test Assert.NotNull(context.Options); Assert.NotNull(context.Identity); await - SecurityStampValidator.OnValidateIdentity(TimeSpan.Zero).Invoke(context); + SecurityStampValidator.ValidateIdentityAsync(context); Assert.Null(context.Identity); signInManager.VerifyAll(); } @@ -102,16 +107,19 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var user = new IdentityUser("test"); var httpContext = new Mock(); var userManager = MockHelpers.MockUserManager(); - var authManager = new Mock(); var claimsManager = new Mock>(); - var identityOptions = new IdentityOptions(); + var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.Zero }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); + var contextAccessor = new Mock>(); + contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - authManager.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).ReturnsAsync(null).Verifiable(); var services = new ServiceCollection(); + services.AddInstance(options.Object); services.AddInstance(signInManager.Object); + services.AddInstance(new SecurityStampValidator()); httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); var id = new ClaimsIdentity(ClaimsIdentityOptions.DefaultAuthenticationType); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); @@ -122,7 +130,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test Assert.NotNull(context.Options); Assert.NotNull(context.Identity); await - SecurityStampValidator.OnValidateIdentity(TimeSpan.Zero).Invoke(context); + SecurityStampValidator.ValidateIdentityAsync(context); Assert.Null(context.Identity); signInManager.VerifyAll(); } @@ -133,17 +141,20 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var user = new IdentityUser("test"); var httpContext = new Mock(); var userManager = MockHelpers.MockUserManager(); - var authManager = new Mock(); var claimsManager = new Mock>(); - var identityOptions = new IdentityOptions(); + var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.FromDays(1) }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); + var contextAccessor = new Mock>(); + contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - authManager.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).Throws(new Exception("Shouldn't be called")); - signInManager.Setup(s => s.SignInAsync(user, false, CancellationToken.None)).Throws(new Exception("Shouldn't be called")); + signInManager.Setup(s => s.SignInAsync(user, false, null, CancellationToken.None)).Throws(new Exception("Shouldn't be called")); var services = new ServiceCollection(); + services.AddInstance(options.Object); services.AddInstance(signInManager.Object); + services.AddInstance(new SecurityStampValidator()); httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); var id = new ClaimsIdentity(ClaimsIdentityOptions.DefaultAuthenticationType); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); @@ -154,7 +165,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test Assert.NotNull(context.Options); Assert.NotNull(context.Identity); await - SecurityStampValidator.OnValidateIdentity(TimeSpan.FromDays(1)).Invoke(context); + SecurityStampValidator.ValidateIdentityAsync(context); Assert.NotNull(context.Identity); } } diff --git a/test/Microsoft.AspNet.Identity.Authentication.Test/HttpSignInTest.cs b/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs similarity index 80% rename from test/Microsoft.AspNet.Identity.Authentication.Test/HttpSignInTest.cs rename to test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs index e2463670d5..d61935f0b3 100644 --- a/test/Microsoft.AspNet.Identity.Authentication.Test/HttpSignInTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs @@ -3,23 +3,21 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.Identity.Test; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; using Moq; using System; +using System.Collections.Generic; using System.Security.Claims; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNet.Identity.Authentication.Test +namespace Microsoft.AspNet.Identity.Test { - public class ApplicationUser : IdentityUser { } - - public class HttpSignInTest + public class SignManagerInTest { -#if ASPNET50 //[Theory] //[InlineData(true)] //[InlineData(false)] @@ -71,11 +69,14 @@ namespace Microsoft.AspNet.Identity.Authentication.Test { Assert.Throws("userManager", () => new SignInManager(null, null, null, null)); var userManager = MockHelpers.MockUserManager().Object; - Assert.Throws("authenticationManager", () => new SignInManager(userManager, null, null, null)); - var authManager = new Mock().Object; - Assert.Throws("claimsFactory", () => new SignInManager(userManager, authManager, null, null)); + Assert.Throws("contextAccessor", () => new SignInManager(userManager, null, null, null)); + var contextAccessor = new Mock>(); + Assert.Throws("contextAccessor", () => new SignInManager(userManager, contextAccessor.Object, null, null)); + var context = new Mock(); + contextAccessor.Setup(a => a.Value).Returns(context.Object); + Assert.Throws("claimsFactory", () => new SignInManager(userManager, contextAccessor.Object, null, null)); var claimsFactory = new Mock>().Object; - Assert.Throws("optionsAccessor", () => new SignInManager(userManager, authManager, claimsFactory, null)); + Assert.Throws("optionsAccessor", () => new SignInManager(userManager, contextAccessor.Object, claimsFactory, null)); } //TODO: Mock fails in K (this works fine in net45) @@ -125,7 +126,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false); @@ -159,7 +160,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test claimsFactory.Setup(m => m.CreateAsync(user, identityOptions.ClaimsIdentity, CancellationToken.None)).ReturnsAsync(new ClaimsIdentity("Microsoft.AspNet.Identity")).Verifiable(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "password", isPersistent, false); @@ -194,7 +195,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); @@ -220,6 +221,9 @@ namespace Microsoft.AspNet.Identity.Authentication.Test { manager.Setup(m => m.IsLockedOutAsync(user, CancellationToken.None)).ReturnsAsync(false).Verifiable(); } + IList providers = new List(); + providers.Add("PhoneNumber"); + manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user, CancellationToken.None)).Returns(Task.FromResult(providers)).Verifiable(); manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable(); manager.Setup(m => m.GetTwoFactorEnabledAsync(user, CancellationToken.None)).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); @@ -235,7 +239,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), new ClaimsIdentityFactory(manager.Object, roleManager.Object), options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, new ClaimsIdentityFactory(manager.Object, roleManager.Object), options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); @@ -269,7 +273,9 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var context = new Mock(); var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.IsAny())).Verifiable(); + response.Setup(r => r.SignIn( + It.Is(v => v.IsPersistent == isPersistent), + It.Is(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider))).Verifiable(); var contextAccessor = new Mock>(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); @@ -278,7 +284,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test claimsFactory.Setup(m => m.CreateAsync(user, identityOptions.ClaimsIdentity, CancellationToken.None)).ReturnsAsync(new ClaimsIdentity("Microsoft.AspNet.Identity")).Verifiable(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.ExternalLoginSignInAsync(loginProvider, providerKey, isPersistent); @@ -293,11 +299,24 @@ namespace Microsoft.AspNet.Identity.Authentication.Test } [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout) + [InlineData(true, true, true, true)] + [InlineData(true, true, false, true)] + [InlineData(true, false, true, true)] + [InlineData(true, false, false, true)] + [InlineData(false, true, true, true)] + [InlineData(false, true, false, true)] + [InlineData(false, false, true, true)] + [InlineData(false, false, false, true)] + [InlineData(true, true, true, false)] + [InlineData(true, true, false, false)] + [InlineData(true, false, true, false)] + [InlineData(true, false, false, false)] + [InlineData(false, true, true, false)] + [InlineData(false, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(false, false, false, false)] + + public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient) { // Setup var user = new TestUser { UserName = "Foo" }; @@ -315,23 +334,43 @@ namespace Microsoft.AspNet.Identity.Authentication.Test manager.Setup(m => m.GetUserNameAsync(user, CancellationToken.None)).ReturnsAsync(user.UserName).Verifiable(); var context = new Mock(); var response = new Mock(); - response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.IsAny())).Verifiable(); - context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - var id = new ClaimsIdentity(HttpAuthenticationManager.TwoFactorUserIdAuthenticationType); - id.AddClaim(new Claim(ClaimTypes.Name, user.Id)); - var authResult = new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription()); - context.Setup(c => c.AuthenticateAsync(HttpAuthenticationManager.TwoFactorUserIdAuthenticationType)).ReturnsAsync(authResult).Verifiable(); var contextAccessor = new Mock>(); - contextAccessor.Setup(a => a.Value).Returns(context.Object); + var twoFactorInfo = new SignInManager.TwoFactorAuthenticationInfo { UserId = user.Id }; + var loginProvider = "loginprovider"; + var id = SignInManager.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null); + var authResult = new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription()); var roleManager = MockHelpers.MockRoleManager(); var claimsFactory = new ClaimsIdentityFactory(manager.Object, roleManager.Object); var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory, options.Object); + if (externalLogin) + { + response.Setup(r => r.SignIn( + It.Is(v => v.IsPersistent == isPersistent), + It.Is(i => i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id + && i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider))).Verifiable(); + } + else + { + response.Setup(r => r.SignIn( + It.Is(v => v.IsPersistent == isPersistent), + It.Is(i => i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id))).Verifiable(); + } + if (rememberClient) + { + response.Setup(r => r.SignIn( + It.Is(v => v.IsPersistent == true), + It.Is(i => i.FindFirstValue(ClaimTypes.Name) == user.Id + && i.AuthenticationType == ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType))).Verifiable(); + } + context.Setup(c => c.Response).Returns(response.Object).Verifiable(); + context.Setup(c => c.AuthenticateAsync(ClaimsIdentityOptions.DefaultTwoFactorUserIdAuthenticationType)).ReturnsAsync(authResult).Verifiable(); + contextAccessor.Setup(a => a.Value).Returns(context.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory, options.Object); // Act - var result = await helper.TwoFactorSignInAsync(provider, code, isPersistent); + var result = await helper.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient); // Assert Assert.Equal(SignInStatus.Success, result); @@ -342,28 +381,39 @@ namespace Microsoft.AspNet.Identity.Authentication.Test } [Fact] - public void RememberClientStoresUserId() + public async Task RememberClientStoresUserId() { // Setup var user = new TestUser { UserName = "Foo" }; + var manager = MockHelpers.MockUserManager(); var context = new Mock(); var response = new Mock(); - context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - response.Setup(r => r.SignIn(It.Is(i => i.AuthenticationType == HttpAuthenticationManager.TwoFactorRememberedAuthenticationType))).Verifiable(); - var id = new ClaimsIdentity(HttpAuthenticationManager.TwoFactorRememberedAuthenticationType); - id.AddClaim(new Claim(ClaimTypes.Name, user.Id)); - var authResult = new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription()); var contextAccessor = new Mock>(); - contextAccessor.Setup(a => a.Value).Returns(context.Object); - var signInService = new HttpAuthenticationManager(contextAccessor.Object); + var roleManager = MockHelpers.MockRoleManager(); + var claimsFactory = new ClaimsIdentityFactory(manager.Object, roleManager.Object); + var identityOptions = new IdentityOptions(); + var options = new Mock>(); + + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id).Verifiable(); + context.Setup(c => c.Response).Returns(response.Object).Verifiable(); + response.Setup(r => r.SignIn( + It.Is(v => v.IsPersistent == true), + It.Is(i => i.FindFirstValue(ClaimTypes.Name) == user.Id + && i.AuthenticationType == ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType))).Verifiable(); + contextAccessor.Setup(a => a.Value).Returns(context.Object).Verifiable(); + options.Setup(a => a.Options).Returns(identityOptions).Verifiable(); + + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory, options.Object); // Act - signInService.RememberClient(user.Id); + await helper.RememberTwoFactorClientAsync(user); // Assert + manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); contextAccessor.VerifyAll(); + options.VerifyAll(); } [Theory] @@ -375,6 +425,9 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var user = new TestUser { UserName = "Foo" }; var manager = MockHelpers.MockUserManager(); manager.Setup(m => m.GetTwoFactorEnabledAsync(user, CancellationToken.None)).ReturnsAsync(true).Verifiable(); + IList providers = new List(); + providers.Add("PhoneNumber"); + manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user, CancellationToken.None)).Returns(Task.FromResult(providers)).Verifiable(); manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable(); manager.Setup(m => m.IsLockedOutAsync(user, CancellationToken.None)).ReturnsAsync(false).Verifiable(); @@ -385,20 +438,19 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.Is(i => i.AuthenticationType == ClaimsIdentityOptions.DefaultAuthenticationType))).Verifiable(); - var id = new ClaimsIdentity(HttpAuthenticationManager.TwoFactorRememberedAuthenticationType); + var id = new ClaimsIdentity(ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType); id.AddClaim(new Claim(ClaimTypes.Name, user.Id)); var authResult = new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription()); - context.Setup(c => c.AuthenticateAsync(HttpAuthenticationManager.TwoFactorRememberedAuthenticationType)).ReturnsAsync(authResult).Verifiable(); + context.Setup(c => c.AuthenticateAsync(ClaimsIdentityOptions.DefaultTwoFactorRememberMeAuthenticationType)).ReturnsAsync(authResult).Verifiable(); var contextAccessor = new Mock>(); contextAccessor.Setup(a => a.Value).Returns(context.Object); - var signInService = new HttpAuthenticationManager(contextAccessor.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); var claimsFactory = new Mock>(manager.Object, roleManager.Object); claimsFactory.Setup(m => m.CreateAsync(user, identityOptions.ClaimsIdentity, CancellationToken.None)).ReturnsAsync(new ClaimsIdentity(ClaimsIdentityOptions.DefaultAuthenticationType)).Verifiable(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, signInService, claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "password", isPersistent, false); @@ -431,7 +483,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); identityOptions.ClaimsIdentity.AuthenticationType = authenticationType; - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act helper.SignOut(); @@ -459,7 +511,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false); @@ -482,7 +534,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync("bogus", "bogus", false, false); @@ -516,7 +568,7 @@ namespace Microsoft.AspNet.Identity.Authentication.Test var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, new HttpAuthenticationManager(contextAccessor.Object), claimsFactory.Object, options.Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); // Act var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, true); @@ -525,6 +577,5 @@ namespace Microsoft.AspNet.Identity.Authentication.Test Assert.Equal(SignInStatus.LockedOut, result); manager.VerifyAll(); } -#endif } } diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index 31db0ad6af..2eae6335bf 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -24,14 +24,14 @@ namespace Microsoft.AspNet.Identity.Test public TestManager(IUserStore store, IOptionsAccessor optionsAccessor, IPasswordHasher passwordHasher, IUserValidator userValidator, IPasswordValidator passwordValidator) - : base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null) { } + : base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null) { } } [Fact] public void EnsureDefaultServicesDefaultsWithStoreWorks() { - var services = new ServiceCollection {IdentityServices.GetDefaultUserServices()}; - services.AddInstance>(new OptionsAccessor(null)); + var services = new ServiceCollection {IdentityServices.GetDefaultServices()}; + services.Add(OptionsServices.GetDefaultServices()); services.AddTransient, NoopUserStore>(); services.AddTransient(); var manager = services.BuildServiceProvider().GetService(); @@ -464,10 +464,11 @@ namespace Microsoft.AspNet.Identity.Test public async Task TokenMethodsThrowWithNoTokenProvider() { var manager = MockHelpers.TestUserManager(new NoopUserStore()); + var user = new TestUser(); await Assert.ThrowsAsync( - async () => await manager.GenerateUserTokenAsync(null, null)); + async () => await manager.GenerateUserTokenAsync(user, "bogus", null)); await Assert.ThrowsAsync( - async () => await manager.VerifyUserTokenAsync(null, null, null)); + async () => await manager.VerifyUserTokenAsync(user, "bogus", null, null)); } [Fact] @@ -581,13 +582,13 @@ namespace Microsoft.AspNet.Identity.Test var passwordValidator = new PasswordValidator(); Assert.Throws("store", - () => new UserManager(null, null, null, null, null, null)); + () => new UserManager(null, null, null, null, null, null, null)); Assert.Throws("optionsAccessor", - () => new UserManager(store, null, null, null, null, null)); + () => new UserManager(store, null, null, null, null, null, null)); Assert.Throws("passwordHasher", - () => new UserManager(store, optionsAccessor, null, null, null, null)); + () => new UserManager(store, optionsAccessor, null, null, null, null, null)); - var manager = new UserManager(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null); + var manager = new UserManager(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null); Assert.Throws("value", () => manager.PasswordHasher = null); Assert.Throws("value", () => manager.Options = null); @@ -608,9 +609,7 @@ namespace Microsoft.AspNet.Identity.Test await Assert.ThrowsAsync("providerKey", async () => await manager.RemoveLoginAsync(null, "", null)); await Assert.ThrowsAsync("email", async () => await manager.FindByEmailAsync(null)); - Assert.Throws("twoFactorProvider", - () => manager.RegisterTwoFactorProvider(null, null)); - Assert.Throws("provider", () => manager.RegisterTwoFactorProvider("bogus", null)); + Assert.Throws("provider", () => manager.RegisterTokenProvider(null)); await Assert.ThrowsAsync("roles", async () => await manager.AddToRolesAsync(new TestUser(), null)); await Assert.ThrowsAsync("roles", async () => await manager.RemoveFromRolesAsync(new TestUser(), null)); } @@ -619,7 +618,7 @@ namespace Microsoft.AspNet.Identity.Test public async Task MethodsFailWithUnknownUserTest() { var manager = MockHelpers.TestUserManager(new EmptyStore()); - manager.UserTokenProvider = new NoOpTokenProvider(); + manager.RegisterTokenProvider(new NoOpTokenProvider()); await Assert.ThrowsAsync("user", async () => await manager.GetUserNameAsync(null)); await Assert.ThrowsAsync("user", @@ -697,7 +696,7 @@ namespace Microsoft.AspNet.Identity.Test await Assert.ThrowsAsync("user", async () => await manager.GetValidTwoFactorProvidersAsync(null)); await Assert.ThrowsAsync("user", - async () => await manager.VerifyUserTokenAsync(null, null, null)); + async () => await manager.VerifyUserTokenAsync(null, null, null, null)); await Assert.ThrowsAsync("user", async () => await manager.AccessFailedAsync(null)); await Assert.ThrowsAsync("user", @@ -763,7 +762,7 @@ namespace Microsoft.AspNet.Identity.Test { public const string ErrorMessage = "I'm Bad."; - public Task ValidateAsync(string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)) + public Task ValidateAsync(TUser user, string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(IdentityResult.Failed(ErrorMessage)); } @@ -1007,6 +1006,8 @@ namespace Microsoft.AspNet.Identity.Test private class NoOpTokenProvider : IUserTokenProvider { + public string Name { get; } = "Noop"; + public Task GenerateAsync(string purpose, UserManager manager, TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult("Test"); @@ -1023,7 +1024,7 @@ namespace Microsoft.AspNet.Identity.Test return Task.FromResult(0); } - public Task IsValidProviderForUserAsync(UserManager manager, TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(true); } diff --git a/test/Shared/MockHelpers.cs b/test/Shared/MockHelpers.cs index a09a87cc75..c7b09dcc50 100644 --- a/test/Shared/MockHelpers.cs +++ b/test/Shared/MockHelpers.cs @@ -13,11 +13,11 @@ namespace Microsoft.AspNet.Identity.Test { public static class MockHelpers { - public static UserManager CreateManager(Func> storeFunc) where TUser : class + public static UserManager CreateManager(IUserStore store) where TUser : class { var services = new ServiceCollection(); services.Add(OptionsServices.GetDefaultServices()); - services.AddIdentity().AddUserStore(storeFunc); + services.AddIdentity().AddUserStore(store); services.SetupOptions(options => { options.Password.RequireDigit = false; @@ -39,7 +39,8 @@ namespace Microsoft.AspNet.Identity.Test new PasswordHasher(), new UserValidator(), new PasswordValidator(), - new UpperInvariantUserNameNormalizer()); + new UpperInvariantUserNameNormalizer(), + null); } public static Mock> MockRoleManager() where TRole : class @@ -58,7 +59,7 @@ namespace Microsoft.AspNet.Identity.Test var options = new OptionsAccessor(null); var validator = new Mock>(); var userManager = new UserManager(store, options, new PasswordHasher(), - validator.Object, new PasswordValidator(), new UpperInvariantUserNameNormalizer()); + validator.Object, new PasswordValidator(), new UpperInvariantUserNameNormalizer(), null); validator.Setup(v => v.ValidateAsync(userManager, It.IsAny(), CancellationToken.None)) .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return userManager; diff --git a/test/Shared/UserManagerTestBase.cs b/test/Shared/UserManagerTestBase.cs index eabab9931c..ab2b45a68e 100644 --- a/test/Shared/UserManagerTestBase.cs +++ b/test/Shared/UserManagerTestBase.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Testing; using Xunit; +using Microsoft.AspNet.Security.DataProtection; namespace Microsoft.AspNet.Identity.Test { @@ -502,6 +503,8 @@ namespace Microsoft.AspNet.Identity.Test private class StaticTokenProvider : IUserTokenProvider { + public string Name { get; } = "Static"; + public Task GenerateAsync(string purpose, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { @@ -519,7 +522,7 @@ namespace Microsoft.AspNet.Identity.Test return Task.FromResult(0); } - public Task IsValidProviderForUserAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(true); } @@ -534,7 +537,8 @@ namespace Microsoft.AspNet.Identity.Test public async Task CanResetPasswordWithStaticTokenProvider() { var manager = CreateManager(); - manager.UserTokenProvider = new StaticTokenProvider(); + manager.RegisterTokenProvider(new StaticTokenProvider()); + manager.Options.PasswordResetTokenProvider = "Static"; var user = CreateTestUser(); const string password = "password"; const string newPassword = "newpassword"; @@ -553,7 +557,8 @@ namespace Microsoft.AspNet.Identity.Test public async Task PasswordValidatorCanBlockResetPasswordWithStaticTokenProvider() { var manager = CreateManager(); - manager.UserTokenProvider = new StaticTokenProvider(); + manager.RegisterTokenProvider(new StaticTokenProvider()); + manager.Options.PasswordResetTokenProvider = "Static"; var user = CreateTestUser(); const string password = "password"; const string newPassword = "newpassword"; @@ -574,7 +579,8 @@ namespace Microsoft.AspNet.Identity.Test public async Task ResetPasswordWithStaticTokenProviderFailsWithWrongToken() { var manager = CreateManager(); - manager.UserTokenProvider = new StaticTokenProvider(); + manager.RegisterTokenProvider(new StaticTokenProvider()); + manager.Options.PasswordResetTokenProvider = "Static"; var user = CreateTestUser(); const string password = "password"; const string newPassword = "newpassword"; @@ -591,23 +597,24 @@ namespace Microsoft.AspNet.Identity.Test public async Task CanGenerateAndVerifyUserTokenWithStaticTokenProvider() { var manager = CreateManager(); - manager.UserTokenProvider = new StaticTokenProvider(); + manager.RegisterTokenProvider(new StaticTokenProvider()); var user = CreateTestUser(); var user2 = CreateTestUser(); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); - var token = await manager.GenerateUserTokenAsync("test", user); - Assert.True(await manager.VerifyUserTokenAsync(user, "test", token)); - Assert.False(await manager.VerifyUserTokenAsync(user, "test2", token)); - Assert.False(await manager.VerifyUserTokenAsync(user, "test", token + "a")); - Assert.False(await manager.VerifyUserTokenAsync(user2, "test", token)); + var token = await manager.GenerateUserTokenAsync(user, "Static", "test"); + Assert.True(await manager.VerifyUserTokenAsync(user, "Static", "test", token)); + Assert.False(await manager.VerifyUserTokenAsync(user, "Static", "test2", token)); + Assert.False(await manager.VerifyUserTokenAsync(user, "Static", "test", token + "a")); + Assert.False(await manager.VerifyUserTokenAsync(user2, "Static", "test", token)); } [Fact] public async Task CanConfirmEmailWithStaticToken() { var manager = CreateManager(); - manager.UserTokenProvider = new StaticTokenProvider(); + manager.RegisterTokenProvider(new StaticTokenProvider()); + manager.Options.EmailConfirmationTokenProvider = "Static"; var user = CreateTestUser(); Assert.False(user.EmailConfirmed); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); @@ -623,7 +630,8 @@ namespace Microsoft.AspNet.Identity.Test public async Task ConfirmEmailWithStaticTokenFailsWithWrongToken() { var manager = CreateManager(); - manager.UserTokenProvider = new StaticTokenProvider(); + manager.RegisterTokenProvider(new StaticTokenProvider()); + manager.Options.EmailConfirmationTokenProvider = "Static"; var user = CreateTestUser(); Assert.False(user.EmailConfirmed); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); @@ -631,20 +639,22 @@ namespace Microsoft.AspNet.Identity.Test Assert.False(await manager.IsEmailConfirmedAsync(user)); } - // TODO: Can't reenable til we have a SecurityStamp linked token provider - //[Fact] - //public async Task ConfirmTokenFailsAfterPasswordChange() - //{ - // var manager = CreateManager(); - // var user = new TUser() { UserName = "test" }; - // Assert.False(user.EmailConfirmed); - // IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); - // var token = await manager.GenerateEmailConfirmationTokenAsync(user); - // Assert.NotNull(token); - // IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, "password", "newpassword")); - // IdentityResultAssert.IsFailure(await manager.ConfirmEmailAsync(user, token), "Invalid token."); - // Assert.False(await manager.IsEmailConfirmedAsync(user)); - //} + [Fact] + public async Task ConfirmTokenFailsAfterPasswordChange() + { + var manager = CreateManager(); + manager.RegisterTokenProvider(new DataProtectorTokenProvider(new DataProtectionTokenProviderOptions(), + DataProtectionProvider.CreateFromDpapi().CreateProtector("ASP.NET Identity"))); + manager.Options.EmailConfirmationTokenProvider = "DataProtection"; + var user = new TUser() { UserName = "test" }; + Assert.False(user.EmailConfirmed); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); + var token = await manager.GenerateEmailConfirmationTokenAsync(user); + Assert.NotNull(token); + IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, "password", "newpassword")); + IdentityResultAssert.IsFailure(await manager.ConfirmEmailAsync(user, token), "Invalid token."); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + } // Lockout tests @@ -837,7 +847,7 @@ namespace Microsoft.AspNet.Identity.Test { public const string ErrorMessage = "I'm Bad."; - public Task ValidateAsync(string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)) + public Task ValidateAsync(TUser user, string password, UserManager manager, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(IdentityResult.Failed(ErrorMessage)); } @@ -1220,73 +1230,16 @@ namespace Microsoft.AspNet.Identity.Test Assert.False(await manager.VerifyChangePhoneNumberTokenAsync(user, token1, num2)); } - private class EmailTokenProvider : IUserTokenProvider - { - public Task GenerateAsync(string purpose, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return Task.FromResult(MakeToken(purpose)); - } - - public Task ValidateAsync(string purpose, string token, UserManager manager, - TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return Task.FromResult(token == MakeToken(purpose)); - } - - public Task NotifyAsync(string token, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return manager.SendEmailAsync(user, token, token); - } - - public async Task IsValidProviderForUserAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return !string.IsNullOrEmpty(await manager.GetEmailAsync(user)); - } - - private static string MakeToken(string purpose) - { - return "email:" + purpose; - } - } - - private class SmsTokenProvider : IUserTokenProvider - { - public Task GenerateAsync(string purpose, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return Task.FromResult(MakeToken(purpose)); - } - - public Task ValidateAsync(string purpose, string token, UserManager manager, - TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return Task.FromResult(token == MakeToken(purpose)); - } - - public Task NotifyAsync(string token, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return manager.SendSmsAsync(user, token, cancellationToken); - } - - public async Task IsValidProviderForUserAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - return !string.IsNullOrEmpty(await manager.GetPhoneNumberAsync(user, cancellationToken)); - } - - private static string MakeToken(string purpose) - { - return "sms:" + purpose; - } - } - [Fact] public async Task CanEmailTwoFactorToken() { var manager = CreateManager(); var messageService = new TestMessageService(); manager.EmailService = messageService; - const string factorId = "EmailCode"; - manager.RegisterTwoFactorProvider(factorId, new EmailTokenProvider()); + const string factorId = "Email"; + manager.RegisterTokenProvider(new EmailTokenProvider()); var user = new TUser() { UserName = "EmailCodeTest", Email = "foo@foo.com" }; + user.EmailConfirmed = true; const string password = "password"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); var stamp = user.SecurityStamp; @@ -1296,8 +1249,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Null(messageService.Message); IdentityResultAssert.IsSuccess(await manager.NotifyTwoFactorTokenAsync(user, factorId, token)); Assert.NotNull(messageService.Message); - Assert.Equal(token, messageService.Message.Subject); - Assert.Equal(token, messageService.Message.Body); + Assert.Equal("Your security code is: "+token, messageService.Message.Body); Assert.True(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); } @@ -1310,7 +1262,7 @@ namespace Microsoft.AspNet.Identity.Test await ExceptionAssert.ThrowsAsync( async () => await manager.NotifyTwoFactorTokenAsync(user, "Bogus", "token"), - "No IUserTwoFactorProvider for 'Bogus' is registered."); + "No IUserTokenProvider named 'Bogus' is registered."); } [Fact] @@ -1319,12 +1271,15 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var messageService = new TestMessageService(); manager.EmailService = messageService; - const string factorId = "EmailCode"; - manager.RegisterTwoFactorProvider(factorId, new EmailTokenProvider + const string factorId = "EmailTestCode"; + const string subject = "Custom subject"; + const string body = "Your code is {0}!"; + manager.RegisterTokenProvider(new EmailTokenProvider(new EmailTokenProviderOptions { - Subject = "Security Code", - BodyFormat = "Your code is: {0}" - }); + Name = factorId, + Subject = subject, + BodyFormat = body + })); var user = CreateTestUser(); user.Email = user.UserName + "@foo.com"; const string password = "password"; @@ -1336,8 +1291,8 @@ namespace Microsoft.AspNet.Identity.Test Assert.Null(messageService.Message); IdentityResultAssert.IsSuccess(await manager.NotifyTwoFactorTokenAsync(user, factorId, token)); Assert.NotNull(messageService.Message); - Assert.Equal("Security Code", messageService.Message.Subject); - Assert.Equal("Your code is: " + token, messageService.Message.Body); + Assert.Equal(subject, messageService.Message.Subject); + Assert.Equal(string.Format(body, token), messageService.Message.Body); Assert.True(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); } @@ -1345,10 +1300,11 @@ namespace Microsoft.AspNet.Identity.Test public async Task EmailFactorFailsAfterSecurityStampChangeTest() { var manager = CreateManager(); - const string factorId = "EmailCode"; - manager.RegisterTwoFactorProvider(factorId, new EmailTokenProvider()); + string factorId = "Email"; //default + manager.RegisterTokenProvider(new EmailTokenProvider()); var user = CreateTestUser(); user.Email = user.UserName + "@foo.com"; + user.EmailConfirmed = true; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); var stamp = user.SecurityStamp; Assert.NotNull(stamp); @@ -1407,8 +1363,8 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var messageService = new TestMessageService(); manager.SmsService = messageService; - const string factorId = "PhoneCode"; - manager.RegisterTwoFactorProvider(factorId, new SmsTokenProvider()); + const string factorId = "Phone"; // default + manager.RegisterTokenProvider(new PhoneNumberTokenProvider()); var user = CreateTestUser(); user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); @@ -1419,7 +1375,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Null(messageService.Message); IdentityResultAssert.IsSuccess(await manager.NotifyTwoFactorTokenAsync(user, factorId, token)); Assert.NotNull(messageService.Message); - Assert.Equal(token, messageService.Message.Body); + Assert.Equal("Your security code is: "+token, messageService.Message.Body); Assert.True(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); } @@ -1429,11 +1385,12 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var messageService = new TestMessageService(); manager.SmsService = messageService; - const string factorId = "PhoneCode"; - manager.RegisterTwoFactorProvider(factorId, new PhoneNumberTokenProvider + const string factorId = "PhoneTestFactors"; + manager.RegisterTokenProvider(new PhoneNumberTokenProvider(new PhoneNumberTokenProviderOptions { + Name = factorId, MessageFormat = "Your code is: {0}" - }); + })); var user = CreateTestUser(); user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); @@ -1454,7 +1411,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var user = CreateTestUser(); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - const string error = "No IUserTwoFactorProvider for 'bogus' is registered."; + const string error = "No IUserTokenProvider named 'bogus' is registered."; await ExceptionAssert.ThrowsAsync( () => manager.GenerateTwoFactorTokenAsync(user, "bogus"), error); @@ -1477,35 +1434,39 @@ namespace Microsoft.AspNet.Identity.Test public async Task CanGetValidTwoFactor() { var manager = CreateManager(); - manager.RegisterTwoFactorProvider("phone", new SmsTokenProvider()); - manager.RegisterTwoFactorProvider("email", new EmailTokenProvider()); + manager.RegisterTokenProvider(new PhoneNumberTokenProvider()); + manager.RegisterTokenProvider(new EmailTokenProvider()); var user = CreateTestUser(); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); var factors = await manager.GetValidTwoFactorProvidersAsync(user); Assert.NotNull(factors); - Assert.True(!factors.Any()); + Assert.False(factors.Any()); IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "111-111-1111")); + user.PhoneNumberConfirmed = true; + await manager.UpdateAsync(user); factors = await manager.GetValidTwoFactorProvidersAsync(user); Assert.NotNull(factors); - Assert.True(factors.Count() == 1); - Assert.Equal("phone", factors[0]); + Assert.Equal(1, factors.Count()); + Assert.Equal("Phone", factors[0]); IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, "test@test.com")); + user.EmailConfirmed = true; + await manager.UpdateAsync(user); factors = await manager.GetValidTwoFactorProvidersAsync(user); Assert.NotNull(factors); - Assert.True(factors.Count() == 2); + Assert.Equal(2, factors.Count()); IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, null)); factors = await manager.GetValidTwoFactorProvidersAsync(user); Assert.NotNull(factors); - Assert.True(factors.Count() == 1); - Assert.Equal("phone", factors[0]); + Assert.Equal(1, factors.Count()); + Assert.Equal("Phone", factors[0]); } [Fact] public async Task PhoneFactorFailsAfterSecurityStampChangeTest() { var manager = CreateManager(); - var factorId = "PhoneCode"; - manager.RegisterTwoFactorProvider(factorId, new PhoneNumberTokenProvider()); + var factorId = "Phone"; // default + manager.RegisterTokenProvider(new PhoneNumberTokenProvider()); var user = CreateTestUser(); user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); @@ -1521,26 +1482,25 @@ namespace Microsoft.AspNet.Identity.Test public async Task VerifyTokenFromWrongTokenProviderFails() { var manager = CreateManager(); - manager.RegisterTwoFactorProvider("PhoneCode", new SmsTokenProvider()); - manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider()); + manager.RegisterTokenProvider(new PhoneNumberTokenProvider()); + manager.RegisterTokenProvider(new EmailTokenProvider()); var user = CreateTestUser(); user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - var token = await manager.GenerateTwoFactorTokenAsync(user, "PhoneCode"); + var token = await manager.GenerateTwoFactorTokenAsync(user, "Phone"); Assert.NotNull(token); - Assert.False(await manager.VerifyTwoFactorTokenAsync(user, "EmailCode", token)); + Assert.False(await manager.VerifyTwoFactorTokenAsync(user, "Email", token)); } [Fact] public async Task VerifyWithWrongSmsTokenFails() { var manager = CreateManager(); - const string factorId = "PhoneCode"; - manager.RegisterTwoFactorProvider(factorId, new SmsTokenProvider()); + manager.RegisterTokenProvider(new PhoneNumberTokenProvider()); var user = CreateTestUser(); user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - Assert.False(await manager.VerifyTwoFactorTokenAsync(user, factorId, "bogus")); + Assert.False(await manager.VerifyTwoFactorTokenAsync(user, "Phone", "bogus")); } public List GenerateUsers(string userNamePrefix, int count)