2016-11-24 15:31:33 +01:00

333 lines
13 KiB
C#

// 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
{
/// <summary>
/// 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
/// </summary>
public class AccountController : Controller
{
//private readonly InMemoryUserLoginService _loginService;
private readonly ILoginService<ApplicationUser> _loginService;
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly ILogger _logger;
public AccountController(
//InMemoryUserLoginService loginService,
ILoginService<ApplicationUser> loginService,
IIdentityServerInteractionService interaction,
IClientStore clientStore,
ILoggerFactory loggerFactory)
{
_loginService = loginService;
_interaction = interaction;
_clientStore = clientStore;
_logger = loggerFactory.CreateLogger<AccountController>();
}
/// <summary>
/// Show login page
/// </summary>
[HttpGet]
public async Task<IActionResult> 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);
}
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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<LoginViewModel> 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<LoginViewModel> 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;
}
/// <summary>
/// Show logout page
/// </summary>
[HttpGet]
public async Task<IActionResult> 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);
}
/// <summary>
/// Handle logout page postback
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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);
}
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[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);
}
/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> 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<Claim>();
//// 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("~/");
}
}
}