diff --git a/src/Auth0.AspNetCore.Authentication/Auth0WebAppOptions.cs b/src/Auth0.AspNetCore.Authentication/Auth0WebAppOptions.cs index e72fdd4..bdf1324 100644 --- a/src/Auth0.AspNetCore.Authentication/Auth0WebAppOptions.cs +++ b/src/Auth0.AspNetCore.Authentication/Auth0WebAppOptions.cs @@ -151,5 +151,13 @@ public class Auth0WebAppOptions /// Set the target to the current scheme to disable forwarding and allow normal processing. /// public string? ForwardSignOut { get; set; } + + /// + /// Defines whether access and refresh tokens should be stored in the + /// after a successful authorization. This property is set to true by default to + /// ensure there are no breaking changes with previous versions of the sdk. Storing tokens will + /// increase the size of the final authentication cookie. + /// + public bool SaveTokens { get; set; } = true; } } diff --git a/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs b/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs index 6aa9c0b..e079773 100644 --- a/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs @@ -91,7 +91,7 @@ private static void ConfigureOpenIdConnect(OpenIdConnectOptions oidcOptions, Aut oidcOptions.Scope.Clear(); oidcOptions.Scope.AddRange(auth0Options.Scope.Split(" ")); oidcOptions.CallbackPath = new PathString(auth0Options.CallbackPath ?? Auth0Constants.DefaultCallbackPath); - oidcOptions.SaveTokens = true; + oidcOptions.SaveTokens = auth0Options.SaveTokens; oidcOptions.ResponseType = auth0Options.ResponseType ?? oidcOptions.ResponseType; oidcOptions.Backchannel = auth0Options.Backchannel!; oidcOptions.MaxAge = auth0Options.MaxAge; diff --git a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs index 025542c..abb649a 100644 --- a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs +++ b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs @@ -20,6 +20,9 @@ using System.Linq; using Auth0.AspNetCore.Authentication.Exceptions; using Microsoft.AspNetCore.Authentication.Cookies; +using Moq; +using Moq.Protected; +using System.Threading; namespace Auth0.AspNetCore.Authentication.IntegrationTests { @@ -1080,6 +1083,111 @@ public async Task Should_Allow_Custom_Token_Validation() } } } + + [Fact] + public async Task Should_Allow_Configuring_SaveTokens_To_False() + { + var nonce = ""; + var configuration = TestConfiguration.GetConfiguration(); + var domain = configuration["Auth0:Domain"]; + var clientId = configuration["Auth0:ClientId"]; + var mockHandler = new OidcMockBuilder() + .MockOpenIdConfig() + .MockJwks() + .MockToken(() => JwtUtils.GenerateToken(1, $"https://{domain}/", clientId, null, nonce), (me) => me.HasAuth0ClientHeader()) + .Build(); + + using (var server = TestServerBuilder.CreateServer(opt => + { + opt.SaveTokens = false; + opt.Backchannel = new HttpClient(mockHandler.Object); + })) + { + using (var client = server.CreateClient()) + { + var loginResponse = (await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Login}")); + var setCookie = Assert.Single(loginResponse.Headers, h => h.Key == "Set-Cookie"); + + var queryParameters = UriUtils.GetQueryParams(loginResponse.Headers.Location); + + // Keep track of the nonce as we need to: + // - Send it to the `/oauth/token` endpoint + // - Include it in the generated ID Token + nonce = queryParameters["nonce"]; + + // Keep track of the state as we need to: + // - Send it to the `/oauth/token` endpoint + var state = queryParameters["state"]; + + var message = new HttpRequestMessage(HttpMethod.Get, $"{TestServerBuilder.Host}/{TestServerBuilder.Callback}?state={state}&nonce={nonce}&code=123"); + + // Pass along the Set-Cookies to ensure `Nonce` and `Correlation` cookies are set. + var callbackResponse = (await client.SendAsync(message, setCookie.Value)); + + // Retrieve logged in cookies + setCookie = Assert.Single(callbackResponse.Headers, h => h.Key == "Set-Cookie"); + var tokens = new HttpRequestMessage(HttpMethod.Get, $"{TestServerBuilder.Host}/{TestServerBuilder.Tokens}"); + + // Pass along the Authentication cooke so we can validate whether the tokens exist + var tokenResponse = (await client.SendAsync(tokens, setCookie.Value)); + var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); + + tokenContent.Should().Be("TokensExist=False"); + } + } + } + + [Fact] + public async Task Should_Have_SaveTokens_To_True() + { + var nonce = ""; + var configuration = TestConfiguration.GetConfiguration(); + var domain = configuration["Auth0:Domain"]; + var clientId = configuration["Auth0:ClientId"]; + var mockHandler = new OidcMockBuilder() + .MockOpenIdConfig() + .MockJwks() + .MockToken(() => JwtUtils.GenerateToken(1, $"https://{domain}/", clientId, null, nonce), (me) => me.HasAuth0ClientHeader()) + .Build(); + + using (var server = TestServerBuilder.CreateServer(opt => + { + opt.Backchannel = new HttpClient(mockHandler.Object); + })) + { + using (var client = server.CreateClient()) + { + var loginResponse = (await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Login}")); + var setCookie = Assert.Single(loginResponse.Headers, h => h.Key == "Set-Cookie"); + + var queryParameters = UriUtils.GetQueryParams(loginResponse.Headers.Location); + + // Keep track of the nonce as we need to: + // - Send it to the `/oauth/token` endpoint + // - Include it in the generated ID Token + nonce = queryParameters["nonce"]; + + // Keep track of the state as we need to: + // - Send it to the `/oauth/token` endpoint + var state = queryParameters["state"]; + + var message = new HttpRequestMessage(HttpMethod.Get, $"{TestServerBuilder.Host}/{TestServerBuilder.Callback}?state={state}&nonce={nonce}&code=123"); + + // Pass along the Set-Cookies to ensure `Nonce` and `Correlation` cookies are set. + var callbackResponse = (await client.SendAsync(message, setCookie.Value)); + + // Retrieve logged in cookies + setCookie = Assert.Single(callbackResponse.Headers, h => h.Key == "Set-Cookie"); + var tokens = new HttpRequestMessage(HttpMethod.Get, $"{TestServerBuilder.Host}/{TestServerBuilder.Tokens}"); + + // Pass along the Authentication cooke so we can validate whether the tokens exist + var tokenResponse = (await client.SendAsync(tokens, setCookie.Value)); + var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); + + tokenContent.Should().Be("TokensExist=True"); + } + } + } [Fact] public async void Should_Refresh_Access_Token_When_Expired() @@ -1429,6 +1537,71 @@ public async void Should_Not_Refresh_Access_Token_When_Not_Expired() } } + [Fact] + public async void Should_Not_Refresh_Access_Token_When_Expired_SaveTokens_False() + { + var nonce = ""; + var configuration = TestConfiguration.GetConfiguration(); + var domain = configuration["Auth0:Domain"]; + var clientId = configuration["Auth0:ClientId"]; + + var mockHandler = new OidcMockBuilder() + .MockOpenIdConfig() + .MockJwks() + .MockToken(() => JwtUtils.GenerateToken(1, $"https://{domain}/", clientId, null, nonce, DateTime.Now.AddSeconds(20)), (me) => me.HasGrantType("authorization_code"), 20) + .MockToken(() => JwtUtils.GenerateToken(1, $"https://{domain}/", clientId, null, null, DateTime.Now.AddSeconds(20)), (me) => me.HasGrantType("refresh_token") && me.HasClientSecret(), + 20) + .Build(); + + using (var server = TestServerBuilder.CreateServer(opts => + { + opts.ClientSecret = "123"; + opts.Backchannel = new HttpClient(mockHandler.Object); + opts.SaveTokens = false; + }, opts => + { + opts.Audience = "123"; + opts.UseRefreshTokens = true; + })) + { + using (var client = server.CreateClient()) + { + var loginResponse = (await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Login}")); + var setCookie = Assert.Single(loginResponse.Headers, h => h.Key == "Set-Cookie"); + + var queryParameters = UriUtils.GetQueryParams(loginResponse.Headers.Location); + + // Keep track of the nonce as we need to: + // - Send it to the `/oauth/token` endpoint + // - Include it in the generated ID Token + nonce = queryParameters["nonce"]; + + // Keep track of the state as we need to: + // - Send it to the `/oauth/token` endpoint + var state = queryParameters["state"]; + + var message = new HttpRequestMessage(HttpMethod.Get, $"{TestServerBuilder.Host}/{TestServerBuilder.Callback}?state={state}&nonce={nonce}&code=123"); + + // Pass along the Set-Cookies to ensure `Nonce` and `Correlation` cookies are set. + var callbackResponse = (await client.SendAsync(message, setCookie.Value)); + + callbackResponse.Headers.Location.OriginalString.Should().Be("/"); + + var response = await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Process}", callbackResponse.Headers.GetValues("Set-Cookie")); + + mockHandler + .Protected() + .Verify( + "SendAsync", + Times.Never(), + ItExpr.Is(me => me.IsTokenEndPoint() + && me.HasGrantType("refresh_token") + && me.HasClientSecret()), + ItExpr.IsAny()); + } + } + } + [Fact] public async void Should_Call_On_Access_Token_Missing() { diff --git a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Controllers/AccountController.cs b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Controllers/AccountController.cs index 98dc8a1..7fe95a1 100644 --- a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Controllers/AccountController.cs +++ b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Controllers/AccountController.cs @@ -101,6 +101,18 @@ public IActionResult AccessDenied() { return View(); } + + public IActionResult Tokens() + { + var authItems = HttpContext.Features.Get()?.AuthenticateResult?.Properties?.Items; + if (authItems == null) return BadRequest("Error with authentication result object."); + if (authItems.ContainsKey(".Token.access_token") + && authItems.ContainsKey(".Token.refresh_token") + && authItems.ContainsKey(".Token.id_token")) + return Ok($"TokensExist=True"); + else + return Ok($"TokensExist=False"); + } private Dictionary ObjectToDictionary(object values) { diff --git a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Infrastructure/TestServerBuilder.cs b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Infrastructure/TestServerBuilder.cs index 701636f..29c5be7 100644 --- a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Infrastructure/TestServerBuilder.cs +++ b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Infrastructure/TestServerBuilder.cs @@ -23,6 +23,7 @@ internal class TestServerBuilder public static readonly string Process = "Process"; public static readonly string Logout = "Account/Logout"; public static readonly string Callback = "Callback"; + public static readonly string Tokens = "Account/Tokens"; public static readonly string ExtraProviderScheme = "ExtraProviderScheme"; ///