- Remove redundant PointsRule repositories (use single PointsRuleRepository) - Clean up Member migrations and consolidate to single Init migration - Update Console frontend API and components for Tenant - Add H5LinkService for member H5 integration
365 lines
18 KiB
C#
365 lines
18 KiB
C#
using Fengling.AuthService.Data;
|
|
using Fengling.AuthService.Models;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using OpenIddict.Abstractions;
|
|
using OpenIddict.Server.AspNetCore;
|
|
using System.Security.Claims;
|
|
using Fengling.AuthService.Configuration;
|
|
using Fengling.AuthService.ViewModels;
|
|
using Microsoft.AspNetCore;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.Extensions.Primitives;
|
|
using Swashbuckle.AspNetCore.Annotations;
|
|
using static OpenIddict.Abstractions.OpenIddictConstants;
|
|
|
|
namespace Fengling.AuthService.Controllers;
|
|
|
|
public class AuthorizationController(
|
|
IOpenIddictApplicationManager applicationManager,
|
|
IOpenIddictAuthorizationManager authorizationManager,
|
|
IOpenIddictScopeManager scopeManager,
|
|
SignInManager<ApplicationUser> signInManager,
|
|
UserManager<ApplicationUser> userManager,
|
|
ILogger<AuthorizationController> logger)
|
|
: Controller
|
|
{
|
|
|
|
[Authorize, FormValueRequired("submit.Accept")]
|
|
[Tags("submit.Accept")]
|
|
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
|
|
[SwaggerIgnore]
|
|
public async Task<IActionResult> Accept()
|
|
{
|
|
var request = HttpContext.GetOpenIddictServerRequest() ??
|
|
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
|
|
|
|
// Retrieve the profile of the logged in user.
|
|
var user = await userManager.GetUserAsync(User) ??
|
|
throw new InvalidOperationException("The user details cannot be retrieved.");
|
|
if (user == null)
|
|
{
|
|
throw new InvalidOperationException("The user details cannot be retrieved.");
|
|
}
|
|
|
|
// Retrieve the application details from the database.
|
|
var application = await applicationManager.FindByClientIdAsync(request.ClientId!) ??
|
|
throw new InvalidOperationException(
|
|
"Details concerning the calling client application cannot be found.");
|
|
|
|
// Retrieve the permanent authorizations associated with the user and the calling client application.
|
|
var authorizations = await authorizationManager.FindAsync(
|
|
subject: await userManager.GetUserIdAsync(user),
|
|
client: await applicationManager.GetIdAsync(application),
|
|
status: OpenIddictConstants.Statuses.Valid,
|
|
type: OpenIddictConstants.AuthorizationTypes.Permanent,
|
|
scopes: request.GetScopes()).ToListAsync();
|
|
|
|
// Note: the same check is already made in the other action but is repeated
|
|
// here to ensure a malicious user can't abuse this POST-only endpoint and
|
|
// force it to return a valid response without the external authorization.
|
|
if (!authorizations.Any() &&
|
|
await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External))
|
|
{
|
|
return Forbid(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties(new Dictionary<string, string>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
|
"The logged in user is not allowed to access this client application."
|
|
}!));
|
|
}
|
|
|
|
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
|
|
|
|
var principal = result.Principal!;
|
|
|
|
// Note: in this sample, the granted scopes match the requested scope
|
|
// but you may want to allow the user to uncheck specific scopes.
|
|
// For that, simply restrict the list of scopes before calling SetScopes.
|
|
principal.SetScopes(request.GetScopes());
|
|
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
|
|
|
|
// Automatically create a permanent authorization to avoid requiring explicit consent
|
|
// for future authorization or token requests containing the same scopes.
|
|
var authorization = authorizations.LastOrDefault();
|
|
if (authorization == null)
|
|
{
|
|
authorization = await authorizationManager.CreateAsync(
|
|
principal: principal,
|
|
subject: await userManager.GetUserIdAsync(user)!,
|
|
client: await applicationManager.GetIdAsync(application) ?? string.Empty,
|
|
type: OpenIddictConstants.AuthorizationTypes.Permanent,
|
|
scopes: principal.GetScopes());
|
|
}
|
|
|
|
principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
|
|
|
|
foreach (var claim in principal.Claims)
|
|
{
|
|
claim.SetDestinations(GetDestinations(claim, principal));
|
|
}
|
|
|
|
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
|
|
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
|
}
|
|
|
|
[Authorize, FormValueRequired("submit.Deny")]
|
|
[Tags("submit.Deny")]
|
|
[SwaggerIgnore]
|
|
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
|
|
// Notify OpenIddict that the authorization grant has been denied by the resource owner
|
|
// to redirect the user agent to the client application using the appropriate response_mode.
|
|
public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
|
|
|
|
|
[HttpGet("~/connect/authorize")]
|
|
[HttpPost("~/connect/authorize")]
|
|
[Tags("Authorize")]
|
|
[IgnoreAntiforgeryToken]
|
|
public async Task<IActionResult> Authorize()
|
|
{
|
|
var request = HttpContext.GetOpenIddictServerRequest() ??
|
|
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
|
|
|
|
// If prompt=login was specified by the client application,
|
|
// immediately return the user agent to the login page.
|
|
if (request.HasPromptValue(OpenIddictConstants.PromptValues.Login))
|
|
{
|
|
// To avoid endless login -> authorization redirects, the prompt=login flag
|
|
// is removed from the authorization request payload before redirecting the user.
|
|
var prompt = string.Join(" ", request.GetPromptValues().Remove(OpenIddictConstants.PromptValues.Login));
|
|
|
|
var parameters = Request.HasFormContentType
|
|
? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList()
|
|
: Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList();
|
|
|
|
parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt)));
|
|
|
|
return Challenge(
|
|
authenticationSchemes: IdentityConstants.ApplicationScheme,
|
|
properties: new AuthenticationProperties
|
|
{
|
|
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
|
|
});
|
|
}
|
|
|
|
// Retrieve the user principal stored in the authentication cookie.
|
|
// If a max_age parameter was provided, ensure that the cookie is not too old.
|
|
// If the user principal can't be extracted or the cookie is too old, redirect the user to the login page.
|
|
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
|
|
if (result is not { Succeeded: true } || (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
|
|
DateTimeOffset.UtcNow - result.Properties.IssuedUtc >
|
|
TimeSpan.FromSeconds(request.MaxAge.Value)))
|
|
{
|
|
// If the client application requested promptless authentication,
|
|
// return an error indicating that the user is not logged in.
|
|
if (request.HasPromptValue(OpenIddictConstants.PromptValues.None))
|
|
{
|
|
return Forbid(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties(new Dictionary<string, string>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] =
|
|
OpenIddictConstants.Errors.LoginRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
|
|
}!));
|
|
}
|
|
|
|
return Challenge(
|
|
authenticationSchemes: IdentityConstants.ApplicationScheme,
|
|
properties: new AuthenticationProperties
|
|
{
|
|
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
|
|
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
|
|
});
|
|
}
|
|
|
|
// Retrieve the profile of the logged in user.
|
|
var user = await userManager.GetUserAsync(result.Principal) ??
|
|
throw new InvalidOperationException("The user details cannot be retrieved.");
|
|
|
|
// Retrieve the application details from the database.
|
|
var application = await applicationManager.FindByClientIdAsync(request.ClientId!) ??
|
|
throw new InvalidOperationException(
|
|
"Details concerning the calling client application cannot be found.");
|
|
|
|
// Retrieve the permanent authorizations associated with the user and the calling client application.
|
|
var authorizations = await authorizationManager.FindAsync(
|
|
subject: await userManager.GetUserIdAsync(user),
|
|
client: (await applicationManager.GetIdAsync(application))!,
|
|
status: OpenIddictConstants.Statuses.Valid,
|
|
type: OpenIddictConstants.AuthorizationTypes.Permanent,
|
|
scopes: request.GetScopes()).ToListAsync();
|
|
|
|
switch (await applicationManager.GetConsentTypeAsync(application))
|
|
{
|
|
// If the consent is external (e.g when authorizations are granted by a sysadmin),
|
|
// immediately return an error if no authorization can be found in the database.
|
|
case OpenIddictConstants.ConsentTypes.External when !authorizations.Any():
|
|
return Forbid(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties(new Dictionary<string, string>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] =
|
|
OpenIddictConstants.Errors.ConsentRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
|
"The logged in user is not allowed to access this client application."
|
|
}!));
|
|
|
|
// If the consent is implicit or if an authorization was found,
|
|
// return an authorization response without displaying the consent form.
|
|
case OpenIddictConstants.ConsentTypes.Implicit:
|
|
case OpenIddictConstants.ConsentTypes.External when authorizations.Any():
|
|
case OpenIddictConstants.ConsentTypes.Explicit
|
|
when authorizations.Any() && !request.HasPromptValue(OpenIddictConstants.PromptValues.Consent):
|
|
var principal = await signInManager.CreateUserPrincipalAsync(user);
|
|
|
|
// Note: in this sample, the granted scopes match the requested scope
|
|
// but you may want to allow the user to uncheck specific scopes.
|
|
// For that, simply restrict the list of scopes before calling SetScopes.
|
|
principal.SetScopes(request.GetScopes());
|
|
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
|
|
|
|
// Automatically create a permanent authorization to avoid requiring explicit consent
|
|
// for future authorization or token requests containing the same scopes.
|
|
var authorization = authorizations.LastOrDefault();
|
|
if (authorization == null)
|
|
{
|
|
authorization = await authorizationManager.CreateAsync(
|
|
principal: principal,
|
|
subject: await userManager.GetUserIdAsync(user),
|
|
client: (await applicationManager.GetIdAsync(application))!,
|
|
type: OpenIddictConstants.AuthorizationTypes.Permanent,
|
|
scopes: principal.GetScopes());
|
|
}
|
|
|
|
principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
|
|
|
|
foreach (var claim in principal.Claims)
|
|
{
|
|
claim.SetDestinations(GetDestinations(claim, principal));
|
|
}
|
|
|
|
return SignIn(principal, 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 OpenIddictConstants.ConsentTypes.Explicit
|
|
when request.HasPromptValue(OpenIddictConstants.PromptValues.None):
|
|
case OpenIddictConstants.ConsentTypes.Systematic
|
|
when request.HasPromptValue(OpenIddictConstants.PromptValues.None):
|
|
return Forbid(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties(new Dictionary<string, string>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] =
|
|
OpenIddictConstants.Errors.ConsentRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
|
"Interactive user consent is required."
|
|
}!));
|
|
|
|
// In every other case, render the consent form.
|
|
default:
|
|
return View(new AuthorizeViewModel(await applicationManager.GetDisplayNameAsync(application),
|
|
request.Scope));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
|
|
{
|
|
// Note: by default, claims are NOT automatically included in 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.
|
|
|
|
switch (claim.Type)
|
|
{
|
|
case OpenIddictConstants.Claims.Name:
|
|
yield return OpenIddictConstants.Destinations.AccessToken;
|
|
|
|
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Profile))
|
|
yield return OpenIddictConstants.Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
case OpenIddictConstants.Claims.Email:
|
|
yield return OpenIddictConstants.Destinations.AccessToken;
|
|
|
|
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Email))
|
|
yield return OpenIddictConstants.Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
case OpenIddictConstants.Claims.Role:
|
|
yield return OpenIddictConstants.Destinations.AccessToken;
|
|
|
|
if (principal.HasScope(OpenIddictConstants.Permissions.Scopes.Roles))
|
|
yield return OpenIddictConstants.Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
// Never include the security stamp in the access and identity tokens, as it's a secret value.
|
|
case "AspNet.Identity.SecurityStamp": yield break;
|
|
|
|
default:
|
|
yield return OpenIddictConstants.Destinations.AccessToken;
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
|
|
[HttpGet("~/connect/userinfo")]
|
|
public async Task<IActionResult> UserInfo()
|
|
{
|
|
var user = await userManager.GetUserAsync(User) ??
|
|
throw new InvalidOperationException("The user details cannot be retrieved.");
|
|
|
|
// 获取用户的角色
|
|
var roles = await userManager.GetRolesAsync(user);
|
|
|
|
// 获取用户的租户信息
|
|
var tenantInfo = user.TenantInfo;
|
|
|
|
var claims = new List<Claim>
|
|
{
|
|
new(OpenIddictConstants.Claims.Subject, await userManager.GetUserIdAsync(user)),
|
|
new(OpenIddictConstants.Claims.Name, user.UserName!),
|
|
new(OpenIddictConstants.Claims.PreferredUsername, user.UserName!)
|
|
};
|
|
if (claims == null) throw new ArgumentNullException(nameof(claims));
|
|
|
|
if (!string.IsNullOrEmpty(user.Email))
|
|
{
|
|
claims.Add(new(OpenIddictConstants.Claims.Email, user.Email!));
|
|
}
|
|
|
|
// 添加角色 claims
|
|
foreach (var role in roles)
|
|
{
|
|
claims.Add(new(OpenIddictConstants.Claims.Role, role));
|
|
}
|
|
|
|
// 添加自定义 tenant 相关 claims
|
|
claims.Add(new Claim("tenant_id", tenantInfo.TenantId.ToString()));
|
|
claims.Add(new Claim("tenant_code", tenantInfo.TenantCode));
|
|
claims.Add(new Claim("tenant_name", tenantInfo.TenantName));
|
|
|
|
return Ok(new Dictionary<string, object>
|
|
{
|
|
["sub"] = await userManager.GetUserIdAsync(user),
|
|
["name"] = user.UserName ?? "Anonymous",
|
|
["preferred_username"] = user.UserName ?? "Anonymous",
|
|
["email"] = user.Email ?? "",
|
|
["role"] = roles.ToArray(),
|
|
["tenant_id"] = tenantInfo.TenantId.ToString(),
|
|
["tenant_code"] = tenantInfo?.TenantCode ?? "",
|
|
["tenant_name"] = tenantInfo?.TenantName ?? ""
|
|
});
|
|
}
|
|
} |