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,