Skip to content

Commit 4956f27

Browse files
committed
Handle back channel logout when multiple custom-domains are enabled
1 parent 09aca42 commit 4956f27

2 files changed

Lines changed: 319 additions & 14 deletions

File tree

src/Auth0.AspNetCore.Authentication/BackchannelLogout/BackchannelLogoutHandler.cs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Security.Claims;
55
using System.Threading.Tasks;
6+
using Auth0.AspNetCore.Authentication.CustomDomains;
67
using Microsoft.AspNetCore.Authentication.Cookies;
78
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
89
using Microsoft.AspNetCore.Http;
@@ -18,7 +19,7 @@ public class BackchannelLogoutHandler
1819
private readonly ILogoutTokenHandler _tokenHandler;
1920
private readonly string _authenticationScheme;
2021

21-
public BackchannelLogoutHandler(ILogoutTokenHandler tokenHandler)
22+
public BackchannelLogoutHandler(ILogoutTokenHandler tokenHandler)
2223
: this(tokenHandler, Auth0Constants.AuthenticationScheme)
2324
{
2425
}
@@ -48,7 +49,17 @@ public async Task HandleRequestAsync(HttpContext context)
4849
.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>()
4950
.Get(_authenticationScheme);
5051

51-
var principal = await ValidateLogoutToken(logoutToken, oidcOptions, context);
52+
var customDomainsOptions = context.RequestServices
53+
.GetService<IOptionsMonitor<Auth0CustomDomainsOptions>>()
54+
?.Get(_authenticationScheme);
55+
var isMcdEnabled = customDomainsOptions?.IsMultipleCustomDomainsEnabled == true;
56+
57+
if (isMcdEnabled)
58+
{
59+
ValidateIssuerMatchesResolvedDomain(logoutToken, context);
60+
}
61+
62+
var principal = await ValidateLogoutToken(logoutToken, oidcOptions, context, isMcdEnabled);
5263

5364
if (principal != null)
5465
{
@@ -84,7 +95,46 @@ await context.WriteErrorAsync(400, "invalid_request",
8495
}
8596
}
8697

87-
private async Task<ClaimsPrincipal> ValidateLogoutToken(String token, OpenIdConnectOptions oidcOptions, HttpContext context)
98+
/// <summary>
99+
/// When MCD is enabled, extracts the issuer from the unverified token and validates it matches
100+
/// the domain resolved for the current request. This check happens BEFORE full JWT
101+
/// validation so we avoid fetching JWKS for tokens from the wrong tenant.
102+
/// </summary>
103+
private static void ValidateIssuerMatchesResolvedDomain(string token, HttpContext context)
104+
{
105+
var unverifiedIssuer = ExtractUnverifiedIssuer(token);
106+
107+
var resolvedDomain = context.GetResolvedDomain();
108+
109+
if (string.IsNullOrWhiteSpace(resolvedDomain))
110+
{
111+
throw new LogoutTokenValidationException(
112+
"Unable to resolve domain for this request. Ensure DomainResolver is configured.");
113+
}
114+
115+
var normalizedIssuer = Utils.ToAuthority(unverifiedIssuer);
116+
var normalizedResolved = Utils.ToAuthority(resolvedDomain);
117+
118+
if (!string.Equals(normalizedIssuer, normalizedResolved))
119+
{
120+
throw new LogoutTokenValidationException("Logout token issuer does not match the resolved domain.");
121+
}
122+
}
123+
124+
/// <summary>
125+
/// Reads the JWT without signature validation to extract the issuer claim.
126+
/// Throws <see cref="LogoutTokenValidationException"/> if the token is malformed.
127+
/// Note: This does not validate the signature or any other JWT claims.
128+
/// </summary>
129+
private static string ExtractUnverifiedIssuer(string token)
130+
{
131+
var handler = new JwtSecurityTokenHandler();
132+
if (!handler.CanReadToken(token))
133+
throw new LogoutTokenValidationException("Logout token is malformed or not a valid JWT.");
134+
return handler.ReadJwtToken(token).Issuer;
135+
}
136+
137+
private async Task<ClaimsPrincipal> ValidateLogoutToken(string token, OpenIdConnectOptions oidcOptions, HttpContext context, bool isMcdEnabled)
88138
{
89139
OpenIdConnectConfiguration? configuration = null;
90140

@@ -93,13 +143,19 @@ private async Task<ClaimsPrincipal> ValidateLogoutToken(String token, OpenIdConn
93143
configuration = await oidcOptions.ConfigurationManager.GetConfigurationAsync(context.RequestAborted);
94144
}
95145

146+
var validIssuer = isMcdEnabled
147+
? Utils.ToAuthority(context.GetResolvedDomain()
148+
?? throw new LogoutTokenValidationException(
149+
"Unable to resolve domain for this request. Ensure DomainResolver is configured."))
150+
: oidcOptions.TokenValidationParameters.ValidIssuer;
151+
96152
var tokenValidationParameters = new TokenValidationParameters
97153
{
98154
ValidateAudience = true,
99155
ValidateIssuer = true,
100156
ValidateLifetime = true,
101157
RequireExpirationTime = true,
102-
ValidIssuer = oidcOptions.TokenValidationParameters.ValidIssuer,
158+
ValidIssuer = validIssuer,
103159
ValidAudience = oidcOptions.TokenValidationParameters.ValidAudience,
104160
};
105161

0 commit comments

Comments
 (0)