Azure AD B2C使用UseOpenIdConnectAuthentication无法获取Refresh Token
问题:.NET 4.6.2中Azure AD B2C授权码流无法获取Refresh Token及相关疑问
问题现象
- 在
Authentication.Challenge步骤设置AuthenticationProperties.AllowRefresh = true,但AuthenticationTicket.Properties.AllowRefresh始终为false ProtocolMessage.AccessToken包含有效访问令牌,但ProtocolMessage.RefreshToken始终为null- 上述情况在
SecurityTokenValidated和AuthorizationCodeReceived回调中均存在
核心原因
- 缺少必要Scope:Azure AD B2C返回Refresh Token必须请求
offline_access范围,当前配置仅设置了openid,不满足条件 - 中间件默认行为限制:OWIN的OpenID Connect中间件在混合流(
code id_token)模式下,不会自动处理Refresh Token的获取,需手动发起令牌交换请求 - Properties属性未被正确继承:
Authentication.Challenge设置的AllowRefresh不会自动同步到AuthenticationTicket.Properties,需手动修正
解决方案
1. 更新OpenID Connect配置,添加offline_access Scope
修改Scope配置,包含获取Refresh Token必需的范围:
Scope = $"{OpenIdConnectScopes.OpenId} offline_access",
2. 手动修正AllowRefresh属性值
在SecurityTokenValidated回调中,强制设置AllowRefresh为true:
private Task OnOrgUserSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context) { // 手动同步AllowRefresh设置 context.AuthenticationTicket.Properties.AllowRefresh = true; // 原有业务逻辑... return Task.CompletedTask; }
3. 主动发起授权码交换获取Refresh Token
在AuthorizationCodeReceived回调中,用授权码向Azure AD B2C的Token端点发起请求,获取Refresh Token并存储:
private async Task OnOrgAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context) { var tokenEndpoint = new Uri(new Uri(context.Options.Authority), "/oauth2/v2.0/token"); var tokenRequest = new Dictionary<string, string> { ["grant_type"] = "authorization_code", ["client_id"] = context.Options.ClientId, ["client_secret"] = "YOUR_CLIENT_SECRET", // 生产环境建议用密钥托管服务存储,禁止硬编码 ["code"] = context.Code, ["redirect_uri"] = context.Options.RedirectUri, ["scope"] = $"{OpenIdConnectScopes.OpenId} offline_access" }; using (var client = new HttpClient()) { var response = await client.PostAsync(tokenEndpoint, new FormUrlEncodedContent(tokenRequest)); response.EnsureSuccessStatusCode(); var tokenResponse = await response.Content.ReadAsAsync<JObject>(); // 提取并存储Refresh Token到AuthenticationTicket var refreshToken = tokenResponse["refresh_token"]?.ToString(); if (!string.IsNullOrEmpty(refreshToken)) { context.AuthenticationTicket.Properties.Dictionary["refresh_token"] = refreshToken; // 存储Refresh Token过期时间(若返回) var refreshTokenExpiresIn = tokenResponse["refresh_token_expires_in"]?.ToObject<int>(); if (refreshTokenExpiresIn.HasValue) { context.AuthenticationTicket.Properties.ExpiresUtc = DateTime.UtcNow.AddSeconds(refreshTokenExpiresIn.Value); } } } }
关于ROPC刷新方式的疑问
不可行。ROPC流的刷新令牌仅适用于ROPC流的令牌刷新,授权码流获取的Refresh Token必须使用grant_type=refresh_token的方式发起刷新请求,需完全重构原有刷新逻辑:
- 当ID Token即将过期时,使用存储的Refresh Token向B2C Token端点请求新令牌,参数包含
grant_type=refresh_token、client_id、client_secret、refresh_token及所需Scope - 若Refresh Token过期(返回
invalid_grant错误),需清除用户认证Cookie并引导重新登录
后续优化建议
- 安全存储Client Secret:使用Azure Key Vault等安全托管服务,避免硬编码敏感信息
- 完善过期处理逻辑:捕获令牌刷新时的
invalid_grant错误,及时触发用户重新登录流程 - 持久化Refresh Token:可将Refresh Token关联用户ID存储到数据库,避免用户清除Cookie后需重新登录
内容的提问来源于stack exchange,提问作者Kiran Ramaswamy
相关产品推荐
相关产品推荐

