From e34a5f8fb8e97b32077e8587261590c3910ad181 Mon Sep 17 00:00:00 2001 From: OpenIDAuthority Date: Wed, 16 Aug 2017 21:13:23 -0700 Subject: [PATCH] Add MaxAge to OpenIdConnectOptions - max_age parameter added to the authentication request if MaxAge is not null - throws exception if MaxAge is set to a negative value - Fractions of seconds are ignored - See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for expected behavior Addresses #1233 --- .../OpenIdConnectHandler.cs | 8 ++++ .../OpenIdConnectOptions.cs | 12 ++++++ .../OpenIdConnectChallengeTests.cs | 41 +++++++++++++++++++ .../OpenIdConnectConfigurationTests.cs | 15 +++++++ .../OpenIdConnect/TestSettings.cs | 19 +++++++++ 5 files changed, 95 insertions(+) diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index 0b2419e2ab..0f60a558ad 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -353,6 +353,14 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect Scope = string.Join(" ", Options.Scope) }; + // Add the 'max_age' parameter to the authentication request if MaxAge is not null. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + if (Options.MaxAge != null) + { + message.MaxAge = Convert.ToInt64(Math.Floor(((TimeSpan)Options.MaxAge).TotalSeconds)) + .ToString(CultureInfo.InvariantCulture); + } + // Omitting the response_mode parameter when it already corresponds to the default // response_mode used for the specified response_type is recommended by the specifications. // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs index e589a2bc87..f6d914731a 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs @@ -84,6 +84,11 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { base.Validate(); + if (MaxAge != null && MaxAge.Value < TimeSpan.Zero) + { + throw new InvalidOperationException("MaxAge must not be a negative TimeSpan."); + } + if (string.IsNullOrEmpty(ClientId)) { throw new ArgumentException("Options.ClientId must be provided", nameof(ClientId)); @@ -159,6 +164,13 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect set => base.Events = value; } + /// + /// Gets or sets the 'max_age'. If set the 'max_age' parameter will be sent with the authentication request. If the identity + /// provider has not actively authenticated the user within the length of time specified, the user will be prompted to + /// re-authenticate. By default no max_age is specified. + /// + public TimeSpan? MaxAge { get; set; } = null; + /// /// Gets or sets the that is used to ensure that the 'id_token' received /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs index fb08ae2786..4ff5aa9adb 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs @@ -409,5 +409,46 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect var exception = await Assert.ThrowsAsync(() => server.SendAsync(ChallengeEndpoint)); Assert.Equal("Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.", exception.Message); } + + [Fact] + public async Task Challenge_WithDefaultMaxAge_HasExpectedMaxAgeParam() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.MaxAge); + } + + [Fact] + public async Task Challenge_WithSpecificMaxAge_HasExpectedMaxAgeParam() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromMinutes(20); + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.MaxAge); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs index d0d1c26096..871ef9d08b 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs @@ -115,6 +115,21 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect ); } + [Fact] + public Task ThrowsWhenMaxAgeIsNegative() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + o.MaxAge = TimeSpan.FromSeconds(-1); + }, + ex => Assert.Equal("MaxAge must not be a negative TimeSpan.", ex.Message) + ); + } + private TestServer BuildTestServer(Action options) { var builder = new WebHostBuilder() diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs index 458f746c44..5b4ea23482 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Text.Encodings.Web; using System.Xml.Linq; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Xunit; @@ -197,6 +199,9 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect case OpenIdConnectParameterNames.PostLogoutRedirectUri: ValidatePostLogoutRedirectUri(actualValues, errors, htmlEncoded); break; + case OpenIdConnectParameterNames.MaxAge: + ValidateMaxAge(actualValues, errors, htmlEncoded); + break; default: throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\"."); } @@ -263,6 +268,20 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect private void ValidatePostLogoutRedirectUri(IDictionary actualParams, ICollection errors, bool htmlEncoded) => ValidateParameter(OpenIdConnectParameterNames.PostLogoutRedirectUri, "https://example.com/signout-callback-oidc", actualParams, errors, htmlEncoded); + private void ValidateMaxAge(IDictionary actualQuery, ICollection errors, bool htmlEncoded) + { + if(_options.MaxAge != null) + { + string expectedMaxAge = Convert.ToInt64(Math.Floor(((TimeSpan)_options.MaxAge).TotalSeconds)) + .ToString(CultureInfo.InvariantCulture); + ValidateParameter(OpenIdConnectParameterNames.MaxAge, expectedMaxAge, actualQuery, errors, htmlEncoded); + } + else if(actualQuery.ContainsKey(OpenIdConnectParameterNames.MaxAge)) + { + errors.Add($"Parameter {OpenIdConnectParameterNames.MaxAge} is present but it should be absent"); + } + } + private void ValidateParameter( string parameterName, string expectedValue,