mirror of
https://gitee.com/dotnetchina/OpenAuth.Net.git
synced 2025-04-24 18:04:55 +08:00
252 lines
11 KiB
C#
252 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Claims;
|
|
using System.Security.Principal;
|
|
using System.Threading.Tasks;
|
|
using IdentityModel;
|
|
using IdentityServer4.Events;
|
|
using IdentityServer4.Services;
|
|
using IdentityServer4.Stores;
|
|
using IdentityServer4.Test;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace OpenAuth.IdentityServer.Quickstart.Account
|
|
{
|
|
[SecurityHeaders]
|
|
[AllowAnonymous]
|
|
public class ExternalController : Controller
|
|
{
|
|
private readonly TestUserStore _users;
|
|
private readonly IIdentityServerInteractionService _interaction;
|
|
private readonly IClientStore _clientStore;
|
|
private readonly ILogger<ExternalController> _logger;
|
|
private readonly IEventService _events;
|
|
|
|
public ExternalController(
|
|
IIdentityServerInteractionService interaction,
|
|
IClientStore clientStore,
|
|
IEventService events,
|
|
ILogger<ExternalController> logger,
|
|
TestUserStore users = null)
|
|
{
|
|
// if the TestUserStore is not in DI, then we'll just use the global users collection
|
|
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
|
_users = users ?? new TestUserStore(TestUsers.Users);
|
|
|
|
_interaction = interaction;
|
|
_clientStore = clientStore;
|
|
_logger = logger;
|
|
_events = events;
|
|
}
|
|
|
|
/// <summary>
|
|
/// initiate roundtrip to external authentication provider
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> Challenge(string provider, string returnUrl)
|
|
{
|
|
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
|
|
|
|
// validate returnUrl - either it is a valid OIDC URL or back to a local page
|
|
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
|
|
{
|
|
// user might have clicked on a malicious link - should be logged
|
|
throw new Exception("invalid return URL");
|
|
}
|
|
|
|
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
|
|
{
|
|
// windows authentication needs special handling
|
|
return await ProcessWindowsLoginAsync(returnUrl);
|
|
}
|
|
else
|
|
{
|
|
// start challenge and roundtrip the return URL and scheme
|
|
var props = new AuthenticationProperties
|
|
{
|
|
RedirectUri = Url.Action(nameof(Callback)),
|
|
Items =
|
|
{
|
|
{ "returnUrl", returnUrl },
|
|
{ "scheme", provider },
|
|
}
|
|
};
|
|
|
|
return Challenge(props, provider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Post processing of external authentication
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> Callback()
|
|
{
|
|
// read external identity from the temporary cookie
|
|
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
|
if (result?.Succeeded != true)
|
|
{
|
|
throw new Exception("External authentication error");
|
|
}
|
|
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
|
|
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
|
}
|
|
|
|
// lookup our user and external provider info
|
|
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
|
|
if (user == null)
|
|
{
|
|
// this might be where you might initiate a custom workflow for user registration
|
|
// in this sample we don't show how that would be done, as our sample implementation
|
|
// simply auto-provisions new external user
|
|
user = AutoProvisionUser(provider, providerUserId, claims);
|
|
}
|
|
|
|
// this allows us to collect any additonal claims or properties
|
|
// for the specific prtotocols used and store them in the local auth cookie.
|
|
// this is typically used to store data needed for signout from those protocols.
|
|
var additionalLocalClaims = new List<Claim>();
|
|
var localSignInProps = new AuthenticationProperties();
|
|
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
|
|
ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps);
|
|
ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);
|
|
|
|
// issue authentication cookie for user
|
|
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray());
|
|
|
|
// delete temporary cookie used during external authentication
|
|
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
|
|
|
// retrieve return URL
|
|
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
|
|
|
|
// check if external login is in the context of an OIDC request
|
|
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
|
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.ClientId));
|
|
|
|
if (context != null)
|
|
{
|
|
if (await _clientStore.IsPkceClientAsync(context.ClientId))
|
|
{
|
|
// if the client is PKCE then we assume it's native, so this change in how to
|
|
// return the response is for better UX for the end user.
|
|
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
|
|
}
|
|
}
|
|
|
|
return Redirect(returnUrl);
|
|
}
|
|
|
|
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
|
|
{
|
|
// see if windows auth has already been requested and succeeded
|
|
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
|
|
if (result?.Principal is WindowsPrincipal wp)
|
|
{
|
|
// we will issue the external cookie and then redirect the
|
|
// user back to the external callback, in essence, treating windows
|
|
// auth the same as any other external authentication mechanism
|
|
var props = new AuthenticationProperties()
|
|
{
|
|
RedirectUri = Url.Action("Callback"),
|
|
Items =
|
|
{
|
|
{ "returnUrl", returnUrl },
|
|
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
|
|
}
|
|
};
|
|
|
|
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
|
|
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
|
|
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
|
|
|
|
// add the groups as claims -- be careful if the number of groups is too large
|
|
if (AccountOptions.IncludeWindowsGroups)
|
|
{
|
|
var wi = wp.Identity as WindowsIdentity;
|
|
var groups = wi.Groups.Translate(typeof(NTAccount));
|
|
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
|
|
id.AddClaims(roles);
|
|
}
|
|
|
|
await HttpContext.SignInAsync(
|
|
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
|
|
new ClaimsPrincipal(id),
|
|
props);
|
|
return Redirect(props.RedirectUri);
|
|
}
|
|
else
|
|
{
|
|
// trigger windows auth
|
|
// since windows auth don't support the redirect uri,
|
|
// this URL is re-triggered when we call challenge
|
|
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
|
|
}
|
|
}
|
|
|
|
private (TestUser user, string provider, string providerUserId, IEnumerable<Claim> claims) FindUserFromExternalProvider(AuthenticateResult result)
|
|
{
|
|
var externalUser = result.Principal;
|
|
|
|
// try to determine the unique id of the external user (issued by the provider)
|
|
// 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 = externalUser.FindFirst(JwtClaimTypes.Subject) ??
|
|
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
|
|
throw new Exception("Unknown userid");
|
|
|
|
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
|
|
var claims = externalUser.Claims.ToList();
|
|
claims.Remove(userIdClaim);
|
|
|
|
var provider = result.Properties.Items["scheme"];
|
|
var providerUserId = userIdClaim.Value;
|
|
|
|
// find external user
|
|
var user = _users.FindByExternalProvider(provider, providerUserId);
|
|
|
|
return (user, provider, providerUserId, claims);
|
|
}
|
|
|
|
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims)
|
|
{
|
|
var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
|
|
return user;
|
|
}
|
|
|
|
private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
|
{
|
|
// if the external system sent a session id claim, copy it over
|
|
// so we can use it for single sign-out
|
|
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
|
|
if (sid != null)
|
|
{
|
|
localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
|
|
}
|
|
|
|
// if the external provider issued an id_token, we'll keep it for signout
|
|
var id_token = externalResult.Properties.GetTokenValue("id_token");
|
|
if (id_token != null)
|
|
{
|
|
localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
|
|
}
|
|
}
|
|
|
|
private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
|
{
|
|
}
|
|
|
|
private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
|
{
|
|
}
|
|
}
|
|
}
|