Using ASP.NET Core w/ .NET Core 3.1.
OIDC authentication flow handled by Microsoft.AspNetCore.Authentication.OpenIdConnect
.
After I started getting the error, I have actually included the above namespace into my project so I can set breakpoints and inspect data easily.
According to this document: https://developer.microsoft.com/en-us/office/blogs/authentication-in-microsoft-teams-apps-tabs/ what I'm trying to achieve should be possible.
Let's say we have configured a tab in Microsoft Teams which is hosted in our ASP.NET Core MVC application at https://localhost:60151
(not via IIS Express, but self-hosted). The MS Teams application can access our application using ngrok, which is started using the command line:
./ngrok http https://localhost:60151
This application has a TabController defined like this:
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);
}
}
Let the index view be defined like this:
@{
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>
When redirected to /tab/tabauthstart, the [Authorize]
attribute will make sure the OIDC challenge handler will pick up the request and redirect to the configured IdentityServer authorize page.
Speaking of OIDC handler, it is configured in Startup.cs like this:
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;
});
and then we have a Configure
method like this:
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?}"
);
});
}
In IdentityServer, assume the client is configured correctly.
So when we start our application and go to a tab in Microsoft Teams app, we see a button saying "authenticate". Click of that button triggers the OIDC challenge handler which prepares the authentication properties, writes nonce and correlation cookies to the Response.Cookies collection.
After generating the correlation Id, we have the following Request parameters:
- Scheme: https
- Host: [assigned-subdomain].ngrok.io
- Path: /tab/tabauthstart
The Set-Cookie
response header contains the following:
.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
After that is done, we are redirected to the IdSrv sign in page.
There we input our sign in details and finish the sign in process, which brings us back to our OIDC handler, which then checks for the existence of the correlation cookie. However, the correlation cookie doesn't exist and thus, the exception is thrown saying "Correlation failed".
These are the request parameters just before the correlation is validated:
- Scheme: https
- Host: [assigned-subdomain].ngrok.io
- Path: /signin-oidc
The cookies collection is empty. Why?
To make things even more interesting, when we open a browser, navigate to https://[assigned-subdomain].ngrok.io/tab/index and start the authentication by clicking the button, the process completes successfully and we are finally redirected to /tab/tabAuthEnd, whose view, by the way, looks like this:
@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>
So... any clue why the OIDC cookies are not saved when redirecting to the IdSrv login page?