// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using nuget_host.Models; using System; using System.Linq; using System.Threading.Tasks; namespace nuget_host.Controllers { [AllowAnonymous] public class AccountController : Controller { private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly SignInManager _signInManager; private readonly UserManager _userManager; public AccountController( IAuthenticationSchemeProvider schemeProvider, SignInManager signInManager, UserManager userManager) { _schemeProvider = schemeProvider; _signInManager = signInManager; _userManager = userManager; } /// /// Entry point into the login workflow /// [HttpGet] public async Task Login(string returnUrl) { // build a model so we know what to show on the login page var vm = await BuildLoginViewModelAsync(returnUrl); if (vm.IsExternalLoginOnly) { // we only have one option for logging in and it's an external provider return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl }); } return View(vm); } /// /// Handle postback from username/password login /// [HttpPost] [ValidateAntiForgeryToken] public async Task Login(LoginInputModel model, string button) { // the user clicked the "cancel" button if (button != "login") { // since we don't have a valid context, then we just go back to the home page return Redirect("~/"); } if (ModelState.IsValid) { // validate username/password var user = await _userManager.FindByNameAsync(model.Username); var signResult = await _signInManager.CheckPasswordSignInAsync(user, model.Password, true); if (signResult.Succeeded) { // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; await _signInManager.SignInAsync(user, model.RememberLogin && AccountOptions.AllowRememberLogin); if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } else if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } else { // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } } ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); } /// /// Show logout page /// [HttpGet] public async Task Logout(string logoutId) { // build a model so the logout page knows what to display var vm = await 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) { // build a model so the logged out page knows what to display var vm = await BuildLoggedOutViewModelAsync(model.LogoutId); if (User?.Identity.IsAuthenticated == true) { // delete local authentication cookie await HttpContext.SignOutAsync(); } // check if we need to trigger sign-out at an upstream identity provider if (vm.TriggerExternalSignout) { // build a return URL so the upstream provider will redirect back // to us after the user has logged out. this allows us to then // complete our single sign-out processing. string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); // this triggers a redirect to the external provider for sign-out return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); } return View("LoggedOut", vm); } [HttpGet] public IActionResult AccessDenied() { return new BadRequestObjectResult(403); } /*****************************************/ /* helper APIs for the AccountController */ /*****************************************/ private async Task BuildLoginViewModelAsync(string returnUrl) { var schemes = await _schemeProvider.GetAllSchemesAsync(); var providers = schemes .Where(x => x.DisplayName != null) .Select(x => new ExternalProvider { DisplayName = x.DisplayName ?? x.Name, AuthenticationScheme = x.Name }).ToList(); var allowLocal = true; return new LoginViewModel { AllowRememberLogin = AccountOptions.AllowRememberLogin, EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, ReturnUrl = returnUrl, ExternalProviders = providers.ToArray() }; } private async Task BuildLoginViewModelAsync(LoginInputModel model) { var vm = await BuildLoginViewModelAsync(model.ReturnUrl); vm.Username = model.Username; vm.RememberLogin = model.RememberLogin; return vm; } private async Task BuildLogoutViewModelAsync(string logoutId) { var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; if (User?.Identity.IsAuthenticated != true) { // if the user is not authenticated, then just show logged out page vm.ShowLogoutPrompt = false; return vm; } // show the logout prompt. this prevents attacks where the user // is automatically signed out by another malicious web page. return vm; } private async Task BuildLoggedOutViewModelAsync(string logoutId) { var vm = new LoggedOutViewModel { AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, LogoutId = logoutId }; return vm; } } }