Skip to content

Commit

Permalink
Change protection to JSON only so as to support AoT/trimming and use …
Browse files Browse the repository at this point in the history
…of serialization options from MVC

Using JSON instead of the type descriptors helps avoid losing sub-second precision for the `DateTime` and `DateTimeOffset` types.

With this PR, `Protect(...)` and `Unprotect(...)` methods allow the passing of `JsonSerializerOptions` where trimming or AoT is not in use. These options are optional and when not supplied, the default JSON options configured for MVC are used. In trimming and AoT scenarios, these methods have overloads that accept `JsonTypeInfo<T>`.

As a side-effect, any pre-protected tokens may fail to unprotect if `options.UseConversionInsteadOfJson` had not been set to true. The workaround for this is to create a custom implementation of `ITokenProtector<T>` to handle the cases. For tokens with a short lifetime, this may not be an issue. The protector purpose has changed from `Tingle.Tokens.v2019-12-17` to `Tingle.AspNetCore.Tokens.v2024-05-05` to ensure one can find away support older scenarios
  • Loading branch information
mburumaxwell committed May 5, 2024
1 parent b851ec0 commit 4c441c1
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 174 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using Tingle.AspNetCore.Tokens.Protection;

Expand All @@ -16,6 +17,8 @@ namespace Tingle.AspNetCore.Tokens.Binders;
/// The implementation is responsible for encrypting and decrypting where needed.
/// </param>
/// <param name="logger">The application's logger, specialized for <see cref="ContinuationTokenModelBinder{T}"/>.</param>
[RequiresUnreferencedCode(MessageStrings.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(MessageStrings.SerializationRequiresDynamicCodeMessage)]
internal class ContinuationTokenModelBinder<T>(ITokenProtector<T> protector, ILogger<ContinuationTokenModelBinder<T>> logger) : IModelBinder
{
private readonly ITokenProtector<T> protector = protector ?? throw new ArgumentNullException(nameof(protector));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ internal class ContinuationTokenJsonConverter : JsonConverter<IToken>
/// <inheritdoc/>
public override IToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> throw new NotSupportedException("Tokens cannot be deserialized because they are protected (obscure) data."
+ " Use model binding instead.");
+ " Use model binding instead.");

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, IToken value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.GetProtected());
}
=> writer.WriteStringValue(value.GetProtected());
}
84 changes: 83 additions & 1 deletion src/Tingle.AspNetCore.Tokens/Extensions/ControllerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Mvc.Infrastructure;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Tingle.AspNetCore.Tokens;

namespace Microsoft.AspNetCore.Mvc;
Expand All @@ -15,13 +18,17 @@ public static class ControllerExtensions
/// <param name="controller">the controller to extend</param>
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="token">the token containing the value</param>
/// <param name="serializerOptions">Options to control the behavior during parsing.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
[RequiresUnreferencedCode(MessageStrings.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(MessageStrings.SerializationRequiresDynamicCodeMessage)]
public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
[ActionResultObjectValue] object value,
ContinuationToken<T> token,
JsonSerializerOptions? serializerOptions = null,
string headerName = TokenDefaults.ContinuationTokenHeaderName)
{
return new ContinuationTokenResult<T>(value, token, headerName);
return new ContinuationTokenResult<T>(value, token, serializerOptions, headerName);
}

/// <summary>
Expand All @@ -31,15 +38,20 @@ public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
/// <param name="controller">the controller to extend</param>
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="tokenValue">the token's value</param>
/// <param name="serializerOptions">Options to control the behavior during parsing.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
[RequiresUnreferencedCode(MessageStrings.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(MessageStrings.SerializationRequiresDynamicCodeMessage)]
public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
[ActionResultObjectValue] object value,
T tokenValue,
JsonSerializerOptions? serializerOptions = null,
string headerName = TokenDefaults.ContinuationTokenHeaderName)
{
return Ok(controller: controller,
value: value,
token: new ContinuationToken<T>(tokenValue),
serializerOptions: serializerOptions,
headerName: headerName);
}

