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

如何从ASP.NET Core API手动重定向至Azure AD租户专属登录页

问题背景与需求

我们有一个多租户应用,所有租户的Azure AD信息都存储在数据库中,不需要在应用启动时配置Azure AD认证。需要在完成部分操作后从数据库获取这些信息,为每个租户动态配置对应的Microsoft Azure AD登录页,并且不在appsettings.json中存储任何租户的AD信息。

业务场景

API中有如下接口,当用户未授权时,需要触发Azure AD登录挑战,通过认证后才能获取用户列表:

[HttpGet]
public async Task<IActionResult> GetUsers()
{
    // 引导用户使用Microsoft AzureAD登录

    // 通过认证后,用户才能获取用户列表

    var users = new List<string>{ "Mike, Jhon"};

    return Ok(users);
}

同时,React前端向该API发起请求时,若租户用户未授权,需要将其重定向至所属租户的专属登录页。

疑问与尝试

这个需求是否可行?我已经做了大量调研但没找到解决方案,若有实现思路或实践经验,请详细说明方法。

我曾尝试以下两种方式,但都未达到预期效果:

  1. 直接调用Azure AD授权端点:
var response = await new HttpClient().GetAsync(@"https://login.microsoftonline.com/xxx43de-b3fb-efdea0768eb7/oauth2/v2.0/authorize?
client_id=xxxxx-efdea0768eb7&
response_type=id_token&
redirect_uri=https://localhost:7008/signin-oidc&
scope=openid&
response_mode=form_post&
state=12345&
nonce=678910");
  1. 使用ASP.NET Core的Challenge方法:
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);

可行方案实现步骤

这个需求完全可行,核心是动态构建OpenID Connect认证方案,而非在启动时固定配置。下面是具体实现步骤:

1. 基础配置:禁用启动时的固定Azure AD配置

确保Program.cs中只保留认证服务的基础注册,不要硬编码OpenID Connect配置:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie();
// 不要在这里添加固定的AddOpenIdConnect配置

2. 动态加载租户AD信息并构建认证方案

在需要触发登录挑战的逻辑中,先从数据库获取当前租户的Azure AD信息(租户ID、客户端ID、重定向URI等),然后动态创建专属的OpenID Connect认证方案:

核心方法:动态注册认证方案

通过IAuthenticationSchemeProvider和IOptionsMonitorCache实现动态方案注册:

private async Task<AuthenticationScheme> GetOrCreateTenantAuthScheme(TenantAdInfo tenantAdInfo)
{
    var schemeName = $"OpenIDConnect-Tenant-{tenantAdInfo.TenantId}";
    var schemeProvider = HttpContext.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
    
    // 检查方案是否已存在,避免重复创建
    var existingScheme = await schemeProvider.GetSchemeAsync(schemeName);
    if (existingScheme != null)
        return existingScheme;

    // 构建OpenID Connect配置选项
    var options = new OpenIdConnectOptions(schemeName)
    {
        ClientId = tenantAdInfo.ClientId,
        Authority = $"https://login.microsoftonline.com/{tenantAdInfo.TenantId}/v2.0",
        RedirectUri = tenantAdInfo.BackendRedirectUri, // 后端回调地址,如/signin-oidc
        ResponseType = OpenIdConnectResponseType.CodeIdToken,
        Scope = { "openid", "profile" },
        SaveTokens = true,
        // 自定义Token验证后的逻辑,比如关联租户用户身份
        Events = new OpenIdConnectEvents
        {
            OnTokenValidated = context =>
            {
                // 这里可添加租户用户的身份映射逻辑
                return Task.CompletedTask;
            }
        }
    };

    // 缓存配置选项并注册新认证方案
    var optionsCache = HttpContext.RequestServices.GetRequiredService<IOptionsMonitorCache<OpenIdConnectOptions>>();
    optionsCache.TryAdd(schemeName, options);
    
    var newScheme = new AuthenticationScheme(schemeName, $"Tenant-{tenantAdInfo.TenantId}", typeof(OpenIdConnectHandler));
    await schemeProvider.AddSchemeAsync(newScheme);
    
    return newScheme;
}

3. 在接口中触发动态登录挑战

修改GetUsers接口,先识别当前租户、检查用户认证状态,未授权时触发动态Challenge:

[HttpGet]
public async Task<IActionResult> GetUsers()
{
    // 从请求上下文(如域名、请求头)识别当前租户ID
    var tenantId = ExtractTenantIdFromRequest(); 
    var tenantAdInfo = await _tenantDbRepository.GetAdInfoByTenantId(tenantId);

    if (!HttpContext.User.Identity.IsAuthenticated)
    {
        var authScheme = await GetOrCreateTenantAuthScheme(tenantAdInfo);
        // 认证完成后重定向回当前接口
        var properties = new AuthenticationProperties { RedirectUri = Request.Path };
        return Challenge(properties, authScheme.Name);
    }

    var users = new List<string>{ "Mike", "Jhon"};
    return Ok(users);
}

4. 适配前端重定向逻辑

因为是React前端发起请求,直接返回302重定向会导致前端无法正确处理,需要调整为:

  • API检测到未授权时,返回401 Unauthorized,并在响应头中携带租户专属的登录URL
  • 前端收到401后,读取登录URL并跳转到该地址,认证完成后回调回前端页面,再重新发起API请求

修改接口的未授权返回逻辑:

if (!HttpContext.User.Identity.IsAuthenticated)
{
    // 构建Azure AD授权URL
    var loginUrl = $"https://login.microsoftonline.com/{tenantAdInfo.TenantId}/oauth2/v2.0/authorize?" +
        $"client_id={tenantAdInfo.ClientId}" +
        $"&response_type=code" +
        $"&redirect_uri={Uri.EscapeDataString(tenantAdInfo.FrontendRedirectUri)}" +
        $"&scope=openid profile api://{tenantAdInfo.ClientId}/access_as_user" +
        $"&state={Uri.EscapeDataString(Request.Path)}" + // 保存原请求路径,认证后跳转
        $"&nonce={Guid.NewGuid().ToString()}";

    Response.Headers.Add("X-Login-Url", loginUrl);
    return Unauthorized();
}

5. 后端回调端点处理

如果使用后端signin-oidc回调,需要从请求中提取租户信息,动态加载对应认证方案完成处理:

[HttpPost("/signin-oidc")]
public async Task<IActionResult> SignInOidc()
{
    // 从state参数解析租户ID和原请求路径
    var state = Request.Form["state"].ToString();
    var (tenantId, originalPath) = ParseStateData(state);
    var tenantAdInfo = await _tenantDbRepository.GetAdInfoByTenantId(tenantId);
    
    var authScheme = await GetOrCreateTenantAuthScheme(tenantAdInfo);
    var authResult = await HttpContext.AuthenticateAsync(authScheme.Name);
    
    if (authResult.Succeeded)
    {
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authResult.Principal);
        return Redirect(originalPath);
    }
    
    return BadRequest("认证失败");
}

之前尝试失败的原因

  • 直接调用HttpClient的方式:这是后端发起的请求,不是引导用户浏览器跳转,用户看不到登录页面,无法完成交互认证。
  • 固定Challenge方法:没有提前配置对应租户的OpenID Connect方案,默认方案缺少租户专属信息,导致跳转到错误的登录页。

内容的提问来源于stack exchange,提问作者Samir Hemzeyev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 18:20:42