The next generation of the Teknik Services. Written in ASP.NET. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AccountController.cs 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. using IdentityModel;
  2. using IdentityServer4.Services;
  3. using IdentityServer4.Stores;
  4. using IdentityServer4.Test;
  5. using Microsoft.AspNetCore.Http;
  6. using Microsoft.AspNetCore.Mvc;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using System.Security.Claims;
  11. using System.Security.Principal;
  12. using System.Threading.Tasks;
  13. using Microsoft.AspNetCore.Authentication;
  14. using IdentityServer4.Events;
  15. using IdentityServer4.Extensions;
  16. using IdentityServer4.Models;
  17. using Microsoft.AspNetCore.Identity;
  18. using Teknik.IdentityServer.Security;
  19. using Teknik.IdentityServer.Services;
  20. using Teknik.IdentityServer.ViewModels;
  21. using Teknik.IdentityServer.Options;
  22. using Teknik.IdentityServer.Models;
  23. using Microsoft.Extensions.Logging;
  24. using Teknik.Logging;
  25. using Teknik.Configuration;
  26. using Teknik.Utilities;
  27. namespace Teknik.IdentityServer.Controllers
  28. {
  29. public class AccountController : DefaultController
  30. {
  31. private readonly UserManager<ApplicationUser> _userManager;
  32. private readonly SignInManager<ApplicationUser> _signInManager;
  33. private readonly IIdentityServerInteractionService _interaction;
  34. private readonly IEventService _events;
  35. private readonly AccountService _account;
  36. public AccountController(
  37. ILogger<Logger> logger,
  38. Config config,
  39. IIdentityServerInteractionService interaction,
  40. IClientStore clientStore,
  41. IHttpContextAccessor httpContextAccessor,
  42. IAuthenticationSchemeProvider schemeProvider,
  43. IEventService events,
  44. UserManager<ApplicationUser> userManager,
  45. SignInManager<ApplicationUser> signInManager) : base(logger, config)
  46. {
  47. // if the TestUserStore is not in DI, then we'll just use the global users collection
  48. _userManager = userManager;
  49. _signInManager = signInManager;
  50. _interaction = interaction;
  51. _events = events;
  52. _account = new AccountService(interaction, httpContextAccessor, schemeProvider, clientStore);
  53. }
  54. /// <summary>
  55. /// Show login page
  56. /// </summary>
  57. [HttpGet]
  58. public async Task<IActionResult> Login(string returnUrl)
  59. {
  60. ViewBag.Title = $"Sign in";
  61. // build a model so we know what to show on the login page
  62. var vm = await _account.BuildLoginViewModelAsync(returnUrl);
  63. return View(vm);
  64. }
  65. /// <summary>
  66. /// Handle postback from username/password login
  67. /// </summary>
  68. [HttpPost]
  69. [ValidateAntiForgeryToken]
  70. public async Task<IActionResult> Login(LoginViewModel model, string button, string returnUrl = null)
  71. {
  72. if (button != "login")
  73. {
  74. // the user clicked the "cancel" button
  75. var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
  76. if (context != null)
  77. {
  78. // if the user cancels, send a result back into IdentityServer as if they
  79. // denied the consent (even if this client does not require consent).
  80. // this will send back an access denied OIDC error response to the client.
  81. await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
  82. // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
  83. return Redirect(returnUrl);
  84. }
  85. else
  86. {
  87. // since we don't have a valid context, then we just go back to the home page
  88. return Redirect("~/");
  89. }
  90. }
  91. if (ModelState.IsValid)
  92. {
  93. // Check to see if the user is banned
  94. var foundUser = await _userManager.FindByNameAsync(model.Username);
  95. if (foundUser != null)
  96. {
  97. if (foundUser.AccountStatus == Utilities.AccountStatus.Banned)
  98. {
  99. // Redirect to banned page
  100. return RedirectToAction(nameof(Banned));
  101. }
  102. var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false);
  103. if (result.Succeeded)
  104. {
  105. foundUser.LastSeen = DateTime.Now;
  106. await _userManager.UpdateAsync(foundUser);
  107. // make sure the returnUrl is still valid, and if so redirect back to authorize endpoint or a local page
  108. if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
  109. {
  110. return Redirect(returnUrl);
  111. }
  112. return Redirect("~/");
  113. }
  114. if (result.RequiresTwoFactor)
  115. {
  116. // Redirect to 2FA page
  117. return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
  118. }
  119. if (result.IsLockedOut)
  120. {
  121. // Redirect to locked out page
  122. return RedirectToAction(nameof(Lockout));
  123. }
  124. }
  125. await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
  126. ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
  127. }
  128. // something went wrong, show form with error
  129. var vm = await _account.BuildLoginViewModelAsync(model);
  130. return View(vm);
  131. }
  132. [HttpGet]
  133. public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
  134. {
  135. ViewBag.Title = "Two-Factor Authentication";
  136. // Ensure the user has gone through the username & password screen first
  137. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  138. if (user == null)
  139. {
  140. throw new ApplicationException($"Unable to load two-factor authentication user.");
  141. }
  142. var model = new LoginWith2faViewModel { RememberMe = rememberMe };
  143. ViewData["ReturnUrl"] = returnUrl;
  144. return View(model);
  145. }
  146. [HttpPost]
  147. [ValidateAntiForgeryToken]
  148. public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
  149. {
  150. if (!ModelState.IsValid)
  151. {
  152. return View(model);
  153. }
  154. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  155. if (user == null)
  156. {
  157. throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
  158. }
  159. var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
  160. var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
  161. if (result.Succeeded)
  162. {
  163. user.LastSeen = DateTime.Now;
  164. await _userManager.UpdateAsync(user);
  165. return RedirectToLocal(returnUrl);
  166. }
  167. else if (result.IsLockedOut)
  168. {
  169. return RedirectToAction(nameof(Lockout));
  170. }
  171. else
  172. {
  173. ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
  174. return View();
  175. }
  176. }
  177. [HttpGet]
  178. public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
  179. {
  180. ViewBag.Title = "Two-Factor Recovery Code";
  181. // Ensure the user has gone through the username & password screen first
  182. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  183. if (user == null)
  184. {
  185. throw new ApplicationException($"Unable to load two-factor authentication user.");
  186. }
  187. ViewData["ReturnUrl"] = returnUrl;
  188. return View();
  189. }
  190. [HttpPost]
  191. [ValidateAntiForgeryToken]
  192. public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
  193. {
  194. if (!ModelState.IsValid)
  195. {
  196. return View(model);
  197. }
  198. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  199. if (user == null)
  200. {
  201. throw new ApplicationException($"Unable to load two-factor authentication user.");
  202. }
  203. var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
  204. var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
  205. if (result.Succeeded)
  206. {
  207. return RedirectToLocal(returnUrl);
  208. }
  209. if (result.IsLockedOut)
  210. {
  211. return RedirectToAction(nameof(Lockout));
  212. }
  213. else
  214. {
  215. ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
  216. return View();
  217. }
  218. }
  219. [HttpGet]
  220. public IActionResult Lockout()
  221. {
  222. ViewBag.Title = "Locked Out";
  223. return View();
  224. }
  225. [HttpGet]
  226. public IActionResult Banned()
  227. {
  228. ViewBag.Title = "Banned";
  229. return View();
  230. }
  231. /// <summary>
  232. /// Show logout page
  233. /// </summary>
  234. [HttpGet]
  235. public async Task<IActionResult> Logout(string logoutId)
  236. {
  237. ViewBag.Title = "Logout";
  238. // build a model so the logout page knows what to display
  239. var vm = await _account.BuildLogoutViewModelAsync(logoutId);
  240. if (vm.ShowLogoutPrompt == false)
  241. {
  242. // if the request for logout was properly authenticated from IdentityServer, then
  243. // we don't need to show the prompt and can just log the user out directly.
  244. return await Logout(vm);
  245. }
  246. return View(vm);
  247. }
  248. /// <summary>
  249. /// Handle logout page postback
  250. /// </summary>
  251. [HttpPost]
  252. [ValidateAntiForgeryToken]
  253. public async Task<IActionResult> Logout(LogoutInputModel model)
  254. {
  255. // get context information (client name, post logout redirect URI and iframe for federated signout)
  256. var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId);
  257. if (User?.Identity.IsAuthenticated == true)
  258. {
  259. await _signInManager.SignOutAsync();
  260. // raise the logout event
  261. await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
  262. }
  263. return View("LoggedOut", vm);
  264. }
  265. [HttpOptions]
  266. public async Task Logout()
  267. {
  268. try
  269. {
  270. if (User?.Identity?.IsAuthenticated == true)
  271. {
  272. await _signInManager.SignOutAsync();
  273. }
  274. }
  275. catch (Exception ex)
  276. {
  277. _logger.LogError(ex.GetFullMessage(true, true));
  278. }
  279. }
  280. private IActionResult RedirectToLocal(string returnUrl)
  281. {
  282. if (Url.IsLocalUrl(returnUrl))
  283. {
  284. return Redirect(returnUrl);
  285. }
  286. else
  287. {
  288. return RedirectToAction(nameof(HomeController.Index), "Home");
  289. }
  290. }
  291. }
  292. }