多租户ASP.NET API基于OpenIddict实现前端自定义登录页的问题求助
多租户ASP.NET API + OpenIddict授权码流:自定义前端登录页的实现问题
背景
我们有一个服务多租户的ASP.NET API服务器,前端基于Angular和React开发。已通过OpenIddict的授权码流实现认证与授权功能,运行状态良好,但存在核心痛点:客户希望登录页能与自身网页设计风格完全统一。虽然可以在服务器端重写登录页外观,但需要为每个客户维护一套页面,客户调整设计时还得同步修改,维护成本极高。曾考虑Duende Identity Server,发现存在同样的限制。
此前未找到授权码流支持完全自定义登录页的文档或示例,因此向开发者社区咨询合适的解决方案。
更新(2023年10月21日)
客户仅接受Google和Facebook作为外部认证提供商,但希望将「使用Google登录」按钮直接放在React前端应用中。我尝试在ASP.NET Core认证服务器上暴露端点、设置重定向URI并返回Challenge,实现了部分功能:用户可通过外部提供商完成认证,服务器也能返回SignIn响应。回调实现参考了OpenIddict Velusia示例的callback/login/{provider},但未使用GoogleDefaults.AuthenticationScheme。
React前端代码
<form method='GET' action={`/api/v1/auth/externalLogin`} > <input type="hidden" name="scheme" value="Google"/> <input type="hidden" name="returnUrl" value="/info" /> <input type="hidden" name="tenantId" value="REDACTED" /> <input type="hidden" name="client_id" value="REDACTED" /> <Button className={styles.googleButton} variant="outlined" type="submit"> <div className={styles.googleButtonContent}> <GoogleIcon className={styles.googleIcon} /> <Typography className={styles.googleText}>Continue with Google</Typography> </div> </Button> </form>
Program.cs代码
builder.Services.AddAuthentication() .AddGoogle(options => { IConfigurationSection googleAuthNSection = builder.Configuration.GetSection("Authentication:Google"); options.ClientId = googleAuthNSection["ClientId"]; options.ClientSecret = googleAuthNSection["ClientSecret"]; options.SignInScheme = IdentityConstants.ExternalScheme; // OpenIddictServerAspNetCoreDefaults.AuthenticationScheme; // - A sign-in/Challenge response cannot be returned from this endpoint. });
Auth控制器 - 客户端登录端点
public override async Task<IActionResult> ExternalLogin(string scheme, string returnUrl, string tenantId) { var properties = new AuthenticationProperties { RedirectUri = Url.Action(nameof(ExternalLoginCallback)), Items = { { "scheme", scheme }, { "returnUrl", returnUrl }, { "tenantId", tenantId }, } }; return Challenge(properties, scheme); }
Auth控制器 - 回调方法
public override async Task<IActionResult> ExternalLoginCallback(string returnUrl = null) { var result = await HttpContext.AuthenticateAsync(GoogleDefaults.AuthenticationScheme); if (result.Principal is not ClaimsPrincipal { Identity.IsAuthenticated: true }) { throw new InvalidOperationException("The external authorization data cannot be used for authentication."); } string returnUri = result.Properties?.Items["returnUrl"]; string tenantId = result.Properties?.Items["tenantId"]; string scheme = result.Properties?.Items["scheme"]; var identity = new ClaimsIdentity(authenticationType: "ExternalLogin"); identity.SetClaim(ClaimTypes.Email, result.Principal.GetClaim(ClaimTypes.Email)) .SetClaim(ClaimTypes.Name, result.Principal.GetClaim(ClaimTypes.Name)) .SetClaim(ClaimTypes.NameIdentifier, result.Principal.GetClaim(ClaimTypes.NameIdentifier)); var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { return NotFound(); return new ObjectResult("No account exists for the user. Please register first!"){StatusCode = StatusCodes.Status418ImATeapot}; // Error occurred } // Sign in the user with this external login provider if the user already has a login. var extResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); if (extResult.IsLockedOut) { return new ForbidResult("User account is locked out"); } else if (!extResult.Succeeded) { return new ObjectResult("No account exists for the user. Please register first!"){StatusCode = StatusCodes.Status418ImATeapot}; } _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); var user = await _userManager.FindByLoginAsync(scheme, result.Principal.GetClaim(ClaimTypes.NameIdentifier)); var user = _userManager.GetUserAsync(new ClaimsPrincipal(identity)).Result; if (user == null) { // User is authenticated ok but needs to register first return new ObjectResult("No account exists for the user. Please register first!") { StatusCode = StatusCodes.Status418ImATeapot }; } // Build the authentication properties based on the properties that were added when the challenge was triggered. var properties = new AuthenticationProperties(result.Properties.Items) { RedirectUri = result.Properties.RedirectUri ?? "/" }; properties.StoreTokens(result.Properties.GetTokens()); return SignIn(new ClaimsPrincipal(identity), properties); }
当前存在的问题
- 不确定该实现是否破坏了授权码工作流;
- OpenIddict流程基本未执行,未设置任何OpenIddict相关Cookie;
- 用户无账号时体验不佳,需跳转回客户端处理注册、同意等操作。
希望社区能指出其他潜在问题,或提供解决方案。理想情况下,希望在登录端点使用OpenIddictClientAspNetCoreDefaults.AuthenticationScheme发起Challenge,引导流程至OpenIddict的~/connect/authorize端点(如官方示例所示)。
内容的提问来源于stack exchange,提问作者Rahul Misra
相关产品推荐
相关产品推荐

