You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多租户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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 19:13:15