using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using System.Collections.Immutable; using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; namespace HuiXin.Identity.OpenIddict.Controllers { [ApiController] [Route("auth")] public class AuthorizationController : Controller { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly IOpenIddictScopeManager _scopeManager; public AuthorizationController(IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager) { _applicationManager = applicationManager; _scopeManager = scopeManager; _authorizationManager = authorizationManager; } /// /// 登录权限校验 /// /// /// /// [HttpGet("connect/authorize")] [IgnoreAntiforgeryToken] public async Task Authorize() { // var s=await _schemeProvider.GetAllSchemesAsync(); //通过扩展的获取自定义的参数校验 var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("未获取到相关认证情况"); #region 存在登录凭证且明确了登录请求的行为 // 存在登录凭证且明确了登录请求的行为 if (request.HasPrompt(Prompts.Login)) { //这里有个小坑,在Challenge之前必须把这个行为去掉 不然 Challenge 进入 /connect/authorize 路由陷入死循环 var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login)); var parameters = Request.HasFormContentType ? Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt))); return Challenge( authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, // IdentityConstants.ApplicationScheme, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) }); } #endregion //检索本地的Cookies信息 确定重定向页面 这里都是UTC时间来设置的过期情况 这里没有用Identity 所以这里可以指定自己的应用名称 var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); //IdentityConstants.ApplicationScheme #region 未获取本地Cookies信息或者 cookie过期的情况 if (request == null || !result.Succeeded || (request.MaxAge != null && result.Properties?.IssuedUtc != null && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) { //是否是无效授权 if (request.HasPrompt(Prompts.None)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户未登录." })); } return Challenge( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create( Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) }); } #endregion //var resultdata = _userService.GetLoginInfo(new Guid(result.Principal.GetClaim(Claims.Subject) ?? throw new Exception("用户标识存在"))) ?? throw new Exception("用户详细信息不存在"); var resultdata = new UserInfo() { Id = "user1", UserName = "测试用户名", NickName = "测试昵称", }; // 获取客户端详细信息 验证其他数据 var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ?? throw new InvalidOperationException("未查找到该客户端的应用详细信息"); //查找当前情况客户端下请求用户的持久化授权数据信息 var authorizations = _authorizationManager.FindAsync( subject: resultdata.Id.ToString(), client: await _applicationManager.GetIdAsync(application) ?? throw new Exception("没有找到客户端的应用信息"), //这里区分下 是application的Id而不是ClientId status: Statuses.Valid, type: AuthorizationTypes.Permanent, scopes: request.GetScopes() ).ToBlockingEnumerable();//.ToListAsync(); var consenttype = await _applicationManager.GetConsentTypeAsync(application); //获取授权同意确认页面 switch (consenttype) { //判断授权同意的类型 //1 外部允许的且没有任何授权项 case ConsentTypes.External when !authorizations.Any(): return Forbid( authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "登录用户没用访问该客户端应用的权限" })); // 隐式、外部授权、显示模式模式 case ConsentTypes.Implicit: case ConsentTypes.External when authorizations.Any(): case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent): ClaimsPrincipal principal = CreateUserPrincpal(resultdata); //设置请求的范围 principal.SetScopes(request.GetScopes()); //查找scope允许访问的资源 var resources = _scopeManager.ListResourcesAsync(principal.GetScopes()).ToBlockingEnumerable(); //通过扩展设置不同的资源访问 其实本质都是设置Claims 只是 key 在 scope以及Resource上不同 //Resource = "oi_rsrc"; // Scope = "oi_scp"; principal.SetResources(resources); // 自动创建一个永久授权,以避免需要明确的同意 用于包含相同范围的未来授权或令牌请求 var authorization = authorizations.LastOrDefault(); if (authorization is null) { authorization = await _authorizationManager.CreateAsync( principal: principal, subject: resultdata.Id.ToString(), client: await _applicationManager.GetIdAsync(application), type: AuthorizationTypes.Permanent, scopes: principal.GetScopes()); } principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } //登录 OpenIddict签发令牌 return SignIn(principal, null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // At this point, no authorization was found in the database and an error must be returned // if the client application specified prompt=none in the authorization request. case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): case ConsentTypes.Systematic when request.HasPrompt(Prompts.None): return Forbid( authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Interactive user consent is required." })); // In every other case, render the consent form. //default: // return View(new AuthorizeViewModel // { // ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application), // Scope = request.Scope // }); default: return Challenge( authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties { RedirectUri = "/" } ); } } [HttpPost("connect/token"), Produces("application/json")] public async Task ConnectToken() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OIDC请求不存在"); if (request.IsClientCredentialsGrantType()) { // Note: the client credentials are automatically validated by OpenIddict: // if client_id or client_secret are invalid, this action won't be invoked. var application = await _applicationManager.FindByClientIdAsync(request.ClientId); if (application is null) { Console.WriteLine($"client_id未注册:{request.ClientId}"); throw new InvalidOperationException("The application cannot be found."); } Console.WriteLine($"获取到客户信息:{JsonConvert.SerializeObject(application)}"); // Create a new ClaimsIdentity containing the claims that // will be used to create an id_token, a token or a code. var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); // Use the client_id as the subject identifier. identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application)); identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application)); identity.SetDestinations(static claim => claim.Type switch { // Allow the "name" claim to be stored in both the access and identity tokens // when the "profile" scope was granted (by calling principal.SetScopes(...)). Claims.Name when claim.Subject.HasScope(Scopes.Profile) => new[] { Destinations.AccessToken, Destinations.IdentityToken }, // Otherwise, only store the claim in the access tokens. _ => new[] { Destinations.AccessToken } }); //Console.WriteLine($"identity信息:{JsonConvert.SerializeObject(identity)}"); var result = SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); //Console.WriteLine($"signIn信息:{JsonConvert.SerializeObject(result)}"); return result; } else if (request.IsPasswordGrantType()) { //数据库获取用户信息 var princpal = CreateUserPrincpal(new UserInfo() { Id = Guid.NewGuid().ToString(), UserName = "测试用户名", NickName = "测试昵称", }); //princpal.SetResources(_scopeManager.ListResourcesAsync(princpal.GetScopes())); foreach (var claim in princpal.Claims) { claim.SetDestinations(GetDestinations(claim, princpal)); } return SignIn(princpal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType()) { var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; //var user = _userService.GetLoginInfo(new Guid(principal?.GetClaim(Claims.Subject) ?? throw new Exception("用户标识存在"))); //UserInfo user = null; //if (user == null) //{ // return Forbid( // authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, // properties: new AuthenticationProperties(new Dictionary // { // [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, // [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "令牌已失效" // }) // ); //} foreach (var claim in principal.Claims) { claim.SetDestinations(GetDestinations(claim, principal)); } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } throw new InvalidOperationException($"不支持的grant_type:{request.GrantType}"); } [HttpPost("connect/logout")] public async Task Logout() { await HttpContext.SignOutAsync(); return SignOut( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties: new AuthenticationProperties { RedirectUri = "/" }); } public static ClaimsPrincipal CreateUserPrincpal(UserInfo user, string claimsIdentityName = "USERINFO") { //登录成功流程 ClaimsIdentity identity = new ClaimsIdentity(claimsIdentityName); identity.AddClaim(new Claim(Claims.Subject, user.Id)); identity.AddClaim(new Claim(Claims.Name, user.UserName)); identity.AddClaim(new Claim(Claims.Nickname, user.NickName)); return new ClaimsPrincipal(identity); } private IEnumerable GetDestinations(Claim claim) { // Note: by default, claims are NOT automatically included in the access and identity tokens. // To allow OpenIddict to serialize them, you must attach them a destination, that specifies // whether they should be included in access tokens, in identity tokens or in both. return claim.Type switch { Claims.Name or Claims.Subject => ImmutableArray.Create(Destinations.AccessToken, Destinations.IdentityToken), _ => ImmutableArray.Create(Destinations.AccessToken), }; } private IEnumerable GetDestinations(Claim claim, ClaimsPrincipal principal) { switch (claim.Type) { case Claims.Name: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Profile)) yield return Destinations.IdentityToken; yield break; case Claims.Email: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Email)) yield return Destinations.IdentityToken; yield break; case Claims.Role: yield return Destinations.AccessToken; if (principal.HasScope(Scopes.Roles)) yield return Destinations.IdentityToken; yield break; case "AspNet.Identity.SecurityStamp": yield break; default: yield return Destinations.AccessToken; yield break; } } } }