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;
}
}
}
}