RemoteAuthenticationHandler的state与code_challenge校验问题
问题原因
发起授权挑战(Challenge)的请求和授权服务器回调的请求是两个完全独立的HTTP请求,你在HandleChallengeAsync中声明的state、codeVerifier是方法级局部变量,当前请求结束后就会被内存回收,回调请求到达时自然无法读取。你当前的实现只把nonce持久化到了Cookie中,没有对state、code_verifier做跨请求持久化,所以会出现值丢失的问题。
实现方案
参考你现有nonce的存储逻辑,在发起重定向前将state、code_verifier加密后存入同域Cookie,回调时取出校验即可,具体修改步骤如下:
1. 修改HandleChallengeAsync,持久化跨请求需要的参数
不要明文存储敏感参数,直接用RemoteAuthenticationHandler基类内置的Protector数据保护对象加密后存储,Cookie路径严格绑定回调地址减少泄漏面:
// 原有生成state、nonce、codeVerifier、codeChallenge的逻辑不变 // 原有Nonce Cookie存储逻辑不变 Response.Cookies.Append("Nonce", Convert.ToBase64String(SHA256.HashData(nonce)), new CookieOptions { Path = "/callback", HttpOnly = true, IsEssential = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = Clock.UtcNow.AddHours(1) }); // 新增:打包需要跨请求保留的授权上下文,加密后存入Cookie var flowContext = new Dictionary<string, string> { ["state"] = Convert.ToBase64String(state), ["code_verifier"] = Convert.ToBase64String(codeVerifier) }; var serializedContext = System.Text.Json.JsonSerializer.Serialize(flowContext); var protectedContext = Protector.Protect(serializedContext); Response.Cookies.Append("OAuthFlowContext", protectedContext, new CookieOptions { Path = Options.CallbackPath, HttpOnly = true, IsEssential = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = Clock.UtcNow.AddHours(1) }); // 原有重定向逻辑不变 Response.Redirect($"{Options.Authority}/authorization?client_id={Options.ClientId}" + $"&callback_uri={Request.Scheme}://{Request.Host}{Options.CallbackPath}&scopes={Options.Scopes}" + $"&state={Convert.ToBase64String(state)}&nonce={Convert.ToBase64String(nonce)}&code_challenge={Convert.ToBase64String(codeChallenge)}");
2. 在HandleRemoteAuthenticateAsync中取出参数完成校验
回调到达时先解密之前存储的上下文,比对state参数,校验通过后取出code_verifier用于后续换取令牌,校验完成后立即删除临时Cookie防止重放攻击:
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync() { // 从回调查询参数中取返回的state if (!Request.Query.TryGetValue("state", out var callbackState)) { return HandleRequestResult.Fail("回调请求缺少state参数"); } // 取之前存储的授权上下文Cookie if (!Request.Cookies.TryGetValue("OAuthFlowContext", out var protectedContext)) { return HandleRequestResult.Fail("缺少授权流程上下文Cookie"); } // 解密上下文,解密失败直接返回校验不通过 string serializedContext; try { serializedContext = Protector.Unprotect(protectedContext); } catch { return HandleRequestResult.Fail("授权流程上下文无效"); } var flowContext = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(serializedContext); var originalState = flowContext["state"]; var codeVerifier = flowContext["code_verifier"]; // 比对state,不一致说明可能存在CSRF攻击 if (!string.Equals(originalState, callbackState, StringComparison.Ordinal)) { // 清理无效Cookie Response.Cookies.Delete("OAuthFlowContext", new CookieOptions { Path = Options.CallbackPath }); Response.Cookies.Delete("Nonce", new CookieOptions { Path = "/callback" }); return HandleRequestResult.Fail("State校验失败,请求可能存在伪造风险"); } // 校验通过后清理临时Cookie,避免重放攻击 Response.Cookies.Delete("OAuthFlowContext", new CookieOptions { Path = Options.CallbackPath }); Response.Cookies.Delete("Nonce", new CookieOptions { Path = "/callback" }); // 后续逻辑:使用回调携带的code、解密得到的codeVerifier调用授权服务器接口换token,构建AuthenticationTicket返回即可 throw new NotImplementedException(); }
注意事项
- 禁止明文存储code_verifier这类敏感参数,基类提供的
Protector和当前认证方案绑定,加密安全性符合框架标准,直接使用即可。 - Cookie的
Path属性必须严格设置为回调路径,保证只有回调请求会携带这些敏感Cookie,尽可能缩小泄漏范围。 - 校验流程走完后必须立即删除临时存储的上下文Cookie,避免Cookie被窃取后重放授权请求。
- 如果不想自己实现Cookie存储逻辑,也可以直接使用基类内置的关联ID(CorrelationId)机制:调用
GenerateCorrelationId(properties)方法会自动生成加密state存入Cookie,回调时调用ValidateCorrelationId(properties)即可完成state校验,code_verifier可以存入AuthenticationProperties.Items集合,通过基类的PropertiesDataFormat加密后随state一起传递,和你现有代码逻辑相比改动量稍大,但更符合框架内置的设计规范。
内容的提问来源于stack exchange,提问作者Szyszka947
相关产品推荐
相关产品推荐

