mirror of
https://git.teknik.io/Teknikode/Teknik.git
synced 2023-08-02 14:16:22 +02:00
324 lines
12 KiB
C#
324 lines
12 KiB
C#
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;
|
|
|
|
namespace Teknik.IdentityServer.Controllers
|
|
{
|
|
public class AccountController : DefaultController
|
|
{
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
private readonly IIdentityServerInteractionService _interaction;
|
|
private readonly IEventService _events;
|
|
private readonly AccountService _account;
|
|
|
|
public AccountController(
|
|
ILogger<Logger> logger,
|
|
Config config,
|
|
IIdentityServerInteractionService interaction,
|
|
IClientStore clientStore,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
IAuthenticationSchemeProvider schemeProvider,
|
|
IEventService events,
|
|
UserManager<ApplicationUser> userManager,
|
|
SignInManager<ApplicationUser> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show login page
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle postback from username/password login
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> 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, ConsentResponse.Denied);
|
|
|
|
// 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)
|
|
{
|
|
// 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<IActionResult> 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<IActionResult> 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)
|
|
{
|
|
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<IActionResult> 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<IActionResult> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show logout page
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle logout page postback
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> 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()
|
|
{
|
|
if (User?.Identity.IsAuthenticated == true)
|
|
{
|
|
await _signInManager.SignOutAsync();
|
|
|
|
// raise the logout event
|
|
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
|
|
}
|
|
}
|
|
|
|
private IActionResult RedirectToLocal(string returnUrl)
|
|
{
|
|
if (Url.IsLocalUrl(returnUrl))
|
|
{
|
|
return Redirect(returnUrl);
|
|
}
|
|
else
|
|
{
|
|
return RedirectToAction(nameof(HomeController.Index), "Home");
|
|
}
|
|
}
|
|
}
|
|
} |