【问题标题】:OIDC correlation failed in Microsoft Teams authentication popup (no problems in browser)Microsoft Teams 身份验证弹出窗口中的 OIDC 关联失败(浏览器中没有问题)
【发布时间】:2025-12-25 06:10:06
【问题描述】:

使用带有 .NET Core 3.1 的 ASP.NET Core。
Microsoft.AspNetCore.Authentication.OpenIdConnect 处理的 OIDC 身份验证流程。
在我开始收到错误后,我实际上已将上述命名空间包含到我的项目中,这样我就可以轻松设置断点和检查数据。

根据本文档:https://developer.microsoft.com/en-us/office/blogs/authentication-in-microsoft-teams-apps-tabs/ 我想要实现的目标应该是可能的。

假设我们在 Microsoft Teams 中配置了一个选项卡,该选项卡托管在我们的 ASP.NET Core MVC 应用程序中,地址为 https://localhost:60151(不是通过 IIS Express,而是自托管)。 MS Teams 应用程序可以使用 ngrok 访问我们的应用程序,它是使用命令行启动的:

./ngrok http https://localhost:60151

这个应用程序有一个 TabController 定义如下:

public class TabController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    [Authorize]
    public IActionResult TabAuthStart()
    {
        return RedirectToAction(nameof(TabAuthEnd), new { serializedClaims = string.Join("; ", User.Claims.Select(x => $"{x.Type}: {x.Value}")) });
    }

    // for simplicity, let's assume no one navigates to this action 
    // except when redirected from TabAuthStart after the authentication flow completes
    public IActionResult TabAuthEnd(string serializedClaims)
    {
        return View(model: serializedClaims);
    }
}

让索引视图定义如下:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>MS Teams Tab</title>
    <script src="https://statics.teams.microsoft.com/sdk/v1.4.2/js/MicrosoftTeams.min.js" crossorigin="anonymous"></script>
    <script>

        // Call the initialize API first
        microsoftTeams.initialize();

        function authenticate() {
            microsoftTeams.authentication.authenticate({
                url: window.location.origin + "/tab/tabauthstart",
                successCallback: function (result) {
                    // do something on success
                },
                failureCallback: function (reason) {
                    // do something on failure
                }
            });
        }

    </script>
</head>
<body>
    @if (!User.Identity.IsAuthenticated)
    {
        <button onclick="authenticate()">authenticate</button>
    }
    else
    {
        <p>Hello, @User.Identity.Name</p>
    }
</body>
</html>

当重定向到 /tab/tabauthstart 时,[Authorize] 属性将确保 OIDC 质询处理程序将接收请求并重定向到配置的 IdentityServer 授权页面。

说到 OIDC 处理器,它在 Startup.cs 中是这样配置的:

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
    options.Cookie.Name = "mvchybridautorefresh";
})
.AddOpenIdConnect(options =>
{
    options.Authority = "https://localhost:44333/"; // The local IdentityServer instance
    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ClientId = "msteams";
    options.ResponseType = "code id_token"; // Hybrid flow
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("offline_access");

    options.ClaimActions.MapAllExcept("iss", "nbf", "exp", "aud", "nonce", "iat", "c_hash");

    options.GetClaimsFromUserInfoEndpoint = true;
    options.SaveTokens = true;

    // The following were added in despair. However, they don't have any effect on the process.
    options.CorrelationCookie.Path = null;
    options.CorrelationCookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
    options.CorrelationCookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
    options.CorrelationCookie.HttpOnly = false;
});

然后我们有一个像这样的Configure 方法:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseDefaultFiles();
    app.UseStaticFiles();

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}"
        );
    });
}

在 IdentityServer 中,假设客户端配置正确。
因此,当我们启动我们的应用程序并转到 Microsoft Teams 应用程序中的一个选项卡时,我们会看到一个显示“身份验证”的按钮。单击该按钮会触发 OIDC 质询处理程序,该处理程序准备身份验证属性,将 nonce 和相关 cookie 写入 Response.Cookies 集合。

