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

.NET API自定义JWT认证下OAuth中间件留存identity.external Cookie问题

外部OAuth认证后自定义JWT签发及identity.external Cookie问题

背景

我正在使用Entity Framework和JWT令牌在.NET API中实现Google、Facebook的OAuth认证,且已搭建好基于JWT访问令牌和刷新令牌的本地认证系统。目标是通过外部提供商(Google、Facebook)完成用户认证,提取用户信息后签发自定义JWT令牌,而非使用OAuth中间件生成的Cookie/令牌。

当前实现

认证端点代码

[HttpGet("google-login")]
[AllowAnonymous]
public IActionResult GoogleLogin([FromQuery] string returnUrl)
{
    var redirectUrl = Url.Action(nameof(GoogleResponse), "Auth", new { returnUrl }, Request.Scheme);
    var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
    return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}

[HttpGet("signin-google")]
[AllowAnonymous]
public async Task<IActionResult> GoogleResponse([FromQuery] string returnUrl)
{
    var authenticateResult = await HttpContext.AuthenticateAsync(GoogleDefaults.AuthenticationScheme);

    if (!authenticateResult.Succeeded)
        return BadRequest("Google authentication failed.");

    var claims = authenticateResult.Principal.Identities.FirstOrDefault()?.Claims;
    var email = claims?.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
    var name = claims?.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
    var key = claims?.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
    var ipAddress = HttpContext.Connection.RemoteIpAddress.MapToIPv6().ToString();

    if (string.IsNullOrEmpty(email))
        return BadRequest("Email not found");

    var result = await authService.SignInWithProviderAsync(email, key, ipAddress, "google");

    return result.Match<IActionResult, OauthResponse>(success =>
    {
        var result = success.Data;

        SetAccessTokenInResponse(result.Jwt);
        SetRefreshTokenInResponse(result.RefreshToken);
        var redirectUri = $"{returnUrl}?access_token={result.Jwt}&refresh_token={result.RefreshToken}";
        return Redirect(redirectUri);
    }, BadRequest);
}

Program.cs中的OAuth配置

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["JwtConfig:Issuer"],
        ValidAudience = builder.Configuration["JwtConfig:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtConfig:Key"]))
    };

    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            context.Token = context.Request.Cookies["tmy209w1"];
            return Task.CompletedTask;
        }
    };
}).AddGoogle(options =>
{
    options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
    options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
    options.CallbackPath = "/signin-google";
    options.SaveTokens = false;
}).AddFacebook(options =>
{
    options.ClientId = builder.Configuration["Authentication:Facebook:AppId"];
    options.ClientSecret = builder.Configuration["Authentication:Facebook:AppSecret"];
    options.CallbackPath = "/signin-facebook";
    options.SaveTokensman =Whenever;港 utg复制ger}Hola口British�Decod More重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试重试Convert先发Fabda Current MoreChief宗教()栓Call重启Remove waterDashboard。

## 疑问
1. `identity.external`是否是必需的?若必需,如何在OAuth认证后阻止该Cookie留存?
2. 我这种通过外部认证后签发自定义JWT令牌的实现方式是否正确?

## 解答
### 1. `identity.external` Cookie相关问题
- **是否必需?** 不是。这个Cookie是ASP.NET Core OAuth中间件默认生成的,用来临时缓存外部认证后的身份数据,主要服务于框架内置的登录流程。既然你完全自定义了令牌签发逻辑,不需要依赖中间件的登录机制,这个Cookie对你的场景没有作用。
- **如何阻止?** 有两种可行方案:
  - **配置层面禁用**:在添加Google/Facebook OAuth服务时,指定`SignInScheme = null`,从根源上阻止中间件生成该Cookie:
    ```csharp
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
        options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
        options.CallbackPath = "/signin-google";
        options.SaveTokens = false;
        options.SignInScheme = null; // 添加这行
    })
    ```
  - **认证后主动清除**:在回调方法中,认证成功后立即调用`SignOutAsync`清除该Cookie:
    ```csharp
    if (authenticateResult.Succeeded)
    {
        // 提取用户信息的逻辑...
        await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
    }
    ```

### 2. 自定义JWT签发实现的正确性及优化
你的实现思路是正确的,核心流程(引导外部认证→获取用户身份→签发自定义令牌)符合行业标准,但有几个可以提升安全性和健壮性的点:
- **禁止URL传递令牌**:当前把令牌放在URL参数中返回,会导致令牌被浏览器历史、服务器日志记录,存在泄露风险。建议改为:
  - 返回包含令牌的JSON响应(适合单页应用或API客户端);
  - 用`SetAccessTokenInResponse`和`SetRefreshTokenInResponse`将令牌存入**HttpOnly、Secure、SameSite=Strict**的Cookie中(刷新令牌必须用HttpOnly Cookie,防止XSS攻击)。
- **加强用户身份校验**:
  - 验证外部提供商返回的邮箱验证状态(比如Google会返回`email_verified`声明),避免使用未验证的邮箱创建账号;
  - 用外部提供商的唯一用户ID(`ClaimTypes.NameIdentifier`)关联本地用户,避免重复创建账号。
- **简化Claims获取**:认证成功后可以直接用`User.Claims`提取信息,代码更简洁:
  ```csharp
  var email = User.FindFirstValue(ClaimTypes.Email);
  var key = User.FindFirstValue(ClaimTypes.NameIdentifier);
  • 完善错误处理:添加对authenticateResult.Failure的日志记录,便于排查认证失败的具体原因。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 15:07:33