using IdentityModel; using IdentityServer4.Services; using IdentityServer4.Stores; using IdentityServer4.Test; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using IdentityServer4.Events; using IdentityServer4.Extensions; using IdentityServer4.Models; using Microsoft.AspNetCore.Identity; using Teknik.IdentityServer.Security; using Teknik.IdentityServer.Services; using Teknik.IdentityServer.ViewModels; using Teknik.IdentityServer.Options; using Teknik.IdentityServer.Models; using Microsoft.Extensions.Logging; using Teknik.Logging; using Teknik.Configuration; using Teknik.Utilities; namespace Teknik.IdentityServer.Controllers { public class AccountController : DefaultController { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IIdentityServerInteractionService _interaction; private readonly IEventService _events; private readonly AccountService _account; public AccountController( ILogger logger, Config config, IIdentityServerInteractionService interaction, IClientStore clientStore, IHttpContextAccessor httpContextAccessor, IAuthenticationSchemeProvider schemeProvider, IEventService events, UserManager userManager, SignInManager signInManager) : base(logger, config) { // if the TestUserStore is not in DI, then we'll just use the global users collection _userManager = userManager; _signInManager = signInManager; _interaction = interaction; _events = events; _account = new AccountService(interaction, httpContextAccessor, schemeProvider, clientStore); } /// /// Show login page /// [HttpGet] public async Task Login(string returnUrl) { ViewBag.Title = $"Sign in"; // build a model so we know what to show on the login page var vm = await _account.BuildLoginViewModelAsync(returnUrl); return View(vm); } /// /// Handle postback from username/password login /// [HttpPost] [ValidateAntiForgeryToken] public async Task Login(LoginViewModel model, string button, string returnUrl = null) { if (button != "login") { // the user clicked the "cancel" button var context = await _interaction.GetAuthorizationContextAsync(returnUrl); if (context != null) { // if the user cancels, send a result back into IdentityServer as if they // denied the consent (even if this client does not require consent). // this will send back an access denied OIDC error response to the client. await _interaction.GrantConsentAsync(context, new ConsentResponse() { Error = AuthorizationError.AccessDenied }); // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null return Redirect(returnUrl); } else { // since we don't have a valid context, then we just go back to the home page return Redirect("~/"); } } if (ModelState.IsValid) { // Check to see if the user is banned var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { if (foundUser.AccountStatus == Utilities.AccountStatus.Banned) { // Redirect to banned page return RedirectToAction(nameof(Banned)); } var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false); if (result.Succeeded) { foundUser.LastSeen = DateTime.Now; await _userManager.UpdateAsync(foundUser); // make sure the returnUrl is still valid, and if so redirect back to authorize endpoint or a local page if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return Redirect("~/"); } if (result.RequiresTwoFactor) { // Redirect to 2FA page return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe }); } if (result.IsLockedOut) { // Redirect to locked out page return RedirectToAction(nameof(Lockout)); } } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await _account.BuildLoginViewModelAsync(model); return View(vm); } [HttpGet] public async Task LoginWith2fa(bool rememberMe, string returnUrl = null) { ViewBag.Title = "Two-Factor Authentication"; // Ensure the user has gone through the username & password screen first var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load two-factor authentication user."); } var model = new LoginWith2faViewModel { RememberMe = rememberMe }; ViewData["ReturnUrl"] = returnUrl; return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null) { if (!ModelState.IsValid) { return View(model); } var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine); if (result.Succeeded) { user.LastSeen = DateTime.Now; await _userManager.UpdateAsync(user); return RedirectToLocal(returnUrl); } else if (result.IsLockedOut) { return RedirectToAction(nameof(Lockout)); } else { ModelState.AddModelError(string.Empty, "Invalid authenticator code."); return View(); } } [HttpGet] public async Task LoginWithRecoveryCode(string returnUrl = null) { ViewBag.Title = "Two-Factor Recovery Code"; // Ensure the user has gone through the username & password screen first var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load two-factor authentication user."); } ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null) { if (!ModelState.IsValid) { return View(model); } var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { throw new ApplicationException($"Unable to load two-factor authentication user."); } var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty); var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); if (result.Succeeded) { return RedirectToLocal(returnUrl); } if (result.IsLockedOut) { return RedirectToAction(nameof(Lockout)); } else { ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); return View(); } } [HttpGet] public IActionResult Lockout() { ViewBag.Title = "Locked Out"; return View(); } [HttpGet] public IActionResult Banned() { ViewBag.Title = "Banned"; return View(); } /// /// Show logout page /// [HttpGet] public async Task Logout(string logoutId) { ViewBag.Title = "Logout"; // build a model so the logout page knows what to display var vm = await _account.BuildLogoutViewModelAsync(logoutId); if (vm.ShowLogoutPrompt == false) { // if the request for logout was properly authenticated from IdentityServer, then // we don't need to show the prompt and can just log the user out directly. return await Logout(vm); } return View(vm); } /// /// Handle logout page postback /// [HttpPost] [ValidateAntiForgeryToken] public async Task Logout(LogoutInputModel model) { // get context information (client name, post logout redirect URI and iframe for federated signout) var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId); if (User?.Identity.IsAuthenticated == true) { await _signInManager.SignOutAsync(); // raise the logout event await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); } return View("LoggedOut", vm); } [HttpOptions] public async Task Logout() { try { if (User?.Identity?.IsAuthenticated == true) { await _signInManager.SignOutAsync(); } } catch (Exception ex) { _logger.LogError(ex.GetFullMessage(true, true)); } } private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); } } } }