333 lines
13 KiB
C#
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("~/");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|