Expand All @@ -52,16 +64,86 @@ public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="tokenValue">the token's value</param>
/// <param name="expiration"></param>
/// <param name="serializerOptions">Options to control the behavior during parsing.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
[RequiresUnreferencedCode(MessageStrings.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(MessageStrings.SerializationRequiresDynamicCodeMessage)]
public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
[ActionResultObjectValue] object value,
T tokenValue,
DateTimeOffset expiration,
JsonSerializerOptions? serializerOptions = null,
string headerName = TokenDefaults.ContinuationTokenHeaderName)
{
return Ok(controller: controller,
value: value,
token: new TimedContinuationToken<T>(tokenValue, expiration),
serializerOptions: serializerOptions,
headerName: headerName);
}

/// <summary>
/// Creates an <see cref="OkObjectResult"/> that supports writing an instance of <see cref="ContinuationToken{T}"/>
/// to a header before writing the response body
/// </summary>
/// <param name="controller">the controller to extend</param>
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="token">the token containing the value</param>
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
[ActionResultObjectValue] object value,
ContinuationToken<T> token,
JsonTypeInfo<T> jsonTypeInfo,
string headerName = TokenDefaults.ContinuationTokenHeaderName)
{
return new ContinuationTokenResult<T>(value, token, jsonTypeInfo, headerName);
}

/// <summary>
/// Creates an <see cref="OkObjectResult"/> that supports writing a token's value wrapped in an instance of
/// <see cref="ContinuationToken{T}"/> to a header before writing the response body
/// </summary>
/// <param name="controller">the controller to extend</param>
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="tokenValue">the token's value</param>
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
[ActionResultObjectValue] object value,
T tokenValue,
JsonTypeInfo<T> jsonTypeInfo,
string headerName = TokenDefaults.ContinuationTokenHeaderName)
{
return Ok(controller: controller,
value: value,
token: new ContinuationToken<T>(tokenValue),
jsonTypeInfo: jsonTypeInfo,
headerName: headerName);
}


