Microsoft Teams 身份验证弹出窗口中的 OIDC 关联失败(在浏览器中没有问题)
OIDC correlation failed in Microsoft Teams authentication popup (no problems in browser)
使用 ASP.NET Core w/ .NET Core 3.1。
Microsoft.AspNetCore.Authentication.OpenIdConnect
.
处理的 OIDC 身份验证流程
在我开始出现错误之后,我实际上已经将上述命名空间包含到我的项目中,因此我可以轻松地设置断点和检查数据。
根据此文档:https://developer.microsoft.com/en-us/office/blogs/authentication-in-microsoft-teams-apps-tabs/我正在尝试实现的目标应该是可能的。
假设我们在 Microsoft Teams 中配置了一个选项卡,该选项卡托管在 ASP.NET 核心 MVC 应用程序中 https://localhost:60151
(不是通过 IIS Express,而是 self-hosted)。 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 应用程序中的选项卡时,我们会看到一个显示 "authenticate" 的按钮。单击该按钮会触发准备身份验证属性的 OIDC 质询处理程序,将随机数和相关性 cookie 写入 Response.Cookies collection.
生成相关Id后,我们有以下Request参数:
- 方案:https
- 主持人:[assigned-subdomain].ngrok.io
- 路径:/tab/tabauthstart
Set-Cookie
响应 header 包含以下内容:
.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 不存在,因此抛出异常 "Correlation failed".
这些是验证关联之前的请求参数:
- 方案:https
- 主持人:[assigned-subdomain].ngrok.io
- 路径:/signin-oidc
cookies collection 为空。为什么?
为了让事情更有趣,当我们打开浏览器时,导航到 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 的任何线索?
您会看到 Set-Cookie 响应 header 以 "secure; samesite=none;" 结尾,Teams 基于 Chrome 版本,不允许这样做,并且没有 cookie存储,导致这个问题。
您还将看到将 SameSite 设置为 Lax 或 Strict 不会更改 Set-Cookie header。您必须在 Startup class(在 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 header。如果它包含 "Teams" 或更具体,则类似于 fon 实例。
Microsoft Teams Teams 以此 User-Agent 标识:
Mozilla/5.0(Windows NT 10.0;Win64;x64)AppleWebKit/537.36(KHTML,如 Gecko)Teams/1.3.00.362 Chrome/66.0.3359。 181Electron/3.1.13Safari/537.36
来源位于此处:
https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
默认情况下,CookiePolicyOptions.Secure
已设置为CookieSecurePolicy.SameAsRequest
,但只有当CookiePolicyOptions.Secure
设置为CookieSecurePolicy.Always
时,浏览器才会传输cookies。
使用 ASP.NET Core w/ .NET Core 3.1。
Microsoft.AspNetCore.Authentication.OpenIdConnect
.
处理的 OIDC 身份验证流程
在我开始出现错误之后,我实际上已经将上述命名空间包含到我的项目中,因此我可以轻松地设置断点和检查数据。
根据此文档:https://developer.microsoft.com/en-us/office/blogs/authentication-in-microsoft-teams-apps-tabs/我正在尝试实现的目标应该是可能的。
假设我们在 Microsoft Teams 中配置了一个选项卡,该选项卡托管在 ASP.NET 核心 MVC 应用程序中 https://localhost:60151
(不是通过 IIS Express,而是 self-hosted)。 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 应用程序中的选项卡时,我们会看到一个显示 "authenticate" 的按钮。单击该按钮会触发准备身份验证属性的 OIDC 质询处理程序,将随机数和相关性 cookie 写入 Response.Cookies collection.
生成相关Id后,我们有以下Request参数:
- 方案:https
- 主持人:[assigned-subdomain].ngrok.io
- 路径:/tab/tabauthstart
Set-Cookie
响应 header 包含以下内容:
.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 不存在,因此抛出异常 "Correlation failed".
这些是验证关联之前的请求参数:
- 方案:https
- 主持人:[assigned-subdomain].ngrok.io
- 路径:/signin-oidc
cookies collection 为空。为什么?
为了让事情更有趣,当我们打开浏览器时,导航到 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 的任何线索?
您会看到 Set-Cookie 响应 header 以 "secure; samesite=none;" 结尾,Teams 基于 Chrome 版本,不允许这样做,并且没有 cookie存储,导致这个问题。
您还将看到将 SameSite 设置为 Lax 或 Strict 不会更改 Set-Cookie header。您必须在 Startup class(在 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 header。如果它包含 "Teams" 或更具体,则类似于 fon 实例。
Microsoft Teams Teams 以此 User-Agent 标识: Mozilla/5.0(Windows NT 10.0;Win64;x64)AppleWebKit/537.36(KHTML,如 Gecko)Teams/1.3.00.362 Chrome/66.0.3359。 181Electron/3.1.13Safari/537.36
来源位于此处: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
默认情况下,CookiePolicyOptions.Secure
已设置为CookieSecurePolicy.SameAsRequest
,但只有当CookiePolicyOptions.Secure
设置为CookieSecurePolicy.Always
时,浏览器才会传输cookies。