ASP.NET Core对接Azure AD实现SSO及混合身份认证的实现方案咨询
现有代码的安全风险
你当前的自定义回调实现存在多个高危安全漏洞,核心问题是完全绕过了OIDC协议的标准安全校验逻辑:
- 没有校验
id_token的数字签名,攻击者可以任意构造包含合法nonce的伪造令牌直接绕过校验 - 缺少令牌核心声明校验:未验证颁发者
iss是否为你的Azure AD租户、未验证受众aud是否为你的应用Client ID、未验证令牌过期时间exp,恶意租户的令牌、其他应用的令牌、过期令牌都能通过校验 - 未校验OIDC流程中的
state参数,存在CSRF攻击风险 - 若你用内存存储
_azureGuids,分布式部署时多实例会出现校验失败的问题,且没有过期清理逻辑会存在内存泄露风险 - 若你当前使用隐式流返回
id_token,该流程本身已被OIDC标准不推荐使用,令牌容易被劫持
正确实现方案
你的整体设计思路是合理的:Azure AD认证完成后兑换为原有系统的JWT,无需修改现有授权体系。不需要完全重写signin-oidc回调接口,Microsoft.Identity.Web预留了扩展点可以满足需求,所有标准安全校验由库内部完成,不需要自己实现:
方案1:使用OIDC事件回调(适合服务端渲染/混合架构)
直接在服务注册阶段配置OpenIdConnectOptions的事件钩子,认证通过后插入自定义逻辑生成自有JWT:
// Startup.cs/Program.cs 服务注册代码 services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches(); // 配置OIDC事件 services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options => { // 令牌校验通过后触发,此时微软库已经完成了所有标准安全校验 options.Events.OnTokenValidated = async context => { // 从校验通过的身份信息中取Azure用户唯一标识 var azureOid = context.Principal.FindFirst("oid")?.Value; // 匹配本地系统用户 var localUser = await _userRepository.GetByAzureOidAsync(azureOid); if (localUser == null) { context.Fail("用户未在系统中注册"); return; } // 生成原有体系的JWT var customJwt = _yourJwtService.GenerateToken(localUser); // 把自定义JWT带到重定向地址返回给前端 context.Properties.RedirectUri = $"{context.Properties.RedirectUri}?custom_token={customJwt}"; }; // 如果需要自定义回调地址可以在这里修改,不需要自己写接口 // options.CallbackPath = "/your-custom-signin-path"; });
方案2:前端MSAL认证后兑换令牌(适合前后端分离Web API场景)
更推荐前后端分离场景下,前端用MSAL.js完成Azure AD认证流程,拿到合法的Azure AD令牌后调用后端兑换接口获取自有JWT,后端只需要校验Azure令牌合法性即可:
[HttpPost("exchange-azure-token")] [AllowAnonymous] [RequireHttps] public async Task<IActionResult> ExchangeToken([FromBody] string azureIdToken) { var validationParams = new TokenValidationParameters { ValidIssuer = $"https://login.microsoftonline.com/{_azureAdConfig.TenantId}/v2.0", ValidAudience = _azureAdConfig.ClientId, ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKeyResolver = (token, securityToken, kid, _) => { var configManager = new ConfigurationManager<OpenIdConnectConfiguration>( $"https://login.microsoftonline.com/{_azureAdConfig.TenantId}/v2.0/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); var openIdConfig = configManager.GetConfigurationAsync(CancellationToken.None).Result; return openIdConfig.SigningKeys.Where(k => k.Kid == kid); } }; try { var handler = new JwtSecurityTokenHandler(); var principal = handler.ValidateToken(azureIdToken, validationParams, out _); var azureOid = principal.FindFirst("oid")?.Value; var localUser = await _userRepository.GetByAzureOidAsync(azureOid); if (localUser == null) return Unauthorized(); var customJwt = _yourJwtService.GenerateToken(localUser); return Ok(new { Token = customJwt }); } catch (SecurityTokenValidationException) { return Unauthorized(); } }
内容的提问来源于stack exchange,提问作者chr1st0scli
相关产品推荐
相关产品推荐