/// <summary>
/// Creates an <see cref="OkObjectResult"/> that supports writing a token's value wrapped in an instance of
/// <see cref="ContinuationTokenResult{T}"/> to a header before writing the response body
/// </summary>
/// <param name="controller">the controller to extend</param>
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="tokenValue">the token's value</param>
/// <param name="expiration"></param>
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
public static ContinuationTokenResult<T> Ok<T>(this ControllerBase controller,
[ActionResultObjectValue] object value,
T tokenValue,
DateTimeOffset expiration,
JsonTypeInfo<T> jsonTypeInfo,
string headerName = TokenDefaults.ContinuationTokenHeaderName)
{
return Ok(controller: controller,
value: value,
token: new TimedContinuationToken<T>(tokenValue, expiration),
jsonTypeInfo: jsonTypeInfo,
headerName: headerName);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics.CodeAnalysis;
using Tingle.AspNetCore.Tokens;
using Tingle.AspNetCore.Tokens.Binders;
using Tingle.AspNetCore.Tokens.Protection;

Expand All @@ -26,17 +24,11 @@ public static class IMvcBuilderExtensions
/// Redis, Blob Storage, Windows Registry etc.
/// </summary>
/// <param name="builder">The application's MVC builder.</param>
/// <param name="configure">
/// An <see cref="Action{T}"/> to further configure instances of <see cref="TokenProtectorOptions"/>.
/// </param>
/// <returns>The modified builder.</returns>
[RequiresUnreferencedCode(MessageStrings.TokenProtectorUnreferencedCodeMessage)]
[RequiresDynamicCode(MessageStrings.TokenProtectorRequiresDynamicCodeMessage)]
public static IMvcBuilder AddTokens(this IMvcBuilder builder, Action<TokenProtectorOptions>? configure = null)
public static IMvcBuilder AddTokens(this IMvcBuilder builder)
{
// Register the protector services
var services = builder.Services;
if (configure != null) services.Configure(configure);
services.AddScoped(typeof(ITokenProtector<>), typeof(TokenProtector<>));

return builder.AddMvcOptions(options =>
Expand Down
20 changes: 0 additions & 20 deletions src/Tingle.AspNetCore.Tokens/Extensions/TokenProtectorOptions.cs

This file was deleted.

7 changes: 2 additions & 5 deletions src/Tingle.AspNetCore.Tokens/MessageStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

internal class MessageStrings
{
//internal const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.";
//internal const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.";

internal const string TokenProtectorUnreferencedCodeMessage = "JSON serialization/deserialization and generic TypeConverters might require types that cannot be statically analyzed.";
internal const string TokenProtectorRequiresDynamicCodeMessage = "JSON serialization/deserialization and generic TypeConverters might require types that cannot be statically analyzed.";
public const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.";
public const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.";
}
57 changes: 49 additions & 8 deletions src/Tingle.AspNetCore.Tokens/Mvc/ContinuationTokenResult.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Tingle.AspNetCore.Tokens;
using Tingle.AspNetCore.Tokens.Protection;

Expand All @@ -9,13 +12,43 @@ namespace Microsoft.AspNetCore.Mvc;
/// An <see cref="OkObjectResult"/> that supports writing the continuation token to a header before writing the response body
/// </summary>
/// <typeparam name="T">The type of data contained</typeparam>
/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="token">the token containing the value</param>
/// <param name="headerName">the name of the header to write the protected token</param>
public class ContinuationTokenResult<T>([ActionResultObjectValue] object value,
ContinuationToken<T> token,
string headerName = TokenDefaults.ContinuationTokenHeaderName) : OkObjectResult(value)
public class ContinuationTokenResult<T> : OkObjectResult
{
private readonly ContinuationToken<T> token;
private readonly string headerName;
private readonly JsonSerializerOptions? serializerOptions;
private readonly JsonTypeInfo<T>? jsonTypeInfo;

/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="token">the token containing the value</param>
/// <param name="serializerOptions">Options to control the behavior during parsing.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
[RequiresUnreferencedCode(MessageStrings.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(MessageStrings.SerializationRequiresDynamicCodeMessage)]
public ContinuationTokenResult([ActionResultObjectValue] object value,
ContinuationToken<T> token,
JsonSerializerOptions? serializerOptions = null,
string headerName = TokenDefaults.ContinuationTokenHeaderName) : base(value)
{
this.token = token ?? throw new ArgumentNullException(nameof(token));
this.serializerOptions = serializerOptions;
this.headerName = headerName ?? throw new ArgumentNullException(nameof(headerName));
}

/// <param name="value">Contains the errors to be returned to the client.</param>
/// <param name="token">the token containing the value</param>
/// <param name="jsonTypeInfo">Metadata about the type to convert.</param>
/// <param name="headerName">the name of the header to write the protected token</param>
public ContinuationTokenResult([ActionResultObjectValue] object value,
ContinuationToken<T> token,
JsonTypeInfo<T> jsonTypeInfo,
string headerName = TokenDefaults.ContinuationTokenHeaderName) : base(value)
{
this.token = token ?? throw new ArgumentNullException(nameof(token));
this.jsonTypeInfo = jsonTypeInfo ?? throw new ArgumentNullException(nameof(jsonTypeInfo));
this.headerName = headerName ?? throw new ArgumentNullException(nameof(headerName));
}

/// <inheritdoc/>
public override void OnFormatting(ActionContext context)
{
Expand All @@ -30,20 +63,28 @@ public override void OnFormatting(ActionContext context)
// get an instance of the protector
var protector = context.HttpContext.RequestServices.GetRequiredService<ITokenProtector<T>>();

#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
// generate a new protected value based on the type of token
string protected_val;
var value = token.GetValue();
if (token is TimedContinuationToken<T> timed)
{
protected_val = protector.Protect(timed.GetValue(), timed.GetExpiration());
var expiration = timed.GetExpiration();
protected_val = jsonTypeInfo is null
? protector.Protect(value, expiration, serializerOptions)
: protector.Protect(value, expiration, jsonTypeInfo);
}
else
{
protected_val = protector.Protect(token.GetValue());
protected_val = jsonTypeInfo is null
? protector.Protect(value)
: protector.Protect(value);
}

// set the header if the protected value is not null
if (!string.IsNullOrWhiteSpace(protected_val))
context.HttpContext.Response.Headers[headerName] = protected_val;
}
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
}
Loading

0 comments on commit 4c441c1

Please sign in to comment.