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
15 changes: 13 additions & 2 deletions src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
SecurityToken = validatedToken
};

tokenValidatedContext.Properties.ExpiresUtc = validatedToken.ValidTo;
tokenValidatedContext.Properties.IssuedUtc = validatedToken.ValidFrom;
tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);

await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
Expand Down Expand Up @@ -202,6 +202,17 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
}
}

private static DateTime? GetSafeDateTime(DateTime dateTime)
{
// Assigning DateTime.MinValue or default(DateTime) to a DateTimeOffset when in a UTC+X timezone will throw
// Since we don't really care about DateTime.MinValue in this case let's just set the field to null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link to the runtime issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out it's by design

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frankly, this feels like the wrong approach. That you're getting an error here means the returned DateTimes aren't DateTimeKind.Utc, which seems... not correct for their intended use. There's no reason for them to be anything other than Utc, and the fact that the "safe" method doesn't otherwise correct the issue means it's going to cause problems further down the line. If you echo/pass on the claims, is this currently causing these timestamps to change due to local/utc confusion?

The JWT claims are effectively ignorant of timezone, but are still absolute timestamps, and so can be considered UTC - a far better approach would be ensuring that the parsing method ensures that it returns the DateTime with the proper kind, and ensure that anything that could modify has the output coerced to UTC (via SpecifyKind(DateTimeKind.Utc) - which is just going to "assign" the kind, and leave the timestamp otherwise unchanged, and won't throw).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (dateTime == DateTime.MinValue)
{
return null;
}
return dateTime;
}

/// <inheritdoc />
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Expand Down
43 changes: 42 additions & 1 deletion src/Security/Authentication/test/JwtBearerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,47 @@ public async Task ExpirationAndIssuedSetOnAuthenticateResult()
Assert.Equal(token.ValidFrom, dom.RootElement.GetProperty("issued").GetDateTimeOffset());
}

[Fact]
public async Task ExpirationAndIssuedNullWhenMinOrMaxValue()
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128)));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "Bob")
};

var token = new JwtSecurityToken(
issuer: "issuer.contoso.com",
audience: "audience.contoso.com",
claims: claims,
expires: DateTime.MaxValue,
signingCredentials: creds);

var tokenText = new JwtSecurityTokenHandler().WriteToken(token);

using var host = await CreateHost(o =>
{
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "issuer.contoso.com",
ValidAudience = "audience.contoso.com",
IssuerSigningKey = key,
};
});

var newBearerToken = "Bearer " + tokenText;
using var server = host.GetTestServer();
var response = await SendAsync(server, "http://example.com/expiration", newBearerToken);
Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
var responseBody = await response.Response.Content.ReadAsStringAsync();
using var dom = JsonDocument.Parse(responseBody);
Assert.Equal(JsonValueKind.Null, dom.RootElement.GetProperty("expires").ValueKind);
Assert.Equal(JsonValueKind.Null, dom.RootElement.GetProperty("issued").ValueKind);
}

class InvalidTokenValidator : ISecurityTokenValidator
{
public InvalidTokenValidator()
Expand Down Expand Up @@ -1065,7 +1106,7 @@ private static async Task<IHost> CreateHost(Action<JwtBearerOptions> options = n
{
var authenticationResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
await context.Response.WriteAsJsonAsync(
new { Expires = authenticationResult.Properties.ExpiresUtc, Issued = authenticationResult.Properties.IssuedUtc });
new { Expires = authenticationResult.Properties?.ExpiresUtc, Issued = authenticationResult.Properties?.IssuedUtc });
}
else
{
Expand Down