fengling-auth-service/Controllers/AuthorizationController.cs
2026-02-03 15:30:12 +08:00

218 lines
11 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.ViewModels;
using Microsoft.AspNetCore;
using Microsoft.Extensions.Primitives;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("connect")]
public class AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
ILogger<AuthorizationController> logger)
: Controller
{
[HttpGet("authorize")]
[HttpPost("authorize")]
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 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.
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;
}
}
}