// 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 IdentityServer4.Services.InMemory; 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 eShopOnContainers.Identity.Services; using eShopOnContainers.Identity.Models; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Authorization; using eShopOnContainers.Identity.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); 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); // validate username/password against in-memory store if (await _loginService.ValidateCredentials(user, model.Password)) { // issue authentication cookie with subject ID and username //var user = _loginService.FindByUsername(model.Username); AuthenticationProperties props = null; // only set explicit expiration here if persistent. // otherwise we reply upon expiration configured in cookie middleware. if (model.RememberMe) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1) }; }; 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); 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 }); } 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(NotSupportedException) { } } // 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); var vm = new LoggedOutViewModel { PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, ClientName = logout?.ClientId, SignOutIframeUrl = logout?.SignOutIFrameUrl }; return View("LoggedOut", vm); } /// /// 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); } /// /// Post processing of external authentication /// [HttpGet] public async Task ExternalLoginCallback(string returnUrl) { // read external identity from the temporary cookie var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); //var tempUser = info?.Principal; //if (tempUser == null) //{ // throw new Exception("External authentication error"); //} //// retrieve claims of the external user //var claims = tempUser.Claims.ToList(); //// try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier //// depending on the external provider, some other claim type might be used //var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject); //if (userIdClaim == null) //{ // userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier); //} //if (userIdClaim == null) //{ // throw new Exception("Unknown userid"); //} //// remove the user id claim from the claims collection and move to the userId property //// also set the name of the external authentication provider //claims.Remove(userIdClaim); //var provider = info.Properties.Items["scheme"]; //var userId = userIdClaim.Value; //// check if the external user is already provisioned //var user = _loginService.FindByExternalProvider(provider, userId); //if (user == null) //{ // // this sample simply auto-provisions new external user // // another common approach is to start a registrations workflow first // user = _loginService.AutoProvisionUser(provider, userId, claims); //} //var additionalClaims = new List(); //// if the external system sent a session id claim, copy it over //var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); //if (sid != null) //{ // additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); //} //// issue authentication cookie for user //await HttpContext.Authentication.SignInAsync(user.Subject, user.Username, provider, additionalClaims.ToArray()); //// delete temporary cookie used during external authentication //await HttpContext.Authentication.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme); //// validate return URL and redirect back to authorization endpoint //if (_interaction.IsValidReturnUrl(returnUrl)) //{ // return Redirect(returnUrl); //} return Redirect("~/"); } // 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); } } return RedirectToAction("index", "home"); } private void AddErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } } }