fengling-console/docs/task-05-create-openiddict-endpoints.md

6.5 KiB

Task 5: Create OpenIddict Endpoints

Task Description

Files:

  • Create: src/Fengling.AuthService/Controllers/AuthorizationController.cs

Implementation Steps

Step 1: Create authorization endpoints

Create: src/Fengling.AuthService/Controllers/AuthorizationController.cs

using Fengling.AuthService.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Fengling.AuthService.Controllers;

public class AuthorizationController : Controller
{
    private readonly IOpenIddictApplicationManager _applicationManager;
    private readonly IOpenIddictAuthorizationManager _authorizationManager;
    private readonly IOpenIddictScopeManager _scopeManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<AuthorizationController> _logger;

    public AuthorizationController(
        IOpenIddictApplicationManager applicationManager,
        IOpenIddictAuthorizationManager authorizationManager,
        IOpenIddictScopeManager scopeManager,
        SignInManager<ApplicationUser> signInManager,
        UserManager<ApplicationUser> userManager,
        ILogger<AuthorizationController> logger)
    {
        _applicationManager = applicationManager;
        _authorizationManager = authorizationManager;
        _scopeManager = scopeManager;
        _signInManager = signInManager;
        _userManager = userManager;
        _logger = logger;
    }

    [HttpPost("~/connect/token")]
    [Produces("application/json")]
    public async Task<IActionResult> Exchange()
    {
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByNameAsync(request.Username);
            if (user == null || user.IsDeleted)
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    new AuthenticationProperties(new Dictionary<string, string?>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户不存在"
                    }));
            }

            var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
            if (!result.Succeeded)
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                    new AuthenticationProperties(new Dictionary<string, string?>
                    {
                        [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
                        [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户名或密码错误"
                    }));
            }

            var principal = await _signInManager.CreateUserPrincipalAsync(user);

            var claims = new List<System.Security.Claims.Claim>
            {
                new(Claims.Subject, user.Id.ToString()),
                new(Claims.Name, user.UserName ?? string.Empty),
                new(Claims.Email, user.Email ?? string.Empty),
                new("tenant_id", user.TenantId.ToString())
            };

            var roles = await _userManager.GetRolesAsync(user);
            foreach (var role in roles)
            {
                claims.Add(new Claim(Claims.Role, role));
            }

            principal.SetScopes(request.GetScopes());
            principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());

            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
            new AuthenticationProperties(new Dictionary<string, string?>
            {
                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnsupportedGrantType,
                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "不支持的授权类型"
            }));
    }

    [HttpGet("~/connect/authorize")]
    [HttpPost("~/connect/authorize")]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> Authorize()
    {
        var request = HttpContext.GetOpenIddictServerRequest() ??
            throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

        var result = await HttpContext.AuthenticateAsync();
        if (result == null || !result.Succeeded)
        {
            return Challenge(
                new AuthenticationProperties
                {
                    RedirectUri = Request.Path + Request.QueryString
                },
                OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        return Ok(new { message = "Authorization endpoint" });
    }

    [HttpPost("~/connect/logout")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync();
        return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }
}

Step 2: Run to verify

Run:

dotnet run

Test with curl:

curl -X POST http://localhost:5000/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "username=admin" \
  -d "password=Admin@123" \
  -d "scope=api offline_access"

Expected: 400 error (user doesn't exist yet, but endpoint is working)

Step 3: Commit

git add src/Fengling.AuthService/Controllers/AuthorizationController.cs
git commit -m "feat(auth): add OpenIddict authorization endpoints"

Context

This task creates OpenIddict OAuth2/OIDC endpoints including token exchange, authorize, and logout. The token endpoint supports password flow for direct authentication.

Tech Stack: OpenIddict 7.2.0, ASP.NET Core

Verification

  • AuthorizationController created
  • Token endpoint supports password flow
  • Tenant ID added to token claims
  • Build succeeds
  • Committed to git

Notes

  • Token endpoint returns JWT with tenant_id claim
  • Logout endpoint clears session