From 87ff50a1864361e80871f91db3ec3dc4f92ece73 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Tue, 3 May 2022 16:44:39 -0400 Subject: [PATCH 1/3] Add owin cookie auth-based signin/signout endpoints to ASP.NET app --- samples/MvcApp/App_Data/dummy.txt | 2 + samples/MvcApp/App_Start/IdentityConfig.cs | 108 ++++ samples/MvcApp/App_Start/Startup.Auth.cs | 67 +++ .../MvcApp/Controllers/AccountController.cs | 482 ++++++++++++++++++ samples/MvcApp/Models/AccountViewModels.cs | 112 ++++ samples/MvcApp/Models/IdentityModels.cs | 32 ++ samples/MvcApp/MvcApp.csproj | 54 +- samples/MvcApp/Startup.cs | 14 + .../MvcApp/Views/Account/ConfirmEmail.cshtml | 10 + .../Account/ExternalLoginConfirmation.cshtml | 36 ++ .../Views/Account/ExternalLoginFailure.cshtml | 8 + .../Views/Account/ForgotPassword.cshtml | 29 ++ .../Account/ForgotPasswordConfirmation.cshtml | 13 + samples/MvcApp/Views/Account/Login.cshtml | 63 +++ samples/MvcApp/Views/Account/Register.cshtml | 41 ++ .../MvcApp/Views/Account/ResetPassword.cshtml | 42 ++ .../Account/ResetPasswordConfirmation.cshtml | 12 + samples/MvcApp/Views/Account/SendCode.cshtml | 24 + .../MvcApp/Views/Account/VerifyCode.cshtml | 38 ++ .../Account/_ExternalLoginsListPartial.cshtml | 28 + samples/MvcApp/Views/Shared/Lockout.cshtml | 11 + samples/MvcApp/Views/Shared/_Layout.cshtml | 1 + .../MvcApp/Views/Shared/_LoginPartial.cshtml | 22 + samples/MvcApp/Web.config | 85 +-- samples/MvcApp/packages.config | 10 + ...ystemWebAdapters.SessionState.Tests.csproj | 7 +- ....AspNetCore.SystemWebAdapters.Tests.csproj | 7 +- 27 files changed, 1317 insertions(+), 41 deletions(-) create mode 100644 samples/MvcApp/App_Data/dummy.txt create mode 100644 samples/MvcApp/App_Start/IdentityConfig.cs create mode 100644 samples/MvcApp/App_Start/Startup.Auth.cs create mode 100644 samples/MvcApp/Controllers/AccountController.cs create mode 100644 samples/MvcApp/Models/AccountViewModels.cs create mode 100644 samples/MvcApp/Models/IdentityModels.cs create mode 100644 samples/MvcApp/Startup.cs create mode 100644 samples/MvcApp/Views/Account/ConfirmEmail.cshtml create mode 100644 samples/MvcApp/Views/Account/ExternalLoginConfirmation.cshtml create mode 100644 samples/MvcApp/Views/Account/ExternalLoginFailure.cshtml create mode 100644 samples/MvcApp/Views/Account/ForgotPassword.cshtml create mode 100644 samples/MvcApp/Views/Account/ForgotPasswordConfirmation.cshtml create mode 100644 samples/MvcApp/Views/Account/Login.cshtml create mode 100644 samples/MvcApp/Views/Account/Register.cshtml create mode 100644 samples/MvcApp/Views/Account/ResetPassword.cshtml create mode 100644 samples/MvcApp/Views/Account/ResetPasswordConfirmation.cshtml create mode 100644 samples/MvcApp/Views/Account/SendCode.cshtml create mode 100644 samples/MvcApp/Views/Account/VerifyCode.cshtml create mode 100644 samples/MvcApp/Views/Account/_ExternalLoginsListPartial.cshtml create mode 100644 samples/MvcApp/Views/Shared/Lockout.cshtml create mode 100644 samples/MvcApp/Views/Shared/_LoginPartial.cshtml diff --git a/samples/MvcApp/App_Data/dummy.txt b/samples/MvcApp/App_Data/dummy.txt new file mode 100644 index 0000000000..3afe4b8e92 --- /dev/null +++ b/samples/MvcApp/App_Data/dummy.txt @@ -0,0 +1,2 @@ +Placeholder file to make sure the App_Data folder exists (so that +a SQL mdf file can be created there when running locally). \ No newline at end of file diff --git a/samples/MvcApp/App_Start/IdentityConfig.cs b/samples/MvcApp/App_Start/IdentityConfig.cs new file mode 100644 index 0000000000..3f132d5a09 --- /dev/null +++ b/samples/MvcApp/App_Start/IdentityConfig.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using MvcApp.Models; + +namespace MvcApp +{ + public class EmailService : IIdentityMessageService + { + public Task SendAsync(IdentityMessage message) + { + // Plug in your email service here to send an email. + return Task.FromResult(0); + } + } + + public class SmsService : IIdentityMessageService + { + public Task SendAsync(IdentityMessage message) + { + // Plug in your SMS service here to send a text message. + return Task.FromResult(0); + } + } + + // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application. + public class ApplicationUserManager : UserManager + { + public ApplicationUserManager(IUserStore store) + : base(store) + { + } + + public static ApplicationUserManager Create(IdentityFactoryOptions options, IOwinContext context) + { + var manager = new ApplicationUserManager(new UserStore(context.Get())); + // Configure validation logic for usernames + manager.UserValidator = new UserValidator(manager) + { + AllowOnlyAlphanumericUserNames = false, + RequireUniqueEmail = true + }; + + // Configure validation logic for passwords + manager.PasswordValidator = new PasswordValidator + { + RequiredLength = 6, + RequireNonLetterOrDigit = true, + RequireDigit = true, + RequireLowercase = true, + RequireUppercase = true, + }; + + // Configure user lockout defaults + manager.UserLockoutEnabledByDefault = true; + manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); + manager.MaxFailedAccessAttemptsBeforeLockout = 5; + + // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user + // You can write your own provider and plug it in here. + manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider + { + MessageFormat = "Your security code is {0}" + }); + manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider + { + Subject = "Security Code", + BodyFormat = "Your security code is {0}" + }); + manager.EmailService = new EmailService(); + manager.SmsService = new SmsService(); + var dataProtectionProvider = options.DataProtectionProvider; + if (dataProtectionProvider != null) + { + manager.UserTokenProvider = + new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); + } + return manager; + } + } + + // Configure the application sign-in manager which is used in this application. + public class ApplicationSignInManager : SignInManager + { + public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) + : base(userManager, authenticationManager) + { + } + + public override Task CreateUserIdentityAsync(ApplicationUser user) + { + return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager); + } + + public static ApplicationSignInManager Create(IdentityFactoryOptions options, IOwinContext context) + { + return new ApplicationSignInManager(context.GetUserManager(), context.Authentication); + } + } +} diff --git a/samples/MvcApp/App_Start/Startup.Auth.cs b/samples/MvcApp/App_Start/Startup.Auth.cs new file mode 100644 index 0000000000..3328fa82a9 --- /dev/null +++ b/samples/MvcApp/App_Start/Startup.Auth.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using MvcApp.Models; +using Owin; + +namespace MvcApp +{ + public partial class Startup + { + // For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864 + public void ConfigureAuth(IAppBuilder app) + { + // Configure the db context, user manager and signin manager to use a single instance per request + app.CreatePerOwinContext(ApplicationDbContext.Create); + app.CreatePerOwinContext(ApplicationUserManager.Create); + app.CreatePerOwinContext(ApplicationSignInManager.Create); + + // Enable the application to use a cookie to store information for the signed in user + // and to use a cookie to temporarily store information about a user logging in with a third party login provider + // Configure the sign in cookie + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, + LoginPath = new PathString("/Account/Login"), + Provider = new CookieAuthenticationProvider + { + // Enables the application to validate the security stamp when the user logs in. + // This is a security feature which is used when you change a password or add an external login to your account. + OnValidateIdentity = SecurityStampValidator.OnValidateIdentity( + validateInterval: TimeSpan.FromMinutes(30), + regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) + } + }); + app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); + + // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process. + app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); + + // Enables the application to remember the second login verification factor such as phone or email. + // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from. + // This is similar to the RememberMe option when you log in. + app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); + + // Uncomment the following lines to enable logging in with third party login providers + //app.UseMicrosoftAccountAuthentication( + // clientId: "", + // clientSecret: ""); + + //app.UseTwitterAuthentication( + // consumerKey: "", + // consumerSecret: ""); + + //app.UseFacebookAuthentication( + // appId: "", + // appSecret: ""); + + //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() + //{ + // ClientId = "", + // ClientSecret = "" + //}); + } + } +} diff --git a/samples/MvcApp/Controllers/AccountController.cs b/samples/MvcApp/Controllers/AccountController.cs new file mode 100644 index 0000000000..c453acfa5e --- /dev/null +++ b/samples/MvcApp/Controllers/AccountController.cs @@ -0,0 +1,482 @@ +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin.Security; +using MvcApp.Models; + +namespace MvcApp.Controllers +{ + [Authorize] + public class AccountController : Controller + { + private ApplicationSignInManager _signInManager; + private ApplicationUserManager _userManager; + + public AccountController() + { + } + + public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager) + { + UserManager = userManager; + SignInManager = signInManager; + } + + public ApplicationSignInManager SignInManager + { + get + { + return _signInManager ?? HttpContext.GetOwinContext().Get(); + } + private set + { + _signInManager = value; + } + } + + public ApplicationUserManager UserManager + { + get + { + return _userManager ?? HttpContext.GetOwinContext().GetUserManager(); + } + private set + { + _userManager = value; + } + } + + // + // GET: /Account/Login + [AllowAnonymous] + public ActionResult Login(string returnUrl) + { + ViewBag.ReturnUrl = returnUrl; + return View(); + } + + // + // POST: /Account/Login + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model, string returnUrl) + { + if (!ModelState.IsValid) + { + return View(model); + } + + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, change to shouldLockout: true + var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false); + switch (result) + { + case SignInStatus.Success: + return RedirectToLocal(returnUrl); + case SignInStatus.LockedOut: + return View("Lockout"); + case SignInStatus.RequiresVerification: + return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, model.RememberMe }); + case SignInStatus.Failure: + default: + ModelState.AddModelError("", "Invalid login attempt."); + return View(model); + } + } + + // + // GET: /Account/VerifyCode + [AllowAnonymous] + public async Task VerifyCode(string provider, string returnUrl, bool rememberMe) + { + // Require that the user has already logged in via username/password or external login + if (!await SignInManager.HasBeenVerifiedAsync()) + { + return View("Error"); + } + 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); + } + + // The following code protects for brute force attacks against the two factor codes. + // If a user enters incorrect codes for a specified amount of time then the user account + // will be locked out for a specified amount of time. + // You can configure the account lockout settings in IdentityConfig + var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: model.RememberMe, rememberBrowser: model.RememberBrowser); + switch (result) + { + case SignInStatus.Success: + return RedirectToLocal(model.ReturnUrl); + case SignInStatus.LockedOut: + return View("Lockout"); + case SignInStatus.Failure: + default: + ModelState.AddModelError("", "Invalid code."); + return View(model); + } + } + + // + // GET: /Account/Register + [AllowAnonymous] + public ActionResult Register() + { + return View(); + } + + // + // POST: /Account/Register + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model) + { + if (ModelState.IsValid) + { + 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, rememberBrowser: false); + + // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771 + // Send an email with this link + // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); + // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); + // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here"); + + return RedirectToAction("Index", "Home"); + } + AddErrors(result); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/ConfirmEmail + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return View("Error"); + } + var result = await UserManager.ConfirmEmailAsync(userId, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + // + // GET: /Account/ForgotPassword + [AllowAnonymous] + public ActionResult 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.Id))) + { + // Don't reveal that the user does not exist or is not confirmed + return View("ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771 + // Send an email with this link + // string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); + // var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); + // await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking here"); + // return RedirectToAction("ForgotPasswordConfirmation", "Account"); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/ForgotPasswordConfirmation + [AllowAnonymous] + public ActionResult ForgotPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/ResetPassword + [AllowAnonymous] + public ActionResult ResetPassword(string code) + { + 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.Id, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction("ResetPasswordConfirmation", "Account"); + } + AddErrors(result); + return View(); + } + + // + // GET: /Account/ResetPasswordConfirmation + [AllowAnonymous] + public ActionResult ResetPasswordConfirmation() + { + return View(); + } + + // + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public ActionResult ExternalLogin(string provider, string returnUrl) + { + // Request a redirect to the external login provider + return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); + } + + // + // GET: /Account/SendCode + [AllowAnonymous] + public async Task SendCode(string returnUrl, bool rememberMe) + { + var userId = await SignInManager.GetVerifiedUserIdAsync(); + if (userId == null) + { + return View("Error"); + } + var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId); + 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, model.ReturnUrl, model.RememberMe }); + } + + // + // GET: /Account/ExternalLoginCallback + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); + if (loginInfo == null) + { + return RedirectToAction("Login"); + } + + // Sign in the user with this external login provider if the user already has a login + var result = await SignInManager.ExternalSignInAsync(loginInfo, 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, RememberMe = false }); + 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 = loginInfo.Login.LoginProvider; + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email }); + } + } + + // + // POST: /Account/ExternalLoginConfirmation + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl) + { + 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 AuthenticationManager.GetExternalLoginInfoAsync(); + 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.Id, info.Login); + if (result.Succeeded) + { + await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } + + ViewBag.ReturnUrl = returnUrl; + return View(model); + } + + // + // POST: /Account/LogOff + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult LogOff() + { + AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); + return RedirectToAction("Index", "Home"); + } + + // + // GET: /Account/ExternalLoginFailure + [AllowAnonymous] + public ActionResult ExternalLoginFailure() + { + return View(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_userManager != null) + { + _userManager.Dispose(); + _userManager = null; + } + + if (_signInManager != null) + { + _signInManager.Dispose(); + _signInManager = null; + } + } + + base.Dispose(disposing); + } + + #region Helpers + // Used for XSRF protection when adding external logins + private const string XsrfKey = "XsrfId"; + + private IAuthenticationManager AuthenticationManager + { + get + { + return HttpContext.GetOwinContext().Authentication; + } + } + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError("", error); + } + } + + private ActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + return RedirectToAction("Index", "Home"); + } + + internal class ChallengeResult : HttpUnauthorizedResult + { + public ChallengeResult(string provider, string redirectUri) + : this(provider, redirectUri, null) + { + } + + public ChallengeResult(string provider, string redirectUri, string userId) + { + LoginProvider = provider; + RedirectUri = redirectUri; + UserId = userId; + } + + public string LoginProvider { get; set; } + public string RedirectUri { get; set; } + public string UserId { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; + if (UserId != null) + { + properties.Dictionary[XsrfKey] = UserId; + } + context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); + } + } + #endregion + } +} diff --git a/samples/MvcApp/Models/AccountViewModels.cs b/samples/MvcApp/Models/AccountViewModels.cs new file mode 100644 index 0000000000..a010ee3351 --- /dev/null +++ b/samples/MvcApp/Models/AccountViewModels.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MvcApp.Models +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [Display(Name = "Email")] + public string Email { get; set; } + } + + public class ExternalLoginListViewModel + { + public string ReturnUrl { get; set; } + } + + 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] + 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 ForgotViewModel + { + [Required] + [Display(Name = "Email")] + public string Email { get; set; } + } + + public class LoginViewModel + { + [Required] + [Display(Name = "Email")] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterViewModel + { + [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 = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { 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 = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [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] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + } +} diff --git a/samples/MvcApp/Models/IdentityModels.cs b/samples/MvcApp/Models/IdentityModels.cs new file mode 100644 index 0000000000..76e8b90983 --- /dev/null +++ b/samples/MvcApp/Models/IdentityModels.cs @@ -0,0 +1,32 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.EntityFramework; + +namespace MvcApp.Models +{ + // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit https://go.microsoft.com/fwlink/?LinkID=317594 to learn more. + public class ApplicationUser : IdentityUser + { + public async Task GenerateUserIdentityAsync(UserManager manager) + { + // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); + // Add custom user claims here + return userIdentity; + } + } + + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext() + : base("DefaultConnection", throwIfV1Schema: false) + { + } + + public static ApplicationDbContext Create() + { + return new ApplicationDbContext(); + } + } +} diff --git a/samples/MvcApp/MvcApp.csproj b/samples/MvcApp/MvcApp.csproj index a2447f20a7..575ef106ff 100644 --- a/samples/MvcApp/MvcApp.csproj +++ b/samples/MvcApp/MvcApp.csproj @@ -47,10 +47,43 @@ 4 + + ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + + + ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll + + + ..\..\packages\Microsoft.AspNet.Identity.Core.2.2.3\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\..\packages\Microsoft.AspNet.Identity.EntityFramework.2.2.3\lib\net45\Microsoft.AspNet.Identity.EntityFramework.dll + + + ..\..\packages\Microsoft.AspNet.Identity.Owin.2.2.3\lib\net45\Microsoft.AspNet.Identity.Owin.dll + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + ..\..\packages\Microsoft.Owin.4.2.1\lib\net45\Microsoft.Owin.dll + + + ..\..\packages\Microsoft.Owin.Host.SystemWeb.4.2.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + + + ..\..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + ..\..\packages\System.ComponentModel.Annotations.5.0.0\lib\net461\System.ComponentModel.Annotations.dll @@ -133,15 +166,21 @@ + + + Global.asax + + + @@ -174,16 +213,29 @@ + + + + + + + + + + + + + + - diff --git a/samples/MvcApp/Startup.cs b/samples/MvcApp/Startup.cs new file mode 100644 index 0000000000..cbe10c1b54 --- /dev/null +++ b/samples/MvcApp/Startup.cs @@ -0,0 +1,14 @@ +using Microsoft.Owin; +using Owin; + +[assembly: OwinStartup(typeof(MvcApp.Startup))] +namespace MvcApp +{ + public partial class Startup + { + public void Configuration(IAppBuilder app) + { + ConfigureAuth(app); + } + } +} diff --git a/samples/MvcApp/Views/Account/ConfirmEmail.cshtml b/samples/MvcApp/Views/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000000..ed02ad870f --- /dev/null +++ b/samples/MvcApp/Views/Account/ConfirmEmail.cshtml @@ -0,0 +1,10 @@ +@{ + 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/MvcApp/Views/Account/ExternalLoginConfirmation.cshtml b/samples/MvcApp/Views/Account/ExternalLoginConfirmation.cshtml new file mode 100644 index 0000000000..e9e8f5cce4 --- /dev/null +++ b/samples/MvcApp/Views/Account/ExternalLoginConfirmation.cshtml @@ -0,0 +1,36 @@ +@model MvcApp.Models.ExternalLoginConfirmationViewModel +@{ + 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 { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/ExternalLoginFailure.cshtml b/samples/MvcApp/Views/Account/ExternalLoginFailure.cshtml new file mode 100644 index 0000000000..3be4ab37d4 --- /dev/null +++ b/samples/MvcApp/Views/Account/ExternalLoginFailure.cshtml @@ -0,0 +1,8 @@ +@{ + ViewBag.Title = "Login Failure"; +} + +
+

@ViewBag.Title.

+

Unsuccessful login with service.

+
diff --git a/samples/MvcApp/Views/Account/ForgotPassword.cshtml b/samples/MvcApp/Views/Account/ForgotPassword.cshtml new file mode 100644 index 0000000000..c20e2dbeee --- /dev/null +++ b/samples/MvcApp/Views/Account/ForgotPassword.cshtml @@ -0,0 +1,29 @@ +@model MvcApp.Models.ForgotPasswordViewModel +@{ + 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 { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/ForgotPasswordConfirmation.cshtml b/samples/MvcApp/Views/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000000..dd7af8ca7c --- /dev/null +++ b/samples/MvcApp/Views/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,13 @@ +@{ + ViewBag.Title = "Forgot Password Confirmation"; +} + +
+

@ViewBag.Title.

+
+
+

+ Please check your email to reset your password. +

+
+ diff --git a/samples/MvcApp/Views/Account/Login.cshtml b/samples/MvcApp/Views/Account/Login.cshtml new file mode 100644 index 0000000000..e8c06a0097 --- /dev/null +++ b/samples/MvcApp/Views/Account/Login.cshtml @@ -0,0 +1,63 @@ +@using MvcApp.Models +@model LoginViewModel +@{ + ViewBag.Title = "Log in"; +} + +

@ViewBag.Title.

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

Use a local account to log in.

+
+ @Html.ValidationSummary(true, "", 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" }) + @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) +
+
+
+ @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
+ @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) + @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" }) +
+
+
+
+
+ @Html.CheckBoxFor(m => m.RememberMe) + @Html.LabelFor(m => m.RememberMe) +
+
+
+
+
+ +
+
+

+ @Html.ActionLink("Register as a new user", "Register") +

+ @* Enable this once you have account confirmation enabled for password reset functionality +

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

*@ + } +
+
+
+
+ @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) +
+
+
+ +@section Scripts { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/Register.cshtml b/samples/MvcApp/Views/Account/Register.cshtml new file mode 100644 index 0000000000..0a4412427b --- /dev/null +++ b/samples/MvcApp/Views/Account/Register.cshtml @@ -0,0 +1,41 @@ +@model MvcApp.Models.RegisterViewModel +@{ + ViewBag.Title = "Register"; +} + +

@ViewBag.Title.

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

Create a new account.

+
+ @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" }) +
+
+
+ @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 { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/ResetPassword.cshtml b/samples/MvcApp/Views/Account/ResetPassword.cshtml new file mode 100644 index 0000000000..1691f429f4 --- /dev/null +++ b/samples/MvcApp/Views/Account/ResetPassword.cshtml @@ -0,0 +1,42 @@ +@model MvcApp.Models.ResetPasswordViewModel +@{ + 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 { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/ResetPasswordConfirmation.cshtml b/samples/MvcApp/Views/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000000..3804516176 --- /dev/null +++ b/samples/MvcApp/Views/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,12 @@ +@{ + 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/MvcApp/Views/Account/SendCode.cshtml b/samples/MvcApp/Views/Account/SendCode.cshtml new file mode 100644 index 0000000000..6f14e36dc5 --- /dev/null +++ b/samples/MvcApp/Views/Account/SendCode.cshtml @@ -0,0 +1,24 @@ +@model MvcApp.Models.SendCodeViewModel +@{ + ViewBag.Title = "Send"; +} + +

@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) +

Send verification code

+
+
+
+ Select Two-Factor Authentication Provider: + @Html.DropDownListFor(model => model.SelectedProvider, Model.Providers) + +
+
+} + +@section Scripts { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/VerifyCode.cshtml b/samples/MvcApp/Views/Account/VerifyCode.cshtml new file mode 100644 index 0000000000..1eb976d83d --- /dev/null +++ b/samples/MvcApp/Views/Account/VerifyCode.cshtml @@ -0,0 +1,38 @@ +@model MvcApp.Models.VerifyCodeViewModel +@{ + ViewBag.Title = "Verify"; +} + +

@ViewBag.Title.

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

Enter verification code

+
+ @Html.ValidationSummary("", new { @class = "text-danger" }) +
+ @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 { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/samples/MvcApp/Views/Account/_ExternalLoginsListPartial.cshtml b/samples/MvcApp/Views/Account/_ExternalLoginsListPartial.cshtml new file mode 100644 index 0000000000..989e1b8d2b --- /dev/null +++ b/samples/MvcApp/Views/Account/_ExternalLoginsListPartial.cshtml @@ -0,0 +1,28 @@ +@model MvcApp.Models.ExternalLoginListViewModel +@using Microsoft.Owin.Security + +

Use another service to log in.

+
+@{ + var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); + if (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 = Model.ReturnUrl })) { + @Html.AntiForgeryToken() +
+

+ @foreach (AuthenticationDescription p in loginProviders) { + + } +

+
+ } + } +} diff --git a/samples/MvcApp/Views/Shared/Lockout.cshtml b/samples/MvcApp/Views/Shared/Lockout.cshtml new file mode 100644 index 0000000000..1e1d8719ff --- /dev/null +++ b/samples/MvcApp/Views/Shared/Lockout.cshtml @@ -0,0 +1,11 @@ + +@model System.Web.Mvc.HandleErrorInfo + +@{ + ViewBag.Title = "Locked Out"; +} + +
+

Locked out.

+

This account has been locked out, please try again later.

+
diff --git a/samples/MvcApp/Views/Shared/_Layout.cshtml b/samples/MvcApp/Views/Shared/_Layout.cshtml index 7330a4dc77..a127297d6e 100644 --- a/samples/MvcApp/Views/Shared/_Layout.cshtml +++ b/samples/MvcApp/Views/Shared/_Layout.cshtml @@ -25,6 +25,7 @@
  • @Html.ActionLink("Contact", "Contact", "Home")
  • @Html.ActionLink("Privacy", "Privacy", "Home")
  • + @Html.Partial("_LoginPartial") diff --git a/samples/MvcApp/Views/Shared/_LoginPartial.cshtml b/samples/MvcApp/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000000..f76037f1e2 --- /dev/null +++ b/samples/MvcApp/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,22 @@ +@using Microsoft.AspNet.Identity +@if (Request.IsAuthenticated) +{ + using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" })) + { + @Html.AntiForgeryToken() + + + } +} +else +{ + +} diff --git a/samples/MvcApp/Web.config b/samples/MvcApp/Web.config index 13d2ce7497..7a5cf74144 100644 --- a/samples/MvcApp/Web.config +++ b/samples/MvcApp/Web.config @@ -1,14 +1,21 @@ - + + + +
    + + + + - - - - + + + +