You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在基于OpenID Connect与Amazon Cognito的ASP.NET Core Web应用中扩展/signin-oidc端点以实现登录成功事件触发及用户档案创建

处理ASP.NET Core + Amazon Cognito登录成功事件与扩展回调端点

你好!针对你的需求,我分两种场景给出解决方案——首先是更推荐的利用认证事件钩子来触发登录后的操作,其次是如果必须自定义/signin-oidc端点时的实现方式。


一、推荐方案:通过OpenIdConnectEvents捕获登录成功事件

ASP.NET Core的OIDC中间件提供了丰富的事件钩子,完全不需要重写默认的/signin-oidc端点就能实现登录成功后的逻辑(比如创建用户档案)。具体步骤如下:

  1. 配置OpenIdConnectOptions时添加事件处理
    在Program.cs(或Startup.cs,取决于你的.NET版本)的认证配置中,找到AddOpenIdConnect的部分,添加Events配置:
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // 你的Cognito基础配置
    options.Authority = "https://cognito-idp.{region}.amazonaws.com/{userPoolId}";
    options.ClientId = "your-client-id";
    options.ClientSecret = "your-client-secret";
    options.ResponseType = "code";
    // 对应你的权限范围
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    
    // 登录成功后的事件处理
    options.Events = new OpenIdConnectEvents
    {
        // 令牌验证通过后触发(此时用户已完成登录)
        OnTokenValidated = async context =>
        {
            // 获取Cognito返回的核心用户信息
            var userId = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); // 对应Cognito的sub字段
            var email = context.Principal.FindFirstValue(ClaimTypes.Email);
            var userName = context.Principal.FindFirstValue(ClaimTypes.Name);

            // 调用自定义服务,检查并创建用户档案
            var userProfileService = context.HttpContext.RequestServices.GetRequiredService<IUserProfileService>();
            await userProfileService.EnsureUserProfileExistsAsync(userId, email, userName);

            // 如需扩展用户Claims,可在此操作
            // var identity = (ClaimsIdentity)context.Principal.Identity;
            // identity.AddClaim(new Claim("custom-claim", "value"));
        },

        // 也可使用OnUserInformationReceived事件(获取用户信息端点数据后触发)
        // OnUserInformationReceived = async context =>
        // {
        //     var userInfo = context.User;
        //     var userId = userInfo["sub"].ToString();
        //     // 执行档案创建逻辑
        // }
    };
});
  1. 实现用户档案服务
    创建接口和实现类,封装检查/创建用户档案的业务逻辑:
public interface IUserProfileService
{
    Task EnsureUserProfileExistsAsync(string userId, string email, string userName);
}

public class UserProfileService : IUserProfileService
{
    private readonly ApplicationDbContext _dbContext;

    public UserProfileService(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task EnsureUserProfileExistsAsync(string userId, string email, string userName)
    {
        var existingProfile = await _dbContext.UserProfiles.FirstOrDefaultAsync(p => p.UserId == userId);
        if (existingProfile == null)
        {
            var newProfile = new UserProfile
            {
                UserId = userId,
                Email = email,
                UserName = userName,
                CreatedAt = DateTime.UtcNow
            };
            _dbContext.UserProfiles.Add(newProfile);
            await _dbContext.SaveChangesAsync();
        }
    }
}

这种方案的优势是完全利用框架扩展点,无需改动默认回调端点,符合ASP.NET Core的设计理念,安全性和可维护性更高。


二、自定义/signin-oidc端点(仅特殊需求使用)

如果你确实需要完全控制/signin-oidc的逻辑,可以通过以下步骤实现:

  1. 禁用默认OIDC回调处理
    在AddOpenIdConnect配置中,修改回调路径或跳过未识别请求,避免默认中间件拦截:
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // 其他配置...
    options.SkipUnrecognizedRequests = true;
    options.CallbackPath = "/custom-signin-oidc"; // 让默认中间件不处理原/signin-oidc
})
  1. 创建自定义控制器处理回调
    手动实现令牌验证、用户身份创建和档案逻辑:
[Route("[controller]")]
public class AuthController : Controller
{
    private readonly IConfiguration _configuration;
    private readonly IUserProfileService _userProfileService;

    public AuthController(IConfiguration configuration, IUserProfileService userProfileService)
    {
        _configuration = configuration;
        _userProfileService = userProfileService;
    }

    [HttpGet("signin-oidc")]
    public async Task<IActionResult> SignInOidc()
    {
        // 1. 获取授权码
        var code = Request.Query["code"];
        if (string.IsNullOrEmpty(code))
        {
            return BadRequest("Authorization code is missing.");
        }

        // 2. 向Cognito请求令牌
        var tokenClient = new HttpClient();
        var tokenRequest = new HttpRequestMessage(HttpMethod.Post, $"{_configuration["Auth:Cognito:Authority"]}/oauth2/token");
        tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "authorization_code",
            ["client_id"] = _configuration["Auth:Cognito:ClientId"],
            ["client_secret"] = _configuration["Auth:Cognito:ClientSecret"],
            ["code"] = code,
            ["redirect_uri"] = Url.Action("SignInOidc", "Auth", null, Request.Scheme)
        });

        var tokenResponse = await tokenClient.SendAsync(tokenRequest);
        tokenResponse.EnsureSuccessStatusCode();
        var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();

        // 3. 验证ID Token并生成ClaimsPrincipal
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = _configuration["Auth:Cognito:Authority"],
            ValidateAudience = true,
            ValidAudience = _configuration["Auth:Cognito:ClientId"],
            ValidateLifetime = true,
            IssuerSigningKeys = await GetCognitoSigningKeysAsync()
        };

        var handler = new JwtSecurityTokenHandler();
        var principal = handler.ValidateToken(tokenData.IdToken, validationParameters, out _);

        // 4. 执行用户档案创建逻辑
        var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
        var email = principal.FindFirstValue(ClaimTypes.Email);
        var userName = principal.FindFirstValue(ClaimTypes.Name);
        await _userProfileService.EnsureUserProfileExistsAsync(userId, email, userName);

        // 5. 登录用户(生成认证Cookie)
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);

        // 6. 重定向到原请求路径或首页
        var returnUrl = Request.Query["returnUrl"] ?? "/";
        return Redirect(returnUrl);
    }

    private async Task<IEnumerable<SecurityKey>> GetCognitoSigningKeysAsync()
    {
        var httpClient = new HttpClient();
        var jwksResponse = await httpClient.GetFromJsonAsync<JsonWebKeySet>($"{_configuration["Auth:Cognito:Authority"]}/.well-known/jwks.json");
        return jwksResponse!.Keys.Select(k => k);
    }
}

// 辅助类:反序列化令牌响应
public class TokenResponse
{
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; } = string.Empty;

    [JsonPropertyName("id_token")]
    public string IdToken { get; set; } = string.Empty;

    [JsonPropertyName("token_type")]
    public string TokenType { get; set; } = string.Empty;

    [JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }
}

⚠️ 注意:这种方式需要手动处理令牌验证、签名密钥获取、错误处理等细节,容易引入安全风险,仅在有特殊自定义需求时使用。

内容的提问来源于stack exchange,提问作者Justin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 14:02:44