OpenIddict部署IIS后GetOpenIddictServerRequest为空及重定向异常
部署至IIS后OpenIddict核心功能异常排查
问题现象
GetOpenIddictServerRequest()在IIS环境下始终返回null,本地测试正常;- 移除上述代码后调用
SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)时抛出异常:System.InvalidOperationException: A sign-in response cannot be returned from this endpoint.
环境配置
- .NET 8.0
- OpenIddict 4.10.1
OpenIddict配置代码
public static void AddConsioAuthenticationDict(this IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); // Enable Quartz.NET integration. services.AddQuartz(options => { options.UseMicrosoftDependencyInjectionJobFactory(); options.UseSimpleTypeLoader(); options.UseInMemoryStore(); }); X509Certificate2? privateKey = null; string currentDirectory = Directory.GetCurrentDirectory(); var bytes = File.ReadAllBytes(currentDirectory + "\\secretCert.pfx"); if (bytes != null && bytes.Length > 2) { privateKey = new X509Certificate2(bytes, "somepassword"); } if (privateKey == null) { X509Store store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); for (int i = 0; i < store.Certificates.Count; i++) { X509Certificate2? cert = store.Certificates[i]; var serial = configuration["NetsSoapSetting:certificate-serial"]; var thumbprint = configuration["NetsSoapSetting:certificate-thumbprint"]; if (string.Equals(cert.SerialNumber, serial, StringComparison.OrdinalIgnoreCase) || string.Equals(cert.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase)) { privateKey = cert; break; } } } services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(connectionString); // Register the entity sets needed by OpenIddict. // Note: use the generic overload if you need to replace the default OpenIddict entities. options.UseOpenIddict(); }); services.AddDatabaseDeveloperPageExceptionFilter(); services.AddOpenIddict() .AddCore(option => { option.UseEntityFrameworkCore() .UseDbContext<ApplicationDbContext>(); option.UseQuartz(); }) .AddServer(option => { option.RegisterScopes( OpenIddictConstants.Scopes.Profile, OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Permissions.Scopes.Profile); option.RequireProofKeyForCodeExchange(); option.Configure(options => options.CodeChallengeMethods.Add(OidcConstants.CodeChallengeMethods.Plain)); option.SetAuthorizationEndpointUris("/connect/authorize/"); // Enable the token endpoint.8 option.SetTokenEndpointUris("/connect/token/"); // Enable the client credentials flow. option.SetUserinfoEndpointUris("/connect/userinfo/"); option.SetLogoutEndpointUris("/connect/logout/"); option.AllowImplicitFlow(); option.AllowClientCredentialsFlow(); option.AllowPasswordFlow(); option.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange(); ; option.AllowHybridFlow(); option.AllowRefreshTokenFlow(); option .AddSigningCertificate(privateKey); //option.AddDevelopmentEncryptionCertificate() // .AddDevelopmentSigningCertificate(); // Register the ASP.NET Core host and configure the ASP.NET Core options. option.UseAspNetCore() .EnableTokenEndpointPassthrough() .EnableLogoutEndpointPassthrough() .EnableAuthorizationEndpointPassthrough() .EnableUserinfoEndpointPassthrough(); option .AddEphemeralEncryptionKey() .AddEphemeralSigningKey(); // encrypt the access token to hide all info public! //.DisableAccessTokenEncryption(); }) .AddValidation(options => { // Import the configuration from the local OpenIddict server instance. options.UseLocalServer(); options.EnableTokenEntryValidation(); // Register the ASP.NET Core host. options.UseAspNetCore(); }); }
Authorize方法代码
[HttpGet("~/connect/authorize")] [HttpPost("~/connect/authorize")] [IgnoreAntiforgeryToken] public async Task<IActionResult> Authorize() { //var request = HttpContext.GetOpenIddictServerRequest() ?? // throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); var remoteIpAddress = HttpContext.Connection.RemoteIpAddress; //var GrantTypes = request.GrantType; //var clientSecret = request.ClientSecret; // Retrieve the user principal stored in the authentication cookie. //var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); // If the user principal can't be extracted, redirect the user to the login page. if (!result.Succeeded) { return Challenge( authenticationSchemes: IdentityConstants.ApplicationScheme, properties: new AuthenticationProperties { RedirectUri = Request.PathBase + Request.Path + QueryString.Create( Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) }); } var userId = string.Empty; if (HttpContext.User.Identity != null && !String.IsNullOrEmpty(HttpContext.User.Identity.Name)) { var username = HttpContext.User.Identity.Name; var user = await _userManager.FindByNameAsync(username); if (user != null) { userId = user.Id; } } var claims = new List<Claim> { // 'subject' claim which is required new Claim(OpenIddictConstants.Claims.Subject, userId), new Claim(OpenIddictConstants.Claims.ExpiresAt, DateTime.UtcNow.AddHours(1).ToString()), new Claim("someclaim", "somevalue").SetDestinations(OpenIddictConstants.Destinations.AccessToken), new Claim("originalIp", remoteIpAddress.ToString()).SetDestinations(OpenIddictConstants.Destinations.AccessToken), }; var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); // Set requested scopes (this is not done automatically) //claimsPrincipal.SetScopes(request.GetScopes()); // Signing in with the OpenIddict authentication scheme trigger OpenIddict to issue a code (which can be exchanged for an access token) // Create a new authentication ticket for the user's principal return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); }
问题原因及修复方案
1. GetOpenIddictServerRequest()返回null的原因及修复
核心原因是IIS的路径处理与OpenIddict的端点匹配逻辑不兼容:
- 配置的授权端点带末尾斜杠
/connect/authorize/,但实际请求可能不带斜杠,或者IIS的URL重写规则修改了路径,导致OpenIddict无法识别该请求属于授权端点; - IIS经典托管管道模式会导致ASP.NET Core中间件无法正常解析请求上下文;
Directory.GetCurrentDirectory()在IIS环境下指向系统目录,而非应用程序根目录,可能导致证书加载失败,间接影响OpenIddict初始化。
修复步骤:
- 统一端点路径:将
SetAuthorizationEndpointUris("/connect/authorize/")改为SetAuthorizationEndpointUris("/connect/authorize"),去掉末尾斜杠,确保与控制器路由一致; - 检查IIS托管模式:将应用程序池的
托管管道模式设置为集成; - 修正证书加载路径:使用
IWebHostEnvironment.ContentRootPath替代Directory.GetCurrentDirectory()获取应用程序根目录,避免路径错误; - 排查URL重写规则:确保IIS没有修改
/connect/*前缀的请求路径。
2. SignIn(...)抛出异常的原因及修复
该异常是第一个问题的连锁反应:当OpenIddict无法识别当前请求为授权端点请求时,不会为响应生成必要的上下文,此时调用SignIn生成授权响应自然会失败。
修复步骤:
- 先解决第一个问题,确保
GetOpenIddictServerRequest()能正常获取请求对象; - 恢复
claimsPrincipal.SetScopes(request.GetScopes());代码,OpenIddict需要根据请求的Scope生成合法的令牌; - 确保认证票据关联请求的客户端信息,比如从
request.ClientId添加对应Claim,保证响应能正确关联到发起请求的客户端。
内容的提问来源于stack exchange,提问作者Hai Pham
相关产品推荐
相关产品推荐