生成关联Id后,我们有如下Request参数:

  • 方案:https
  • 主机:[assigned-subdomain].ngrok.io
  • 路径:/tab/tabauthstart

Set-Cookie 响应标头包含以下内容:

.AspNetCore.OpenIdConnect.Nonce.blabla; expires=Tue, 21 Jan 2020 20:54:28 GMT; path=/signin-oidc; secure; samesite=none; httponly,
.AspNetCore.Correlation.OpenIdConnect.blabla; expires=Tue, 21 Jan 2020 20:58:57 GMT; path=/signin-oidc; secure; samesite=none

完成后,我们将被重定向到 IdSrv 登录页面。

我们在此处输入登录详细信息并完成登录过程,这会将我们带回 OIDC 处理程序,然后检查相关 cookie 是否存在。但是,关联 cookie 不存在,因此会引发异常,提示“关联失败”。

这些是验证相关性之前的请求参数:

  • 方案:https
  • 主机:[assigned-subdomain].ngrok.io
  • 路径:/signin-oidc

cookies 集合为空。为什么?

为了让事情变得更有趣,当我们打开浏览器时,导航到 https://[assigned-subdomain].ngrok.io/tab/index 并通过单击按钮开始身份验证,该过程成功完成,我们最终被重定向到 /tab/tabAuthEnd,顺便说一下,它的视图如下所示:

@model string
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Authentication successful</title>
    <script src="https://statics.teams.microsoft.com/sdk/v1.4.2/js/MicrosoftTeams.min.js" crossorigin="anonymous"></script>
    <script>

        // Call the initialize API first
        microsoftTeams.initialize();
        microsoftTeams.authentication.notifySuccess(@Model);

    </script>
</head>
<body>

    <p>Redirecting back..</p>

</body>
</html>

所以...任何线索为什么在重定向到 IdSrv 登录页面时没有保存 OIDC cookie?

【问题讨论】:

  • 为铬引入了新的samesite policy。我建议你看看这个sample code 这可以帮助你实现身份验证选项卡。
  • @Trinetra-MSFT 如果您仔细查看上述 Startup.cs 中的 OIDC 配置,您会发现 CorrelationCookie 明确配置了 SameSite.NoneSecure。此外,您可以看到 Set-Cookie 响应标头具有 secure; samesite=none; 用于关联和随机数 cookie。我错过了你说的什么吗?此外,您链接到的示例不处理基于 Web 的身份验证,而是基于机器人框架。该链接对我有什么帮助?抱歉,如果我遗漏了一些明显的东西。

标签: asp.net-core openid-connect microsoft-teams


【解决方案1】:

您将看到 Set-Cookie 响应标头以“secure;samesite=none;”结尾并且 Teams 基于不允许这样做的 Chrome 版本,并且没有存储任何 cookie,从而导致此问题。

您还将看到将 SameSite 设置为 Lax 或 Strict 不会更改 Set-Cookie 标头。您必须像这样在 Startup 类(在 aspnetcore 中)中管理它:

private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        // TODO: Use your User Agent library of choice here.
        if (/* UserAgent doesn’t support new behavior */)
        {
               // For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1)
               options.SameSite = SameSiteMode.Unspecified;
         }
    }
}

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
        options.OnAppendCookie = cookieContext => 
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext => 
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });
}

public void Configure(IApplicationBuilder app)
{
    app.UseCookiePolicy(); // Before UseAuthentication or anything else that writes cookies.
    app.UseAuthentication();
    // …
}

这是检查: if (/* UserAgent 不支持新行为 */)

...您检查 User-Agent 标头。像 fon 实例,如果它包含“团队”或更具体。

Microsoft Teams Teams 由该用户代理识别: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.3.00.362 Chrome/66.0.3359.181 Electron/3.1.13 Safari/537.36

源代码位于此处: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/

【讨论】:

    【解决方案2】:

    CookiePolicyOptions.Secure默认设置为CookieSecurePolicy.SameAsRequest,但只有CookiePolicyOptions.Secure设置为CookieSecurePolicy.Always时浏览器才会传输cookies。

    【讨论】: