mirror of
				https://gitee.com/dotnetchina/OpenAuth.Net.git
				synced 2025-10-25 18:29:01 +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) | ||
|  |         { | ||
|  |         } | ||
|  |     } | ||
|  | } |