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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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. // make sure the returnUrl is still valid, and if so redirect back to authorize endpoint or a local page
  105. if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
  106. {
  107. return Redirect(returnUrl);
  108. }
  109. return Redirect("~/");
  110. }
  111. if (result.RequiresTwoFactor)
  112. {
  113. // Redirect to 2FA page
  114. return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe });
  115. }
  116. if (result.IsLockedOut)
  117. {
  118. // Redirect to locked out page
  119. return RedirectToAction(nameof(Lockout));
  120. }
  121. }
  122. await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
  123. ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
  124. }
  125. // something went wrong, show form with error
  126. var vm = await _account.BuildLoginViewModelAsync(model);
  127. return View(vm);
  128. }
  129. [HttpGet]
  130. public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
  131. {
  132. ViewBag.Title = "Two-Factor Authentication";
  133. // Ensure the user has gone through the username & password screen first
  134. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  135. if (user == null)
  136. {
  137. throw new ApplicationException($"Unable to load two-factor authentication user.");
  138. }
  139. var model = new LoginWith2faViewModel { RememberMe = rememberMe };
  140. ViewData["ReturnUrl"] = returnUrl;
  141. return View(model);
  142. }
  143. [HttpPost]
  144. [ValidateAntiForgeryToken]
  145. public async Task<IActionResult> LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null)
  146. {
  147. if (!ModelState.IsValid)
  148. {
  149. return View(model);
  150. }
  151. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  152. if (user == null)
  153. {
  154. throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
  155. }
  156. var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
  157. var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
  158. if (result.Succeeded)
  159. {
  160. return RedirectToLocal(returnUrl);
  161. }
  162. else if (result.IsLockedOut)
  163. {
  164. return RedirectToAction(nameof(Lockout));
  165. }
  166. else
  167. {
  168. ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
  169. return View();
  170. }
  171. }
  172. [HttpGet]
  173. public async Task<IActionResult> LoginWithRecoveryCode(string returnUrl = null)
  174. {
  175. ViewBag.Title = "Two-Factor Recovery Code";
  176. // Ensure the user has gone through the username & password screen first
  177. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  178. if (user == null)
  179. {
  180. throw new ApplicationException($"Unable to load two-factor authentication user.");
  181. }
  182. ViewData["ReturnUrl"] = returnUrl;
  183. return View();
  184. }
  185. [HttpPost]
  186. [ValidateAntiForgeryToken]
  187. public async Task<IActionResult> LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null)
  188. {
  189. if (!ModelState.IsValid)
  190. {
  191. return View(model);
  192. }
  193. var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
  194. if (user == null)
  195. {
  196. throw new ApplicationException($"Unable to load two-factor authentication user.");
  197. }
  198. var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
  199. var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
  200. if (result.Succeeded)
  201. {
  202. return RedirectToLocal(returnUrl);
  203. }
  204. if (result.IsLockedOut)
  205. {
  206. return RedirectToAction(nameof(Lockout));
  207. }
  208. else
  209. {
  210. ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
  211. return View();
  212. }
  213. }
  214. [HttpGet]
  215. public IActionResult Lockout()
  216. {
  217. ViewBag.Title = "Locked Out";
  218. return View();
  219. }
  220. [HttpGet]
  221. public IActionResult Banned()
  222. {
  223. ViewBag.Title = "Banned";
  224. return View();
  225. }
  226. /// <summary>
  227. /// Show logout page
  228. /// </summary>
  229. [HttpGet]
  230. public async Task<IActionResult> Logout(string logoutId)
  231. {
  232. ViewBag.Title = "Logout";
  233. // build a model so the logout page knows what to display
  234. var vm = await _account.BuildLogoutViewModelAsync(logoutId);
  235. if (vm.ShowLogoutPrompt == false)
  236. {
  237. // if the request for logout was properly authenticated from IdentityServer, then
  238. // we don't need to show the prompt and can just log the user out directly.
  239. return await Logout(vm);
  240. }
  241. return View(vm);
  242. }
  243. /// <summary>
  244. /// Handle logout page postback
  245. /// </summary>
  246. [HttpPost]
  247. [ValidateAntiForgeryToken]
  248. public async Task<IActionResult> Logout(LogoutInputModel model)
  249. {
  250. // get context information (client name, post logout redirect URI and iframe for federated signout)
  251. var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId);
  252. if (User?.Identity.IsAuthenticated == true)
  253. {
  254. await _signInManager.SignOutAsync();
  255. // raise the logout event
  256. await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
  257. }
  258. return View("LoggedOut", vm);
  259. }
  260. [HttpOptions]
  261. public async Task Logout()
  262. {
  263. if (User?.Identity.IsAuthenticated == true)
  264. {
  265. await _signInManager.SignOutAsync();
  266. // raise the logout event
  267. await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
  268. }
  269. }
  270. private IActionResult RedirectToLocal(string returnUrl)
  271. {
  272. if (Url.IsLocalUrl(returnUrl))
  273. {
  274. return Redirect(returnUrl);
  275. }
  276. else
  277. {
  278. return RedirectToAction(nameof(HomeController.Index), "Home");
  279. }
  280. }
  281. }
  282. }