// 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; 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; public AccountController( //InMemoryUserLoginService loginService, ILoginService loginService, IIdentityServerInteractionService interaction, IClientStore clientStore, ILoggerFactory loggerFactory) { _loginService = loginService; _interaction = interaction; _clientStore = clientStore; _logger = loggerFactory.CreateLogger(); } /// /// 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); if (vm.EnableLocalLogin == false && vm.ExternalProviders.Count() == 1) { // only one option for logging in return ExternalLogin(vm.ExternalProviders.First().AuthenticationScheme, returnUrl); } return View(vm); } /// /// Handle postback from username/password login /// [HttpPost] [ValidateAntiForgeryToken] public async Task Login(LoginInputModel model) { if (ModelState.IsValid) { var user = await _loginService.FindByUsername(model.Username); // 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.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddMonths(1) }; }; //await HttpContext.Authentication.SignInAsync(, user.UserName, props); 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 providers = HttpContext.Authentication.GetAuthenticationSchemes() .Where(x => x.DisplayName != null) .Select(x => new ExternalProvider { DisplayName = x.DisplayName, AuthenticationScheme = x.AuthenticationScheme }); var allowLocal = true; if (context?.ClientId != null) { var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); if (client != null) { allowLocal = client.EnableLocalLogin; if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) { providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)); } } } return new LoginViewModel { EnableLocalLogin = allowLocal, ReturnUrl = returnUrl, Username = context?.LoginHint, ExternalProviders = providers.ToArray() }; } async Task BuildLoginViewModelAsync(LoginInputModel model) { var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); var vm = await BuildLoginViewModelAsync(model.ReturnUrl, context); vm.Username = model.Username; vm.RememberLogin = model.RememberLogin; 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("~/"); } } }