// 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 IdentityModel; using IdentityServer4.Quickstart.UI.Models; using IdentityServer4.Services; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using IdentityServer4.Models; using IdentityServer4.Stores; using Identity.API.Services; using Identity.API.Models; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Authorization; using Identity.API.Models.AccountViewModels; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authentication; namespace IdentityServer4.Quickstart.UI.Controllers { /// /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval /// public class AccountController : Controller { //private readonly InMemoryUserLoginService _loginService; private readonly ILoginService _loginService; private readonly IIdentityServerInteractionService _interaction; private readonly IClientStore _clientStore; private readonly ILogger _logger; private readonly UserManager _userManager; public AccountController( //InMemoryUserLoginService loginService, ILoginService loginService, IIdentityServerInteractionService interaction, IClientStore clientStore, ILoggerFactory loggerFactory, UserManager userManager) { _loginService = loginService; _interaction = interaction; _clientStore = clientStore; _logger = loggerFactory.CreateLogger(); _userManager = userManager; } /// /// Show login page /// [HttpGet] public async Task Login(string returnUrl) { var context = await _interaction.GetAuthorizationContextAsync(returnUrl); if (context?.IdP != null) { // if IdP is passed, then bypass showing the login screen return ExternalLogin(context.IdP, returnUrl); } var vm = await BuildLoginViewModelAsync(returnUrl, context); ViewData["ReturnUrl"] = returnUrl; return View(vm); } /// /// Handle postback from username/password login /// [HttpPost] [ValidateAntiForgeryToken] public async Task Login(LoginViewModel model) { if (ModelState.IsValid) { var user = await _loginService.FindByUsername(model.Email); if (await _loginService.ValidateCredentials(user, model.Password)) { AuthenticationProperties props = null; if (model.RememberMe) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddYears(10) }; }; await _loginService.SignIn(user); // make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint if (_interaction.IsValidReturnUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); } ModelState.AddModelError("", "Invalid username or password."); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); ViewData["ReturnUrl"] = model.ReturnUrl; return View(vm); } async Task BuildLoginViewModelAsync(string returnUrl, AuthorizationRequest context) { var allowLocal = true; if (context?.ClientId != null) { var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); if (client != null) { allowLocal = client.EnableLocalLogin; } } return new LoginViewModel { ReturnUrl = returnUrl, Email = context?.LoginHint, }; } async Task BuildLoginViewModelAsync(LoginViewModel model) { var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); var vm = await BuildLoginViewModelAsync(model.ReturnUrl, context); vm.Email = model.Email; vm.RememberMe = model.RememberMe; return vm; } /// /// Show logout page /// [HttpGet] public async Task Logout(string logoutId) { if (User.Identity.IsAuthenticated == false) { // if the user is not authenticated, then just show logged out page return await Logout(new LogoutViewModel { LogoutId = logoutId }); } //Test for Xamarin. var context = await _interaction.GetLogoutContextAsync(logoutId); if (context?.ShowSignoutPrompt == false) { //it's safe to automatically sign-out return await Logout(new LogoutViewModel { LogoutId = logoutId }); } // show the logout prompt. this prevents attacks where the user // is automatically signed out by another malicious web page. var vm = new LogoutViewModel { LogoutId = logoutId }; return View(vm); } /// /// Handle logout page postback /// [HttpPost] [ValidateAntiForgeryToken] public async Task Logout(LogoutViewModel model) { var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider) { if (model.LogoutId == null) { // if there's no current logout context, we need to create one // this captures necessary info from the current logged in user // before we signout and redirect away to the external IdP for signout model.LogoutId = await _interaction.CreateLogoutContextAsync(); } string url = "/Account/Logout?logoutId=" + model.LogoutId; try { // hack: try/catch to handle social providers that throw await HttpContext.Authentication.SignOutAsync(idp, new AuthenticationProperties { RedirectUri = url }); } catch(Exception ex) { _logger.LogCritical(ex.Message); } } // delete authentication cookie await HttpContext.Authentication.SignOutAsync(); // set this so UI rendering sees an anonymous user HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); // get context information (client name, post logout redirect URI and iframe for federated signout) var logout = await _interaction.GetLogoutContextAsync(model.LogoutId); return Redirect(logout?.PostLogoutRedirectUri); } public async Task DeviceLogOut(string redirectUrl) { // delete authentication cookie await HttpContext.Authentication.SignOutAsync(); // set this so UI rendering sees an anonymous user HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity()); return Redirect(redirectUrl); } /// /// initiate roundtrip to external authentication provider /// [HttpGet] public IActionResult ExternalLogin(string provider, string returnUrl) { if (returnUrl != null) { returnUrl = UrlEncoder.Default.Encode(returnUrl); } returnUrl = "/account/externallogincallback?returnUrl=" + returnUrl; // start challenge and roundtrip the return URL var props = new AuthenticationProperties { RedirectUri = returnUrl, Items = { { "scheme", provider } } }; return new ChallengeResult(provider, props); } // GET: /Account/Register [HttpGet] [AllowAnonymous] public IActionResult Register(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } // // POST: /Account/Register [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task Register(RegisterViewModel model, string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email, CardHolderName = model.User.CardHolderName, CardNumber = model.User.CardNumber, CardType = model.User.CardType, City = model.User.City, Country = model.User.Country, Expiration = model.User.Expiration, LastName = model.User.LastName, Name = model.User.Name, Street = model.User.Street, State = model.User.State, ZipCode = model.User.ZipCode, PhoneNumber = model.User.PhoneNumber, SecurityNumber = model.User.SecurityNumber }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Errors.Count() > 0) { AddErrors(result); // If we got this far, something failed, redisplay form return View(model); } } if (returnUrl != null) { if (HttpContext.User.Identity.IsAuthenticated) return Redirect(returnUrl); else if (ModelState.IsValid) return RedirectToAction("login", "account", new { returnUrl = returnUrl }); else return View(model); } return RedirectToAction("index", "home"); } [HttpGet] public IActionResult Redirecting() { return View(); } private void AddErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } } }