mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 01:18:22 +00:00
Merge pull request #2378 from DuendeSoftware/dh/revert-host-cookie-prefix
Revert __Host- cookie prefix (PR #2373)
This commit is contained in:
commit
cc5e631a34
8 changed files with 3 additions and 618 deletions
|
|
@ -1,102 +0,0 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Duende.IdentityServer.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods to support seamless cookie name migration.
|
||||
/// </summary>
|
||||
public static class CookieNameMigrationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds middleware that transparently migrates requests using an old cookie name to a new
|
||||
/// cookie name. This enables seamless migration when cookie names change (e.g., when
|
||||
/// adopting the <c>__Host-</c> prefix in IdentityServer 8.0) without invalidating existing
|
||||
/// user sessions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Register this middleware <b>before</b> <c>app.UseIdentityServer()</c> in the pipeline.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// On each request where the old cookie is present but the new one is absent, the middleware:
|
||||
/// <list type="number">
|
||||
/// <item>Patches the incoming request so downstream auth handlers find the value under the new name.</item>
|
||||
/// <item>On the response: issues a <c>Set-Cookie</c> for the new name and expires the old one.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is a transient migration aid. Once all active sessions have been re-issued under the
|
||||
/// new cookie name the middleware can be removed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To migrate from the IdentityServer 7.x defaults to the 8.0 defaults, register twice:
|
||||
/// <code>
|
||||
/// app.MigrateIdentityServerCookieName("idsrv", "__Host-idsrv");
|
||||
/// app.MigrateIdentityServerCookieName("idsrv.external", "__Host-idsrv.external");
|
||||
/// app.UseIdentityServer();
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="app">The application builder.</param>
|
||||
/// <param name="oldCookieName">The old cookie name to migrate from.</param>
|
||||
/// <param name="newCookieName">The new cookie name to migrate to.</param>
|
||||
/// <returns>The application builder.</returns>
|
||||
public static IApplicationBuilder MigrateIdentityServerCookieName(
|
||||
this IApplicationBuilder app,
|
||||
string oldCookieName,
|
||||
string newCookieName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(oldCookieName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(newCookieName);
|
||||
|
||||
return app.Use(async (context, next) =>
|
||||
{
|
||||
var oldValue = context.Request.Cookies[oldCookieName];
|
||||
var newValue = context.Request.Cookies[newCookieName];
|
||||
|
||||
if (oldValue != null && newValue == null)
|
||||
{
|
||||
// Patch the request Cookie header so downstream auth handlers
|
||||
// find the encrypted value under the new cookie name.
|
||||
var existingHeader = context.Request.Headers.Cookie.ToString();
|
||||
var newCookiePair = $"{newCookieName}={oldValue}";
|
||||
context.Request.Headers.Cookie = string.IsNullOrWhiteSpace(existingHeader)
|
||||
? newCookiePair
|
||||
: existingHeader + "; " + newCookiePair;
|
||||
|
||||
// Once the response starts, re-issue the cookie under the new name
|
||||
// and expire the old one so the migration happens transparently.
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
var isHostPrefixed = newCookieName.StartsWith("__Host-", StringComparison.Ordinal);
|
||||
var idsrvOptions = context.RequestServices.GetRequiredService<IdentityServerOptions>();
|
||||
|
||||
context.Response.Cookies.Append(newCookieName, oldValue, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = isHostPrefixed || context.Request.IsHttps,
|
||||
Path = "/",
|
||||
IsEssential = true,
|
||||
SameSite = idsrvOptions.Authentication.CookieSameSiteMode,
|
||||
Domain = isHostPrefixed ? null : default
|
||||
});
|
||||
|
||||
context.Response.Cookies.Delete(oldCookieName, new CookieOptions
|
||||
{
|
||||
Path = "/",
|
||||
Domain = null
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
await next(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Stores.Default;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -27,10 +26,9 @@ internal class ConfigureInternalCookieOptions : IConfigureNamedOptions<CookieAut
|
|||
{
|
||||
options.SlidingExpiration = _idsrv.Authentication.CookieSlidingExpiration;
|
||||
options.ExpireTimeSpan = _idsrv.Authentication.CookieLifetime;
|
||||
options.Cookie.Name = _idsrv.Authentication.CookieName;
|
||||
options.Cookie.Name = IdentityServerConstants.DefaultCookieAuthenticationScheme;
|
||||
options.Cookie.IsEssential = true;
|
||||
options.Cookie.SameSite = _idsrv.Authentication.CookieSameSiteMode;
|
||||
EnforceHostPrefixRequirements(options.Cookie);
|
||||
|
||||
options.LoginPath = ExtractLocalUrl(_idsrv.UserInteraction.LoginUrl);
|
||||
options.LogoutPath = ExtractLocalUrl(_idsrv.UserInteraction.LogoutUrl);
|
||||
|
|
@ -44,7 +42,7 @@ internal class ConfigureInternalCookieOptions : IConfigureNamedOptions<CookieAut
|
|||
|
||||
if (name == IdentityServerConstants.ExternalCookieAuthenticationScheme)
|
||||
{
|
||||
options.Cookie.Name = _idsrv.Authentication.ExternalCookieName;
|
||||
options.Cookie.Name = IdentityServerConstants.ExternalCookieAuthenticationScheme;
|
||||
options.Cookie.IsEssential = true;
|
||||
// https://github.com/IdentityServer/IdentityServer4/issues/2595
|
||||
// need to set None because iOS 12 safari considers the POST back to the client from the
|
||||
|
|
@ -53,17 +51,6 @@ internal class ConfigureInternalCookieOptions : IConfigureNamedOptions<CookieAut
|
|||
// hold onto them and send on the next redirect to the callback page.
|
||||
// see: https://brockallen.com/2019/01/11/same-site-cookies-asp-net-core-and-external-authentication-providers/
|
||||
options.Cookie.SameSite = _idsrv.Authentication.CookieSameSiteMode;
|
||||
EnforceHostPrefixRequirements(options.Cookie);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnforceHostPrefixRequirements(CookieBuilder cookie)
|
||||
{
|
||||
if (cookie.Name?.StartsWith("__Host-", StringComparison.Ordinal) == true)
|
||||
{
|
||||
cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
cookie.Path = "/";
|
||||
cookie.Domain = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,22 +34,6 @@ public class AuthenticationOptions
|
|||
/// </summary>
|
||||
public SameSiteMode CookieSameSiteMode { get; set; } = SameSiteMode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the IdentityServer authentication cookie.
|
||||
/// Defaults to "__Host-idsrv". The "__Host-" prefix enforces that the cookie
|
||||
/// is only sent over HTTPS, with Path=/ and no Domain attribute.
|
||||
/// Set to "idsrv" to use the legacy cookie name when upgrading from a previous version.
|
||||
/// </summary>
|
||||
public string CookieName { get; set; } = "__Host-idsrv";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the IdentityServer external/temporary authentication cookie.
|
||||
/// Defaults to "__Host-idsrv.external". The "__Host-" prefix enforces that the cookie
|
||||
/// is only sent over HTTPS, with Path=/ and no Domain attribute.
|
||||
/// Set to "idsrv.external" to use the legacy cookie name when upgrading from a previous version.
|
||||
/// </summary>
|
||||
public string ExternalCookieName { get; set; } = "__Host-idsrv.external";
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if user must be authenticated to accept parameters to end session endpoint. Defaults to false.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -211,16 +211,6 @@ public static class IdentityServerApplicationBuilderExtensions
|
|||
throw new InvalidOperationException("CheckSessionCookieName is not configured");
|
||||
}
|
||||
|
||||
if (options.Authentication.CookieName.IsMissing())
|
||||
{
|
||||
throw new InvalidOperationException("CookieName is not configured");
|
||||
}
|
||||
|
||||
if (options.Authentication.ExternalCookieName.IsMissing())
|
||||
{
|
||||
throw new InvalidOperationException("ExternalCookieName is not configured");
|
||||
}
|
||||
|
||||
if (options.Cors.CorsPolicyName.IsMissing())
|
||||
{
|
||||
throw new InvalidOperationException("CorsPolicyName is not configured");
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ public class IdentityServerPipeline
|
|||
BrowserClient.AllowAutoRedirect = old;
|
||||
}
|
||||
|
||||
public void RemoveLoginCookie() => BrowserClient.RemoveCookie(BaseUrl, Options.Authentication.CookieName);
|
||||
public void RemoveLoginCookie() => BrowserClient.RemoveCookie(BaseUrl, IdentityServerConstants.DefaultCookieAuthenticationScheme);
|
||||
public void RemoveSessionCookie() => BrowserClient.RemoveCookie(BaseUrl, IdentityServerConstants.DefaultCheckSessionCookieName);
|
||||
public Cookie GetSessionCookie() => BrowserClient.GetCookie(BaseUrl, IdentityServerConstants.DefaultCheckSessionCookieName);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,300 +0,0 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace UnitTests.Configuration;
|
||||
|
||||
public class CookieNameMigrationExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a request through the migration middleware and returns:
|
||||
/// - The cookie values visible to the downstream handler (captured during the request)
|
||||
/// - The Set-Cookie response headers
|
||||
/// </summary>
|
||||
private static async Task<(IRequestCookieCollection downstreamCookies, IHeaderDictionary responseHeaders)> InvokeMiddleware(
|
||||
string oldCookieName,
|
||||
string newCookieName,
|
||||
string[] requestCookies,
|
||||
IdentityServerOptions idsrvOptions = null)
|
||||
{
|
||||
IRequestCookieCollection capturedCookies = null;
|
||||
|
||||
using var host = await new HostBuilder()
|
||||
.ConfigureWebHost(webHost =>
|
||||
{
|
||||
webHost.UseTestServer();
|
||||
webHost.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton(idsrvOptions ?? new IdentityServerOptions());
|
||||
});
|
||||
webHost.Configure(app =>
|
||||
{
|
||||
app.MigrateIdentityServerCookieName(oldCookieName, newCookieName);
|
||||
app.Run(ctx =>
|
||||
{
|
||||
// Capture cookie values while the HttpContext is still alive
|
||||
capturedCookies = ctx.Request.Cookies;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
})
|
||||
.StartAsync();
|
||||
|
||||
var testServer = host.GetTestServer();
|
||||
var context = await testServer.SendAsync(ctx =>
|
||||
{
|
||||
ctx.Request.Headers.Cookie = string.Join("; ", requestCookies);
|
||||
});
|
||||
|
||||
return (capturedCookies, context.Response.Headers);
|
||||
}
|
||||
|
||||
// --- Old cookie migrated to new name when only old is present ---
|
||||
|
||||
[Fact]
|
||||
public async Task when_old_cookie_present_and_new_absent_request_is_patched_with_new_name()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
var (downstreamCookies, _) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={cookieValue}"]);
|
||||
|
||||
// The downstream handler should see the value under the new cookie name
|
||||
downstreamCookies[newName].ShouldBe(cookieValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task when_old_cookie_present_and_new_absent_response_sets_new_cookie()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={cookieValue}"]);
|
||||
|
||||
var setCookieHeaders = responseHeaders["Set-Cookie"].ToList();
|
||||
setCookieHeaders.ShouldContain(h => h.StartsWith(newName + "="));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task when_old_cookie_present_and_new_absent_response_expires_old_cookie()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={cookieValue}"]);
|
||||
|
||||
var setCookieHeaders = responseHeaders["Set-Cookie"].ToList();
|
||||
// Old cookie should be deleted (expires=epoch / max-age=0)
|
||||
setCookieHeaders.ShouldContain(h => h.StartsWith(oldName + "=") && h.Contains("expires="));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task host_prefixed_new_cookie_has_secure_attribute()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={cookieValue}"]);
|
||||
|
||||
var newCookieHeader = responseHeaders["Set-Cookie"]
|
||||
.FirstOrDefault(h => h.StartsWith(newName + "="));
|
||||
|
||||
newCookieHeader.ShouldNotBeNull();
|
||||
newCookieHeader.ShouldContain("secure", Case.Insensitive);
|
||||
}
|
||||
|
||||
// --- When both cookies are present, no migration occurs ---
|
||||
|
||||
[Fact]
|
||||
public async Task when_both_old_and_new_cookies_present_new_cookie_value_is_not_overwritten()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string oldValue = "old-encrypted-value";
|
||||
const string newValue = "new-encrypted-value";
|
||||
|
||||
var (downstreamCookies, _) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={oldValue}", $"{newName}={newValue}"]);
|
||||
|
||||
// New cookie should remain unchanged
|
||||
downstreamCookies[newName].ShouldBe(newValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task when_both_cookies_present_no_set_cookie_headers_are_emitted()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}=old-value", $"{newName}=new-value"]);
|
||||
|
||||
responseHeaders["Set-Cookie"].Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
// --- When neither cookie is present, nothing happens ---
|
||||
|
||||
[Fact]
|
||||
public async Task when_neither_cookie_present_downstream_sees_no_cookies()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
|
||||
var (downstreamCookies, _) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[]);
|
||||
|
||||
downstreamCookies[oldName].ShouldBeNull();
|
||||
downstreamCookies[newName].ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task when_neither_cookie_present_no_set_cookie_headers_are_emitted()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[]);
|
||||
|
||||
responseHeaders["Set-Cookie"].Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
// --- Argument validation ---
|
||||
|
||||
[Fact]
|
||||
public void null_old_cookie_name_throws_argument_exception()
|
||||
{
|
||||
var app = new ApplicationBuilder(null!);
|
||||
Should.Throw<ArgumentException>(() => app.MigrateIdentityServerCookieName(null!, "__Host-idsrv"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void null_new_cookie_name_throws_argument_exception()
|
||||
{
|
||||
var app = new ApplicationBuilder(null!);
|
||||
Should.Throw<ArgumentException>(() => app.MigrateIdentityServerCookieName("idsrv", null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void empty_old_cookie_name_throws_argument_exception()
|
||||
{
|
||||
var app = new ApplicationBuilder(null!);
|
||||
Should.Throw<ArgumentException>(() => app.MigrateIdentityServerCookieName("", "__Host-idsrv"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void empty_new_cookie_name_throws_argument_exception()
|
||||
{
|
||||
var app = new ApplicationBuilder(null!);
|
||||
Should.Throw<ArgumentException>(() => app.MigrateIdentityServerCookieName("idsrv", ""));
|
||||
}
|
||||
|
||||
// --- Patched Cookie header does not start with "; " when only old cookie is present ---
|
||||
|
||||
[Fact]
|
||||
public async Task when_only_old_cookie_present_patched_header_does_not_start_with_semicolon()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
string patchedHeader = null;
|
||||
|
||||
using var host = await new HostBuilder()
|
||||
.ConfigureWebHost(webHost =>
|
||||
{
|
||||
webHost.UseTestServer();
|
||||
webHost.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton(new IdentityServerOptions());
|
||||
});
|
||||
webHost.Configure(app =>
|
||||
{
|
||||
app.MigrateIdentityServerCookieName(oldName, newName);
|
||||
app.Run(ctx =>
|
||||
{
|
||||
patchedHeader = ctx.Request.Headers.Cookie.ToString();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
})
|
||||
.StartAsync();
|
||||
|
||||
await host.GetTestServer().SendAsync(ctx =>
|
||||
{
|
||||
ctx.Request.Headers.Cookie = $"{oldName}={cookieValue}";
|
||||
});
|
||||
|
||||
patchedHeader.ShouldNotBeNull();
|
||||
patchedHeader.ShouldNotStartWith("; ");
|
||||
patchedHeader.ShouldContain($"{newName}={cookieValue}");
|
||||
}
|
||||
|
||||
// --- SameSite mode is taken from IdentityServerOptions ---
|
||||
|
||||
[Fact]
|
||||
public async Task same_site_mode_from_identity_server_options_is_applied_to_migrated_cookie()
|
||||
{
|
||||
const string oldName = "idsrv";
|
||||
const string newName = "__Host-idsrv";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
var options = new IdentityServerOptions();
|
||||
options.Authentication.CookieSameSiteMode = SameSiteMode.Strict;
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={cookieValue}"],
|
||||
options);
|
||||
|
||||
var newCookieHeader = responseHeaders["Set-Cookie"]
|
||||
.FirstOrDefault(h => h.StartsWith(newName + "="));
|
||||
|
||||
newCookieHeader.ShouldNotBeNull();
|
||||
newCookieHeader.ShouldContain("samesite=strict", Case.Insensitive);
|
||||
}
|
||||
|
||||
// --- __Host- prefix check is case-sensitive per RFC 6265bis ---
|
||||
|
||||
[Fact]
|
||||
public async Task wrong_case_host_prefix_does_not_set_secure_attribute()
|
||||
{
|
||||
// "__host-idsrv" (lowercase) must not trigger host-prefix enforcement per RFC 6265bis.
|
||||
const string oldName = "idsrv-old";
|
||||
const string newName = "__host-idsrv-new";
|
||||
const string cookieValue = "encrypted-ticket-value";
|
||||
|
||||
var (_, responseHeaders) = await InvokeMiddleware(
|
||||
oldName, newName,
|
||||
[$"{oldName}={cookieValue}"]);
|
||||
|
||||
var newCookieHeader = responseHeaders["Set-Cookie"]
|
||||
.FirstOrDefault(h => h.StartsWith(newName + "="));
|
||||
|
||||
newCookieHeader.ShouldNotBeNull();
|
||||
newCookieHeader.ShouldNotContain("secure", Case.Insensitive);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace UnitTests.Configuration.DependencyInjection;
|
||||
|
||||
public class ConfigureInternalCookieOptionsTests
|
||||
{
|
||||
private static CookieAuthenticationOptions ConfigureMainCookie(IdentityServerOptions idsrvOptions)
|
||||
{
|
||||
var sut = new ConfigureInternalCookieOptions(idsrvOptions);
|
||||
var cookieOptions = new CookieAuthenticationOptions();
|
||||
sut.Configure(IdentityServerConstants.DefaultCookieAuthenticationScheme, cookieOptions);
|
||||
return cookieOptions;
|
||||
}
|
||||
|
||||
private static CookieAuthenticationOptions ConfigureExternalCookie(IdentityServerOptions idsrvOptions)
|
||||
{
|
||||
var sut = new ConfigureInternalCookieOptions(idsrvOptions);
|
||||
var cookieOptions = new CookieAuthenticationOptions();
|
||||
sut.Configure(IdentityServerConstants.ExternalCookieAuthenticationScheme, cookieOptions);
|
||||
return cookieOptions;
|
||||
}
|
||||
|
||||
// --- Default cookie names ---
|
||||
|
||||
[Fact]
|
||||
public void main_cookie_name_defaults_to_host_prefixed_idsrv()
|
||||
{
|
||||
var options = ConfigureMainCookie(new IdentityServerOptions());
|
||||
options.Cookie.Name.ShouldBe("__Host-idsrv");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void external_cookie_name_defaults_to_host_prefixed_idsrv_external()
|
||||
{
|
||||
var options = ConfigureExternalCookie(new IdentityServerOptions());
|
||||
options.Cookie.Name.ShouldBe("__Host-idsrv.external");
|
||||
}
|
||||
|
||||
// --- Custom cookie names are applied ---
|
||||
|
||||
[Fact]
|
||||
public void custom_main_cookie_name_is_applied()
|
||||
{
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.CookieName = "my-custom-cookie";
|
||||
|
||||
var options = ConfigureMainCookie(idsrvOptions);
|
||||
|
||||
options.Cookie.Name.ShouldBe("my-custom-cookie");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void custom_external_cookie_name_is_applied()
|
||||
{
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.ExternalCookieName = "my-custom-external-cookie";
|
||||
|
||||
var options = ConfigureExternalCookie(idsrvOptions);
|
||||
|
||||
options.Cookie.Name.ShouldBe("my-custom-external-cookie");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void legacy_main_cookie_name_can_be_restored()
|
||||
{
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.CookieName = "idsrv";
|
||||
|
||||
var options = ConfigureMainCookie(idsrvOptions);
|
||||
|
||||
options.Cookie.Name.ShouldBe("idsrv");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void legacy_external_cookie_name_can_be_restored()
|
||||
{
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.ExternalCookieName = "idsrv.external";
|
||||
|
||||
var options = ConfigureExternalCookie(idsrvOptions);
|
||||
|
||||
options.Cookie.Name.ShouldBe("idsrv.external");
|
||||
}
|
||||
|
||||
// --- __Host- prefix enforces SecurePolicy, Path, and no Domain ---
|
||||
|
||||
[Fact]
|
||||
public void host_prefixed_main_cookie_sets_secure_policy_to_always()
|
||||
{
|
||||
var options = ConfigureMainCookie(new IdentityServerOptions());
|
||||
options.Cookie.SecurePolicy.ShouldBe(CookieSecurePolicy.Always);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void host_prefixed_main_cookie_sets_path_to_root()
|
||||
{
|
||||
var options = ConfigureMainCookie(new IdentityServerOptions());
|
||||
options.Cookie.Path.ShouldBe("/");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void host_prefixed_main_cookie_clears_domain()
|
||||
{
|
||||
var options = ConfigureMainCookie(new IdentityServerOptions());
|
||||
options.Cookie.Domain.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void host_prefixed_external_cookie_sets_secure_policy_to_always()
|
||||
{
|
||||
var options = ConfigureExternalCookie(new IdentityServerOptions());
|
||||
options.Cookie.SecurePolicy.ShouldBe(CookieSecurePolicy.Always);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void host_prefixed_external_cookie_sets_path_to_root()
|
||||
{
|
||||
var options = ConfigureExternalCookie(new IdentityServerOptions());
|
||||
options.Cookie.Path.ShouldBe("/");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void host_prefixed_external_cookie_clears_domain()
|
||||
{
|
||||
var options = ConfigureExternalCookie(new IdentityServerOptions());
|
||||
options.Cookie.Domain.ShouldBeNull();
|
||||
}
|
||||
|
||||
// --- Non-__Host- names do NOT override SecurePolicy/Path/Domain ---
|
||||
|
||||
[Fact]
|
||||
public void non_host_prefixed_main_cookie_does_not_force_secure_policy()
|
||||
{
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.CookieName = "idsrv";
|
||||
|
||||
var options = ConfigureMainCookie(idsrvOptions);
|
||||
|
||||
// Default CookieSecurePolicy is SameAsRequest, not Always
|
||||
options.Cookie.SecurePolicy.ShouldBe(CookieSecurePolicy.SameAsRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void non_host_prefixed_external_cookie_does_not_force_secure_policy()
|
||||
{
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.ExternalCookieName = "idsrv.external";
|
||||
|
||||
var options = ConfigureExternalCookie(idsrvOptions);
|
||||
|
||||
options.Cookie.SecurePolicy.ShouldBe(CookieSecurePolicy.SameAsRequest);
|
||||
}
|
||||
|
||||
// --- __Host- prefix check is case-sensitive per RFC 6265bis ---
|
||||
|
||||
[Fact]
|
||||
public void wrong_case_host_prefix_does_not_enforce_secure_policy()
|
||||
{
|
||||
// "__host-idsrv" (lowercase) must not be treated as host-prefixed per RFC 6265bis.
|
||||
var idsrvOptions = new IdentityServerOptions();
|
||||
idsrvOptions.Authentication.CookieName = "__host-idsrv";
|
||||
|
||||
var options = ConfigureMainCookie(idsrvOptions);
|
||||
|
||||
options.Cookie.SecurePolicy.ShouldNotBe(CookieSecurePolicy.Always);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue