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

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