mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Merge pull request #2121 from DuendeSoftware/jmdc/7.2-to-7.3
Merge fixes from 7.2.x forward to 7.3.x
This commit is contained in:
commit
b0b257570a
11 changed files with 410 additions and 67 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -228,4 +228,4 @@ artifacts
|
|||
|
||||
*.Artifacts/
|
||||
|
||||
reports
|
||||
reports
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@
|
|||
<MinVerMinimumMajorMinor>7.0</MinVerMinimumMajorMinor>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ public class LoggingOptions
|
|||
public ICollection<string> AuthorizeRequestSensitiveValuesFilter { get; set; } =
|
||||
new HashSet<string>
|
||||
{
|
||||
// Secrets and assertions may be passed to the authorize endpoint via PAR
|
||||
OidcConstants.TokenRequest.ClientSecret,
|
||||
OidcConstants.TokenRequest.ClientAssertion,
|
||||
OidcConstants.AuthorizeRequest.IdTokenHint,
|
||||
OidcConstants.AuthorizeRequest.Request
|
||||
};
|
||||
|
|
@ -58,11 +61,15 @@ public class LoggingOptions
|
|||
/// Gets or sets the collection of keys that will be used to redact sensitive values from a pushed authorization request log.
|
||||
/// </summary>
|
||||
/// <remarks>Please be aware that initializing this property could expose sensitive information in your logs.</remarks>
|
||||
/// <remarks>Note that pushed authorization parameters are eventually handled by the authorize request pipeline.
|
||||
/// In most cases, changes to this collection should also be made to <see cref="AuthorizeRequestSensitiveValuesFilter"/>
|
||||
/// </remarks>
|
||||
public ICollection<string> PushedAuthorizationSensitiveValuesFilter { get; set; } =
|
||||
new HashSet<string>
|
||||
{
|
||||
OidcConstants.TokenRequest.ClientSecret,
|
||||
OidcConstants.TokenRequest.ClientAssertion,
|
||||
OidcConstants.AuthorizeRequest.IdTokenHint,
|
||||
OidcConstants.AuthorizeRequest.Request
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,18 @@ public class MutualTlsOptions
|
|||
|
||||
/// <summary>
|
||||
/// Specifies a separate domain to run the MTLS endpoints on.
|
||||
/// If the string does not contain any dots, a subdomain is assumed - e.g. main domain: identityserver.local, MTLS domain: mtls.identityserver.local
|
||||
/// If the string contains dots, a completely separate domain is assumend, e.g. main domain: identity.app.com, MTLS domain: mtls.app.com. In this case you must set a static issuer name on the options.
|
||||
/// </summary>
|
||||
/// <remarks>If the string does not contain any dots, it is treated as a
|
||||
/// subdomain. For example, if the non-mTLS endpoints are hosted at
|
||||
/// example.com, configuring this option with the value "mtls" means that
|
||||
/// mtls is required for requests to mtls.example.com.
|
||||
///
|
||||
/// If the string contains dots, it is treated as a complete domain.
|
||||
/// mTLS will be required for requests whose host name matches the
|
||||
/// configured domain name completely, including the port number.
|
||||
/// This allows for separate domains for the mTLS and non-mTLS endpoints.
|
||||
/// For example, identity.example.com and mtls.example.com.
|
||||
/// </remarks>
|
||||
public string? DomainName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -58,17 +58,25 @@ internal class ProtectedResourceErrorHttpWriter : IHttpResponseWriter<ProtectedR
|
|||
errorDescription = "The access token expired";
|
||||
}
|
||||
|
||||
var errorString = string.Format($"error=\"{error}\"");
|
||||
if (errorDescription.IsMissing())
|
||||
var values = new List<string>
|
||||
{
|
||||
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, new StringValues(new[] { "Bearer realm=\"IdentityServer\"", errorString }));
|
||||
}
|
||||
else
|
||||
"""
|
||||
Bearer realm="IdentityServer"
|
||||
""",
|
||||
$"""
|
||||
error="{error}"
|
||||
"""
|
||||
};
|
||||
|
||||
if (!errorDescription.IsMissing())
|
||||
{
|
||||
var errorDescriptionString = string.Format($"error_description=\"{errorDescription}\"");
|
||||
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, new StringValues(new[] { "Bearer realm=\"IdentityServer\"", errorString, errorDescriptionString }));
|
||||
values.Add($"""
|
||||
error_description="{errorDescription}"
|
||||
""");
|
||||
}
|
||||
|
||||
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, string.Join(",", values));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,64 +35,116 @@ public class MutualTlsEndpointMiddleware
|
|||
_sanitizedLogger = new SanitizedLogger<MutualTlsEndpointMiddleware>(logger);
|
||||
}
|
||||
|
||||
internal enum MtlsEndpointType
|
||||
{
|
||||
None,
|
||||
SeparateDomain,
|
||||
Subdomain,
|
||||
PathBased
|
||||
}
|
||||
|
||||
internal MtlsEndpointType DetermineMtlsEndpointType(HttpContext context, out PathString? subPath)
|
||||
{
|
||||
subPath = null;
|
||||
|
||||
if (!_options.MutualTls.Enabled)
|
||||
{
|
||||
return MtlsEndpointType.None;
|
||||
}
|
||||
|
||||
if (_options.MutualTls.DomainName.IsPresent())
|
||||
{
|
||||
if (_options.MutualTls.DomainName.Contains('.'))
|
||||
{
|
||||
var requestedHost = HostString.FromUriComponent(_options.MutualTls.DomainName);
|
||||
// Separate domain
|
||||
if (RequestedHostMatches(context.Request.Host, _options.MutualTls.DomainName))
|
||||
{
|
||||
_sanitizedLogger.LogDebug("Requiring mTLS because the request's domain matches the configured mTLS domain name.");
|
||||
return MtlsEndpointType.SeparateDomain;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subdomain
|
||||
if (context.Request.Host.Host.StartsWith(_options.MutualTls.DomainName + ".", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_sanitizedLogger.LogDebug("Requiring mTLS because the request's subdomain matches the configured mTLS domain name.");
|
||||
return MtlsEndpointType.Subdomain;
|
||||
}
|
||||
}
|
||||
|
||||
_sanitizedLogger.LogDebug("Not requiring mTLS because this request's domain does not match the configured mTLS domain name.");
|
||||
return MtlsEndpointType.None;
|
||||
}
|
||||
|
||||
// Check path-based MTLS
|
||||
if (context.Request.Path.StartsWithSegments(
|
||||
ProtocolRoutePaths.MtlsPathPrefix.EnsureLeadingSlash(), out var path))
|
||||
{
|
||||
_sanitizedLogger.LogDebug("Requiring mTLS because the request's path begins with the configured mTLS path prefix.");
|
||||
subPath = path;
|
||||
return MtlsEndpointType.PathBased;
|
||||
}
|
||||
|
||||
return MtlsEndpointType.None;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Invoke(HttpContext context, IAuthenticationSchemeProvider schemes)
|
||||
{
|
||||
if (_options.MutualTls.Enabled)
|
||||
var mtlsConfigurationStyle = DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
if (mtlsConfigurationStyle != MtlsEndpointType.None)
|
||||
{
|
||||
// domain-based MTLS
|
||||
if (_options.MutualTls.DomainName.IsPresent())
|
||||
var result = await TriggerCertificateAuthentication(context);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
// separate domain
|
||||
if (_options.MutualTls.DomainName.Contains('.'))
|
||||
{
|
||||
if (context.Request.Host.Host.Equals(_options.MutualTls.DomainName,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = await TriggerCertificateAuthentication(context);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// sub-domain
|
||||
else
|
||||
{
|
||||
if (context.Request.Host.Host.StartsWith(_options.MutualTls.DomainName + ".", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = await TriggerCertificateAuthentication(context);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// path based MTLS
|
||||
else if (context.Request.Path.StartsWithSegments(ProtocolRoutePaths.MtlsPathPrefix.EnsureLeadingSlash(), out var subPath))
|
||||
|
||||
// Additional processing for path-based MTLS
|
||||
if (mtlsConfigurationStyle == MtlsEndpointType.PathBased && subPath.HasValue)
|
||||
{
|
||||
var result = await TriggerCertificateAuthentication(context);
|
||||
var path = ProtocolRoutePaths.ConnectPathPrefix + subPath.Value.ToString().EnsureLeadingSlash();
|
||||
path = path.EnsureLeadingSlash();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var path = ProtocolRoutePaths.ConnectPathPrefix + subPath.ToString().EnsureLeadingSlash();
|
||||
path = path.EnsureLeadingSlash();
|
||||
|
||||
_sanitizedLogger.LogDebug("Rewriting MTLS request from: {oldPath} to: {newPath}",
|
||||
context.Request.Path.ToString(), path);
|
||||
context.Request.Path = path;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
_sanitizedLogger.LogDebug("Rewriting MTLS request from: {oldPath} to: {newPath}",
|
||||
context.Request.Path.ToString(), path);
|
||||
context.Request.Path = path;
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
|
||||
private bool RequestedHostMatches(HostString requestHost, string configuredDomain)
|
||||
{
|
||||
// Parse the configured domain which might contain a port
|
||||
var configuredHostname = configuredDomain;
|
||||
var configuredPort = 443;
|
||||
|
||||
var colonIndex = configuredDomain.IndexOf(':');
|
||||
if (colonIndex >= 0)
|
||||
{
|
||||
configuredHostname = configuredDomain.Substring(0, colonIndex);
|
||||
if (int.TryParse(configuredDomain.Substring(colonIndex + 1), out var port))
|
||||
{
|
||||
configuredPort = port;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare hostnames (case-insensitive)
|
||||
if (!string.Equals(requestHost.Host, configuredHostname, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requestPort = requestHost.Port ?? 443;
|
||||
return requestPort == configuredPort;
|
||||
}
|
||||
|
||||
private async Task<AuthenticateResult> TriggerCertificateAuthentication(HttpContext context)
|
||||
{
|
||||
var x509AuthResult = await context.AuthenticateAsync(_options.MutualTls.ClientCertificateAuthenticationScheme);
|
||||
|
|
|
|||
|
|
@ -140,14 +140,17 @@ public class DefaultSessionCoordinationService : ISessionCoordinationService
|
|||
{
|
||||
var client = await ClientStore.FindClientByIdAsync(clientId); // i don't think we care if it's an enabled client at this point
|
||||
|
||||
var shouldCoordinate =
|
||||
client.CoordinateLifetimeWithUserSession == true ||
|
||||
(Options.Authentication.CoordinateClientLifetimesWithUserSession && client.CoordinateLifetimeWithUserSession != false);
|
||||
|
||||
if (shouldCoordinate)
|
||||
if (client != null)
|
||||
{
|
||||
// this implies they should also be contacted for backchannel logout below
|
||||
clientsToCoordinate.Add(clientId);
|
||||
var shouldCoordinate =
|
||||
client.CoordinateLifetimeWithUserSession == true ||
|
||||
(Options.Authentication.CoordinateClientLifetimesWithUserSession && client.CoordinateLifetimeWithUserSession != false);
|
||||
|
||||
if (shouldCoordinate)
|
||||
{
|
||||
// this implies they should also be contacted for backchannel logout below
|
||||
clientsToCoordinate.Add(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public class PushedAuthorizationTests
|
|||
{
|
||||
private readonly IdentityServerPipeline _mockPipeline = new();
|
||||
private Client _client;
|
||||
private string clientSecret = Guid.NewGuid().ToString();
|
||||
|
||||
public PushedAuthorizationTests()
|
||||
{
|
||||
|
|
@ -22,7 +23,7 @@ public class PushedAuthorizationTests
|
|||
ConfigureUsers();
|
||||
ConfigureScopesAndResources();
|
||||
|
||||
_mockPipeline.Initialize();
|
||||
_mockPipeline.Initialize(enableLogging: true);
|
||||
|
||||
_mockPipeline.Options.Endpoints.EnablePushedAuthorizationEndpoint = true;
|
||||
}
|
||||
|
|
@ -58,12 +59,32 @@ public class PushedAuthorizationTests
|
|||
authorization.State.ShouldBe(expectedState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task sensitive_values_should_not_be_logged_on_bad_request_to_par_endpoint()
|
||||
{
|
||||
// Login
|
||||
await _mockPipeline.LoginAsync("bob");
|
||||
_mockPipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
// Push Authorization
|
||||
var expectedCallback = _client.RedirectUris.First();
|
||||
var expectedState = "123_state";
|
||||
var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(
|
||||
clientSecret: clientSecret,
|
||||
redirectUri: "bogus", // <-- Intentionally wrong, to provoke logging an error with raw request
|
||||
state: expectedState
|
||||
);
|
||||
|
||||
_mockPipeline.MockLogger.LogMessages.ShouldContain(msg => msg.Contains("\"client_secret\": \"***REDACTED***\""));
|
||||
_mockPipeline.MockLogger.LogMessages.ShouldNotContain(msg => msg.Contains(clientSecret));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task using_pushed_authorization_when_it_is_globally_disabled_fails()
|
||||
{
|
||||
_mockPipeline.Options.Endpoints.EnablePushedAuthorizationEndpoint = false;
|
||||
|
||||
var (_, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync();
|
||||
var (_, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(clientSecret: clientSecret);
|
||||
statusCode.ShouldBe(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +134,7 @@ public class PushedAuthorizationTests
|
|||
public async Task existing_pushed_authorization_request_uris_become_invalid_when_par_is_disabled()
|
||||
{
|
||||
// PAR is enabled when we push authorization...
|
||||
var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync();
|
||||
var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(clientSecret: clientSecret);
|
||||
statusCode.ShouldBe(HttpStatusCode.Created);
|
||||
parJson.ShouldNotBeNull();
|
||||
|
||||
|
|
@ -141,7 +162,7 @@ public class PushedAuthorizationTests
|
|||
// Login
|
||||
await _mockPipeline.LoginAsync("bob");
|
||||
|
||||
var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync();
|
||||
var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(clientSecret: clientSecret); ;
|
||||
statusCode.ShouldBe(HttpStatusCode.Created);
|
||||
parJson.ShouldNotBeNull();
|
||||
|
||||
|
|
@ -274,7 +295,7 @@ public class PushedAuthorizationTests
|
|||
ClientId = "client1",
|
||||
ClientSecrets = new []
|
||||
{
|
||||
new Secret("secret".Sha256())
|
||||
new Secret(clientSecret.Sha256())
|
||||
},
|
||||
AllowedGrantTypes = GrantTypes.Implicit,
|
||||
RequireConsent = false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Endpoints.Results;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace UnitTests.Endpoints.Results;
|
||||
|
||||
public class ProtectedResourceErrorResultTests
|
||||
{
|
||||
private readonly ProtectedResourceErrorHttpWriter writer = new();
|
||||
|
||||
[Fact]
|
||||
public void WwwAuthenticate_header_with_error_and_description_should_be_a_single_line()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
writer.WriteHttpResponse(
|
||||
new ProtectedResourceErrorResult("oops", "big oops"),
|
||||
context
|
||||
);
|
||||
|
||||
var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate].ToString();
|
||||
wwwAuthHeader.ShouldBe(
|
||||
"""
|
||||
Bearer realm="IdentityServer",error="oops",error_description="big oops"
|
||||
""");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WwwAuthenticate_header_with_error_should_be_a_single_line()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
writer.WriteHttpResponse(
|
||||
new ProtectedResourceErrorResult("oops"),
|
||||
context
|
||||
);
|
||||
|
||||
var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate].ToString();
|
||||
wwwAuthHeader.ShouldBe(
|
||||
"""
|
||||
Bearer realm="IdentityServer",error="oops"
|
||||
""");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WwwAuthenticate_header_should_always_be_a_single_string_value()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
writer.WriteHttpResponse(
|
||||
new ProtectedResourceErrorResult("oops", "big oops"),
|
||||
context
|
||||
);
|
||||
|
||||
var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate];
|
||||
wwwAuthHeader.Count.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -382,6 +382,149 @@ public class MutualTlsEndpointMiddlewareTests
|
|||
context.Request.Path.ToString().ShouldStartWith("/");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
internal void mtls_endpoint_type_when_mtls_disabled_should_be_none()
|
||||
{
|
||||
_options.MutualTls.Enabled = false;
|
||||
var context = CreateContext();
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None);
|
||||
subPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("mtls.example.com", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
[InlineData("mTLS.example.com", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
[InlineData("mtls.example.com", "mTLS.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
[InlineData("mtls.example.com:443", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
[InlineData("mtls.example.com:5001", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.None)]
|
||||
[InlineData("mtls.example.com", "mtls.example.com:443", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
[InlineData("mtls.example.com:443", "mtls.example.com:443", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
[InlineData("mtls.example.com:5001", "mtls.example.com:443", MutualTlsEndpointMiddleware.MtlsEndpointType.None)]
|
||||
[InlineData("mtls.example.com", "mtls.example.com:5001", MutualTlsEndpointMiddleware.MtlsEndpointType.None)]
|
||||
[InlineData("mtls.example.com:443", "mtls.example.com:5001", MutualTlsEndpointMiddleware.MtlsEndpointType.None)]
|
||||
[InlineData("mtls.example.com:5001", "mtls.example.com:5001", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)]
|
||||
internal void mtls_endpoint_type_separate_domain_should_be_detected(string requestedHost, string configuredDomainName, MutualTlsEndpointMiddleware.MtlsEndpointType expectedType)
|
||||
{
|
||||
// Arrange
|
||||
_options.MutualTls.Enabled = true;
|
||||
_options.MutualTls.DomainName = configuredDomainName;
|
||||
var context = CreateContext();
|
||||
context.Request.Host = new HostString(requestedHost);
|
||||
|
||||
// Act
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(expectedType);
|
||||
subPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("example.com", "mtls.example.com")]
|
||||
[InlineData("example.com:443", "mtls.example.com")]
|
||||
[InlineData("example.com:5001", "mtls.example.com")]
|
||||
[InlineData("other.example.com", "mtls.example.com")]
|
||||
[InlineData("other.example.com:443", "mtls.example.com")]
|
||||
[InlineData("example.com", "mtls.example.com:443")]
|
||||
[InlineData("example.com:443", "mtls.example.com:443")]
|
||||
[InlineData("other.example.com", "mtls.example.com:5001")]
|
||||
[InlineData("other.example.com:5001", "mtls.example.com:5001")]
|
||||
internal void mtls_endpoint_type_separate_domain_should_not_match_different_domain(string requestedHost, string configuredDomainName)
|
||||
{
|
||||
// Arrange
|
||||
_options.MutualTls.Enabled = true;
|
||||
_options.MutualTls.DomainName = configuredDomainName;
|
||||
var context = CreateContext();
|
||||
context.Request.Host = new HostString(requestedHost);
|
||||
|
||||
// Act
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None);
|
||||
subPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("mtls.example.com", "mtls")]
|
||||
[InlineData("mtls.example.com", "mTLS")]
|
||||
[InlineData("mTLS.example.com", "mtls")]
|
||||
[InlineData("mtls.example.com:443", "mtls")]
|
||||
[InlineData("mtls.example.com:5001", "mtls")]
|
||||
internal void mtls_endpoint_type_subdomain_should_be_detected(string requestedHost, string configuredDomainName)
|
||||
{
|
||||
// Arrange
|
||||
_options.MutualTls.Enabled = true;
|
||||
_options.MutualTls.DomainName = configuredDomainName;
|
||||
var context = CreateContext();
|
||||
context.Request.Host = new HostString(requestedHost);
|
||||
|
||||
// Act
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.Subdomain);
|
||||
subPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("api.example.com", "mtls")]
|
||||
[InlineData("api.example.com:443", "mtls")]
|
||||
[InlineData("example.com", "mtls")]
|
||||
[InlineData("example.com:5001", "mtls")]
|
||||
internal void mtls_endpoint_type_subdomain_should_not_match_different_subdomain(string requestedHost, string configuredDomainName)
|
||||
{
|
||||
// Arrange
|
||||
_options.MutualTls.Enabled = true;
|
||||
_options.MutualTls.DomainName = configuredDomainName;
|
||||
var context = CreateContext();
|
||||
context.Request.Host = new HostString(requestedHost);
|
||||
|
||||
// Act
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None);
|
||||
subPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/connect/mtls/token")]
|
||||
[InlineData("/connect/mTLS/token")]
|
||||
internal void mtls_endpoint_type_path_based_should_be_detected(string requestedPath)
|
||||
{
|
||||
// Arrange
|
||||
_options.MutualTls.Enabled = true;
|
||||
var context = CreateContext();
|
||||
context.Request.Path = new PathString(requestedPath);
|
||||
|
||||
// Act
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.PathBased);
|
||||
subPath.Value.ToString().ShouldBe("/token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
internal void mtls_endpoint_type_should_be_none_when_enabled_but_no_matching_configuration()
|
||||
{
|
||||
// Arrange
|
||||
_options.MutualTls.Enabled = true;
|
||||
_options.MutualTls.DomainName = "mtls.example.com";
|
||||
var context = CreateContext();
|
||||
context.Request.Host = new HostString("regular.example.com");
|
||||
context.Request.Path = new PathString("/connect/token");
|
||||
|
||||
// Act
|
||||
var result = _subject.DetermineMtlsEndpointType(context, out var subPath);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None);
|
||||
subPath.ShouldBeNull();
|
||||
}
|
||||
|
||||
private async Task<string> GetResponseBodyAsString(HttpContext context)
|
||||
{
|
||||
context.Response.Body.Seek(0, SeekOrigin.Begin);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using UnitTests.Endpoints.EndSession;
|
||||
|
||||
namespace UnitTests.Services.Default;
|
||||
|
||||
public class DefaultSessionCoordinationServiceTests
|
||||
{
|
||||
public DefaultSessionCoordinationService Service;
|
||||
|
||||
[Fact]
|
||||
public async Task Handles_missing_client_null_reference()
|
||||
{
|
||||
var stubBackChannelLogoutClient = new StubBackChannelLogoutClient();
|
||||
Service = new DefaultSessionCoordinationService(
|
||||
new IdentityServerOptions(),
|
||||
new InMemoryPersistedGrantStore(),
|
||||
new InMemoryClientStore([]),
|
||||
stubBackChannelLogoutClient,
|
||||
new NullLogger<DefaultSessionCoordinationService>());
|
||||
|
||||
await Service.ProcessExpirationAsync(new UserSession
|
||||
{
|
||||
ClientIds = ["not_found"],
|
||||
SessionId = "1",
|
||||
SubjectId = "1"
|
||||
});
|
||||
|
||||
stubBackChannelLogoutClient
|
||||
.SendLogoutsWasCalled
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue