Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,8 @@ Additionally, you will also need to call `UseBackchannelLogout();` on the Applic
app.UseBackchannelLogout();
```

When using a custom scheme, make sure to use the same scheme name consistently throughout your authentication flow (login, logout, and backchannel logout configuration).

As logout tokens need to be stored, you will also need to provide something for our SDK to store the tokens in.

```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ public Auth0WebAppAuthenticationBuilder WithAccessToken(Action<Auth0WebAppWithAc
/// <returns>An instance of <see cref="Auth0WebAppAuthenticationBuilder"/></returns>
public Auth0WebAppAuthenticationBuilder WithBackchannelLogout()
{
_services.AddTransient<BackchannelLogoutHandler>();
_services.AddTransient<BackchannelLogoutHandler>(sp =>
new BackchannelLogoutHandler(
sp.GetRequiredService<ILogoutTokenHandler>(),
_authenticationScheme));
_services.AddTransient<ILogoutTokenHandler, DefaultLogoutTokenHandler>();
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@
public class BackchannelLogoutHandler
{
private readonly ILogoutTokenHandler _tokenHandler;
private readonly string _authenticationScheme;

public BackchannelLogoutHandler(ILogoutTokenHandler tokenHandler)
public BackchannelLogoutHandler(ILogoutTokenHandler tokenHandler)
: this(tokenHandler, Auth0Constants.AuthenticationScheme)
{
}

public BackchannelLogoutHandler(ILogoutTokenHandler tokenHandler, string authenticationScheme)
{
_tokenHandler = tokenHandler;
_authenticationScheme = authenticationScheme;
}

public async Task HandleRequestAsync(HttpContext context)
{
try
{
context.Response.Headers.Add("Cache-Control", "no-cache, no-store");

Check warning on line 36 in src/Auth0.AspNetCore.Authentication/BackchannelLogout/BackchannelLogoutHandler.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

Use IHeaderDictionary.Append or the indexer to append or set headers. IDictionary.Add will throw an ArgumentException when attempting to add a duplicate key. (https://aka.ms/aspnet/analyzers)

if (context.Request.HasFormContentType)
{
Expand All @@ -36,10 +43,10 @@
{
var auth0Options = context.RequestServices
.GetRequiredService<IOptionsSnapshot<Auth0WebAppOptions>>()
.Get(Auth0Constants.AuthenticationScheme);
.Get(_authenticationScheme);
var oidcOptions = context.RequestServices
.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>()
.Get(Auth0Constants.AuthenticationScheme);
.Get(_authenticationScheme);

var principal = await ValidateLogoutToken(logoutToken, oidcOptions, context);

Expand Down Expand Up @@ -104,7 +111,7 @@
}

var principal =
oidcOptions.SecurityTokenValidator.ValidateToken(token, tokenValidationParameters, out SecurityToken _);

Check warning on line 114 in src/Auth0.AspNetCore.Authentication/BackchannelLogout/BackchannelLogoutHandler.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

'OpenIdConnectOptions.SecurityTokenValidator' is obsolete: 'SecurityTokenValidator is no longer used by default. Use TokenHandler instead. To continue using SecurityTokenValidator, set UseSecurityTokenValidator to true. See https://aka.ms/aspnetcore8/security-token-changes'

Check warning on line 114 in src/Auth0.AspNetCore.Authentication/BackchannelLogout/BackchannelLogoutHandler.cs

View workflow job for this annotation

GitHub Actions / build (net8.0)

'OpenIdConnectOptions.SecurityTokenValidator' is obsolete: 'SecurityTokenValidator is no longer used by default. Use TokenHandler instead. To continue using SecurityTokenValidator, set UseSecurityTokenValidator to true. See https://aka.ms/aspnetcore8/security-token-changes'

LogoutTokenValidator.Validate(new JwtSecurityTokenHandler().ReadJwtToken(token));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.5.0" />
<PackageReference Include="FluentAssertions" Version="7.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.*" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="2.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,4 +553,44 @@ public async Task Should_Not_Logout_When_Sid_Doesnt_Match()
protectedResponse2.StatusCode.Should().Be(HttpStatusCode.OK);
protectedResponse2.Headers.Location.Should().BeNull();
}

[Fact]
public async Task Should_Support_Custom_Authentication_Scheme()
{
var configuration = TestConfiguration.GetConfiguration();
var domain = configuration["Auth0:Domain"];
var clientId = configuration["Auth0:ClientId"];
var customScheme = "CustomScheme";

var mockHandler = new OidcMockBuilder()
.MockOpenIdConfig()
.MockJwks()
.MockToken(() => new JwtTokenBuilder(1)
.WithIssuer($"https://{domain}/")
.WithAudience(clientId)
.Build())
.Build();

// Create a server with a custom authentication scheme
using var server = TestServerBuilder.CreateServerWithCustomScheme(customScheme, opt =>
{
opt.Backchannel = new HttpClient(mockHandler.Object);
}, null, false, false, false, null, true);
using var client = server.CreateClient();

// Create a valid logout token
var logoutToken = new JwtTokenBuilder(1)
.WithIssuer($"https://{domain}/")
.WithAudience(clientId)
.WithClaim(JwtRegisteredClaimNames.Sid, "sid")
.WithClaim("events", "{ \"http://schemas.openid.net/event/backchannel-logout\": {} }" )
.Build();

var formData = new Dictionary<string, string> { { "logout_token", logoutToken } };
using var req = new HttpRequestMessage(HttpMethod.Post, $"{TestServerBuilder.Host}/backchannel-logout");
req.Content = new FormUrlEncodedContent(formData);
using var response = await client.SendAsync(req);

response.StatusCode.Should().Be((HttpStatusCode)200);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ internal class TestServerBuilder
/// </summary>
/// <param name="configureOptions">Action used to provide custom configuration for the Auth0 middleware.</param>
/// <param name="mockAuthentication">Indicated whether or not the authenitcation should be mocked, useful because some tests require an authenticated user while others require no user to exist.</param>
/// <param name="authenticationScheme">Optional custom authentication scheme to use.</param>
/// <returns>The created TestServer instance.</returns>
public static TestServer CreateServer(Action<Auth0WebAppOptions> configureOptions = null, Action<Auth0WebAppWithAccessTokenOptions> configureWithAccessTokensOptions = null, bool mockAuthentication = false, bool useServiceCollectionExtension = false, bool addExtraProvider = false, Action<Auth0WebAppOptions> configureAdditionalOptions = null, bool enableBackchannelLogout = false)
public static TestServer CreateServer(Action<Auth0WebAppOptions> configureOptions = null, Action<Auth0WebAppWithAccessTokenOptions> configureWithAccessTokensOptions = null, bool mockAuthentication = false, bool useServiceCollectionExtension = false, bool addExtraProvider = false, Action<Auth0WebAppOptions> configureAdditionalOptions = null, bool enableBackchannelLogout = false, string authenticationScheme = null)
{
var configuration = TestConfiguration.GetConfiguration();
var host = new HostBuilder()
Expand Down Expand Up @@ -78,13 +79,21 @@ await res.WriteAsync(JsonSerializer.Serialize(new
Auth0WebAppAuthenticationBuilder builder;
if (useServiceCollectionExtension)
{
builder = services.AddAuth0WebAppAuthentication(options =>
{
options.Domain = configuration["Auth0:Domain"];
options.ClientId = configuration["Auth0:ClientId"];
builder = string.IsNullOrEmpty(authenticationScheme)
? services.AddAuth0WebAppAuthentication(options =>
{
options.Domain = configuration["Auth0:Domain"];
options.ClientId = configuration["Auth0:ClientId"];

if (configureOptions != null) configureOptions(options);
});
if (configureOptions != null) configureOptions(options);
})
: services.AddAuth0WebAppAuthentication(authenticationScheme, options =>
{
options.Domain = configuration["Auth0:Domain"];
options.ClientId = configuration["Auth0:ClientId"];

if (configureOptions != null) configureOptions(options);
});
}
else
{
Expand All @@ -98,13 +107,21 @@ await res.WriteAsync(JsonSerializer.Serialize(new
}
});

builder = authenticationBuilder.AddAuth0WebAppAuthentication(options =>
{
options.Domain = configuration["Auth0:Domain"];
options.ClientId = configuration["Auth0:ClientId"];
builder = string.IsNullOrEmpty(authenticationScheme)
? authenticationBuilder.AddAuth0WebAppAuthentication(options =>
{
options.Domain = configuration["Auth0:Domain"];
options.ClientId = configuration["Auth0:ClientId"];

if (configureOptions != null) configureOptions(options);
});
if (configureOptions != null) configureOptions(options);
})
: authenticationBuilder.AddAuth0WebAppAuthentication(authenticationScheme, options =>
{
options.Domain = configuration["Auth0:Domain"];
options.ClientId = configuration["Auth0:ClientId"];

if (configureOptions != null) configureOptions(options);
});

if (addExtraProvider)
{
Expand Down Expand Up @@ -147,6 +164,27 @@ await res.WriteAsync(JsonSerializer.Serialize(new
host.Start();
return host.GetTestServer();
}

/// <summary>
/// Create an instance of the TestServer with a custom authentication scheme to use for Integration Tests.
/// This is used to reproduce the issue where BackchannelLogoutHandler doesn't respect custom authentication schemes.
/// </summary>
/// <param name="authenticationScheme">The custom authentication scheme to use.</param>
/// <param name="configureOptions">Action used to provide custom configuration for the Auth0 middleware.</param>
/// <param name="mockAuthentication">Indicated whether or not the authenitcation should be mocked.</param>
/// <returns>The created TestServer instance.</returns>
public static TestServer CreateServerWithCustomScheme(string authenticationScheme, Action<Auth0WebAppOptions> configureOptions = null, Action<Auth0WebAppWithAccessTokenOptions> configureWithAccessTokensOptions = null, bool mockAuthentication = false, bool useServiceCollectionExtension = false, bool addExtraProvider = false, Action<Auth0WebAppOptions> configureAdditionalOptions = null, bool enableBackchannelLogout = false)
{
return CreateServer(
configureOptions: configureOptions,
configureWithAccessTokensOptions: configureWithAccessTokensOptions,
mockAuthentication: mockAuthentication,
useServiceCollectionExtension: useServiceCollectionExtension,
addExtraProvider: addExtraProvider,
configureAdditionalOptions: configureAdditionalOptions,
enableBackchannelLogout: enableBackchannelLogout,
authenticationScheme: authenticationScheme);
}
}
}

Loading