Browse Source

Enable session validation support in the server and validation stacks

release/8.0.0-preview.3 8.0.0-preview.3
Kévin Chalet 2 weeks ago
parent
commit
2fd30447f0
  1. 7
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs
  2. 5
      src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
  3. 9
      src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs
  4. 11
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  5. 25
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  6. 20
      src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
  7. 6
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  8. 25
      src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs
  9. 28
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  10. 2
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs
  11. 63
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
  12. 2
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs
  13. 63
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
  14. 18
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
  15. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
  16. 1
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  17. 14
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  18. 47
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  19. 10
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  20. 8
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  21. 5
      src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
  22. 2
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  23. 28
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  24. 48
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  25. 8
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  26. 1155
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  27. 316
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs
  28. 17
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  29. 17
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs
  30. 46
      test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs

7
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs

@ -320,10 +320,11 @@ builder.Services.AddOpenIddict()
// For applications that need immediate access token or authorization // For applications that need immediate access token or authorization
// revocation, the database entry of the received tokens and their // revocation, the database entry of the received tokens and their
// associated authorizations can be validated for each API call. // associated authorizations can be validated for each API call.
// Enabling these options may have a negative impact on performance.
// //
// options.EnableAuthorizationEntryValidation(); // Note: enabling these options may have a negative impact on performance.
// options.EnableTokenEntryValidation(); options.EnableAuthorizationEntryValidation()
.EnableSessionEntryValidation()
.EnableTokenEntryValidation();
}); });
builder.Services.AddTransient<IEmailSender, AuthMessageSender>(); builder.Services.AddTransient<IEmailSender, AuthMessageSender>();

5
src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs

@ -60,6 +60,11 @@ public class OpenIddictTokenDescriptor
/// </remarks> /// </remarks>
public string? ReferenceId { get; set; } public string? ReferenceId { get; set; }
/// <summary>
/// Gets or sets the identifier of the session associated with the token.
/// </summary>
public string? SessionId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the status of the token. /// Gets or sets the status of the token.
/// </summary> /// </summary>

9
src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs

@ -258,6 +258,15 @@ public interface IOpenIddictSessionManager
/// </returns> /// </returns>
ValueTask<string?> GetSubjectAsync(object session, CancellationToken cancellationToken = default); ValueTask<string?> GetSubjectAsync(object session, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given session has the specified status.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="status">The expected status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><see langword="true"/> if the session has the specified status, <see langword="false"/> otherwise.</returns>
ValueTask<bool> HasStatusAsync(object session, string status, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>

11
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -277,6 +277,17 @@ public interface IOpenIddictTokenManager
/// </returns> /// </returns>
ValueTask<string?> GetReferenceIdAsync(object token, CancellationToken cancellationToken = default); ValueTask<string?> GetReferenceIdAsync(object token, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the optional session identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session identifier associated with the token.
/// </returns>
ValueTask<string?> GetSessionIdAsync(object token, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Retrieves the status associated with a token. /// Retrieves the status associated with a token.
/// </summary> /// </summary>

25
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -537,10 +537,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A
<value>The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used as TLS client certificates or to produce client assertions if the authorization server supports it.</value> <value>The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used as TLS client certificates or to produce client assertions if the authorization server supports it.</value>
</data> </data>
<data name="ID0133" xml:space="preserve"> <data name="ID0133" xml:space="preserve">
<value>Authorization entry validation cannot be enabled when using introspection.</value> <value>Authorization entry, session entry and token entry validation cannot be enabled when using introspection.</value>
</data>
<data name="ID0134" xml:space="preserve">
<value>Token entry validation cannot be enabled when using introspection.</value>
</data> </data>
<data name="ID0135" xml:space="preserve"> <data name="ID0135" xml:space="preserve">
<value>A discovery client must be registered when using server discovery. <value>A discovery client must be registered when using server discovery.
@ -557,7 +554,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A
This may indicate that it was not properly registered in the dependency injection container. To register an event handler, use 'services.AddOpenIddict().AddValidation().AddEventHandler()'.</value> This may indicate that it was not properly registered in the dependency injection container. To register an event handler, use 'services.AddOpenIddict().AddValidation().AddEventHandler()'.</value>
</data> </data>
<data name="ID0139" xml:space="preserve"> <data name="ID0139" xml:space="preserve">
<value>The core services must be registered when enabling token entry validation. <value>The core services must be registered when enabling authorization entry, session entry or token entry validation.
To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.</value> To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.</value>
</data> </data>
<data name="ID0140" xml:space="preserve"> <data name="ID0140" xml:space="preserve">
@ -566,10 +563,6 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
<data name="ID0141" xml:space="preserve"> <data name="ID0141" xml:space="preserve">
<value>An unknown error occurred while introspecting the access token.</value> <value>An unknown error occurred while introspecting the access token.</value>
</data> </data>
<data name="ID0142" xml:space="preserve">
<value>The core services must be registered when enabling authorization entry validation.
To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.</value>
</data>
<data name="ID0143" xml:space="preserve"> <data name="ID0143" xml:space="preserve">
<value>The URI cannot be null or empty.</value> <value>The URI cannot be null or empty.</value>
</data> </data>
@ -856,10 +849,7 @@ Reload the entity from the database and retry the operation.</value>
Make sure that the entity is not abstract and has a public parameterless constructor or create a custom store that overrides 'InstantiateAsync()' to use a custom factory.</value> Make sure that the entity is not abstract and has a public parameterless constructor or create a custom store that overrides 'InstantiateAsync()' to use a custom factory.</value>
</data> </data>
<data name="ID0244" xml:space="preserve"> <data name="ID0244" xml:space="preserve">
<value>The application matching the specified identifier cannot be found in the change tracker or in the database.</value> <value>The entity matching the specified identifier cannot be found in the change tracker or in the database.</value>
</data>
<data name="ID0251" xml:space="preserve">
<value>The authorization matching the specified identifier cannot be found in the change tracker or in the database.</value>
</data> </data>
<data name="ID0253" xml:space="preserve"> <data name="ID0253" xml:space="preserve">
<value>No Entity Framework Core context was configured to be used with OpenIddict. <value>No Entity Framework Core context was configured to be used with OpenIddict.
@ -2430,6 +2420,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID2209" xml:space="preserve"> <data name="ID2209" xml:space="preserve">
<value>The login identifier cannot be null or empty and must match the value used to represent the user session.</value> <value>The login identifier cannot be null or empty and must match the value used to represent the user session.</value>
</data> </data>
<data name="ID2210" xml:space="preserve">
<value>The session associated with the token is no longer valid.</value>
</data>
<data name="ID4000" xml:space="preserve"> <data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value> <value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data> </data>
@ -2496,6 +2489,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID4021" xml:space="preserve"> <data name="ID4021" xml:space="preserve">
<value>The length of the memory span ({0}) doesn't match the expected value ({1}).</value> <value>The length of the memory span ({0}) doesn't match the expected value ({1}).</value>
</data> </data>
<data name="ID4022" xml:space="preserve">
<value>The session identifier shouldn't be null or empty at this point.</value>
</data>
<data name="ID6000" xml:space="preserve"> <data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value> <value>An error occurred while validating the token '{Token}'.</value>
</data> </data>
@ -3288,6 +3284,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6296" xml:space="preserve"> <data name="ID6296" xml:space="preserve">
<value>A signing key of type '{Type}' was ignored because its ML-DSA public key couldn't be extracted.</value> <value>A signing key of type '{Type}' was ignored because its ML-DSA public key couldn't be extracted.</value>
</data> </data>
<data name="ID6297" xml:space="preserve">
<value>The session '{Identifier}' was no longer valid.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

20
src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs

@ -232,6 +232,17 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
/// </returns> /// </returns>
ValueTask<string?> GetReferenceIdAsync(TToken token, CancellationToken cancellationToken); ValueTask<string?> GetReferenceIdAsync(TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the optional session identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session identifier associated with the token.
/// </returns>
ValueTask<string?> GetSessionIdAsync(TToken token, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves the status associated with a token. /// Retrieves the status associated with a token.
/// </summary> /// </summary>
@ -359,6 +370,15 @@ public interface IOpenIddictTokenStore<TToken> where TToken : class
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns> /// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetAuthorizationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken); ValueTask SetAuthorizationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the session identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Sets the creation date associated with a token. /// Sets the creation date associated with a token.
/// </summary> /// </summary>

6
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -133,6 +133,12 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
await Store.SetStatusAsync(authorization, Statuses.Valid, cancellationToken); await Store.SetStatusAsync(authorization, Statuses.Valid, cancellationToken);
} }
// If no creation date was explicitly specified, set it to the current time.
if (await Store.GetCreationDateAsync(authorization, cancellationToken) is null)
{
await Store.SetCreationDateAsync(authorization, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken);
}
var results = await GetValidationResultsAsync(authorization, cancellationToken); var results = await GetValidationResultsAsync(authorization, cancellationToken);
if (results.Any(static result => result != ValidationResult.Success)) if (results.Any(static result => result != ValidationResult.Success))
{ {

25
src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs

@ -132,6 +132,12 @@ public class OpenIddictSessionManager<TSession> : IOpenIddictSessionManager wher
await Store.SetStatusAsync(session, Statuses.Valid, cancellationToken); await Store.SetStatusAsync(session, Statuses.Valid, cancellationToken);
} }
// If no creation date was explicitly specified, set it to the current time.
if (await Store.GetCreationDateAsync(session, cancellationToken) is null)
{
await Store.SetCreationDateAsync(session, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken);
}
var results = await GetValidationResultsAsync(session, cancellationToken); var results = await GetValidationResultsAsync(session, cancellationToken);
if (results.Any(static result => result != ValidationResult.Success)) if (results.Any(static result => result != ValidationResult.Success))
{ {
@ -601,6 +607,21 @@ public class OpenIddictSessionManager<TSession> : IOpenIddictSessionManager wher
return Store.GetSubjectAsync(session, cancellationToken); return Store.GetSubjectAsync(session, cancellationToken);
} }
/// <summary>
/// Determines whether a given session has the specified status.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="status">The expected status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><see langword="true"/> if the session has the specified status, <see langword="false"/> otherwise.</returns>
public virtual async ValueTask<bool> HasStatusAsync(TSession session, string status, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(session);
ArgumentException.ThrowIfNullOrEmpty(status);
return string.Equals(await GetStatusAsync(session, cancellationToken), status, StringComparison.Ordinal);
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -898,6 +919,10 @@ public class OpenIddictSessionManager<TSession> : IOpenIddictSessionManager wher
ValueTask<string?> IOpenIddictSessionManager.GetSubjectAsync(object session, CancellationToken cancellationToken) ValueTask<string?> IOpenIddictSessionManager.GetSubjectAsync(object session, CancellationToken cancellationToken)
=> GetSubjectAsync((TSession) session, cancellationToken); => GetSubjectAsync((TSession) session, cancellationToken);
/// <inheritdoc/>
ValueTask<bool> IOpenIddictSessionManager.HasStatusAsync(object session, string status, CancellationToken cancellationToken)
=> HasStatusAsync((TSession) session, status, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
IAsyncEnumerable<object> IOpenIddictSessionManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) IAsyncEnumerable<object> IOpenIddictSessionManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken)
=> ListAsync(count, offset, cancellationToken); => ListAsync(count, offset, cancellationToken);

28
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -134,6 +134,12 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
await Store.SetStatusAsync(token, Statuses.Valid, cancellationToken); await Store.SetStatusAsync(token, Statuses.Valid, cancellationToken);
} }
// If no creation date was explicitly specified, set it to the current time.
if (await Store.GetCreationDateAsync(token, cancellationToken) is null)
{
await Store.SetCreationDateAsync(token, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken);
}
// If a reference identifier was set, obfuscate it. // If a reference identifier was set, obfuscate it.
var identifier = await Store.GetReferenceIdAsync(token, cancellationToken); var identifier = await Store.GetReferenceIdAsync(token, cancellationToken);
if (!string.IsNullOrEmpty(identifier)) if (!string.IsNullOrEmpty(identifier))
@ -632,6 +638,22 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
return Store.GetReferenceIdAsync(token, cancellationToken); return Store.GetReferenceIdAsync(token, cancellationToken);
} }
/// <summary>
/// Retrieves the optional session identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the session identifier associated with the token.
/// </returns>
public virtual ValueTask<string?> GetSessionIdAsync(TToken token, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(token);
return Store.GetSessionIdAsync(token, cancellationToken);
}
/// <summary> /// <summary>
/// Retrieves the status associated with a token. /// Retrieves the status associated with a token.
/// </summary> /// </summary>
@ -820,6 +842,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
await Store.SetPropertiesAsync(token, descriptor.Properties.ToImmutableDictionary(), cancellationToken); await Store.SetPropertiesAsync(token, descriptor.Properties.ToImmutableDictionary(), cancellationToken);
await Store.SetRedemptionDateAsync(token, descriptor.RedemptionDate, cancellationToken); await Store.SetRedemptionDateAsync(token, descriptor.RedemptionDate, cancellationToken);
await Store.SetReferenceIdAsync(token, descriptor.ReferenceId, cancellationToken); await Store.SetReferenceIdAsync(token, descriptor.ReferenceId, cancellationToken);
await Store.SetSessionIdAsync(token, descriptor.SessionId, cancellationToken);
await Store.SetStatusAsync(token, descriptor.Status, cancellationToken); await Store.SetStatusAsync(token, descriptor.Status, cancellationToken);
await Store.SetSubjectAsync(token, descriptor.Subject, cancellationToken); await Store.SetSubjectAsync(token, descriptor.Subject, cancellationToken);
await Store.SetTypeAsync(token, descriptor.Type, cancellationToken); await Store.SetTypeAsync(token, descriptor.Type, cancellationToken);
@ -848,6 +871,7 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
descriptor.Payload = await Store.GetPayloadAsync(token, cancellationToken); descriptor.Payload = await Store.GetPayloadAsync(token, cancellationToken);
descriptor.RedemptionDate = await Store.GetRedemptionDateAsync(token, cancellationToken); descriptor.RedemptionDate = await Store.GetRedemptionDateAsync(token, cancellationToken);
descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken); descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken);
descriptor.SessionId = await Store.GetSessionIdAsync(token, cancellationToken);
descriptor.Status = await Store.GetStatusAsync(token, cancellationToken); descriptor.Status = await Store.GetStatusAsync(token, cancellationToken);
descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken); descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken);
descriptor.Type = await Store.GetTypeAsync(token, cancellationToken); descriptor.Type = await Store.GetTypeAsync(token, cancellationToken);
@ -1272,6 +1296,10 @@ public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TTok
ValueTask<string?> IOpenIddictTokenManager.GetReferenceIdAsync(object token, CancellationToken cancellationToken) ValueTask<string?> IOpenIddictTokenManager.GetReferenceIdAsync(object token, CancellationToken cancellationToken)
=> GetReferenceIdAsync((TToken) token, cancellationToken); => GetReferenceIdAsync((TToken) token, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictTokenManager.GetSessionIdAsync(object token, CancellationToken cancellationToken)
=> GetSessionIdAsync((TToken) token, cancellationToken);
/// <inheritdoc/> /// <inheritdoc/>
ValueTask<string?> IOpenIddictTokenManager.GetStatusAsync(object token, CancellationToken cancellationToken) ValueTask<string?> IOpenIddictTokenManager.GetStatusAsync(object token, CancellationToken cancellationToken)
=> GetStatusAsync((TToken) token, cancellationToken); => GetStatusAsync((TToken) token, cancellationToken);

2
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs

@ -644,7 +644,7 @@ public class OpenIddictEntityFrameworkSessionStore<
session.Authorization = await context.Set<TAuthorization>().FindAsync( session.Authorization = await context.Set<TAuthorization>().FindAsync(
cancellationToken, ConvertIdentifierFromString(identifier)) cancellationToken, ConvertIdentifierFromString(identifier))
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244));
} }
else else

63
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs

@ -437,6 +437,33 @@ public class OpenIddictEntityFrameworkTokenStore<
return new(token.ReferenceId); return new(token.ReferenceId);
} }
/// <inheritdoc/>
public virtual async ValueTask<string?> GetSessionIdAsync(TToken token, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(token);
// If the session is not attached to the token, try to load it manually.
if (token.Session is null)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var reference = context.Entry(token).Reference(static entry => entry.Session);
if (reference.EntityEntry.State is EntityState.Detached)
{
return null;
}
await reference.LoadAsync(cancellationToken);
}
if (token.Session is null)
{
return null;
}
return ConvertIdentifierToString(token.Session.Id);
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask<string?> GetStatusAsync(TToken token, CancellationToken cancellationToken) public virtual ValueTask<string?> GetStatusAsync(TToken token, CancellationToken cancellationToken)
{ {
@ -855,7 +882,7 @@ public class OpenIddictEntityFrameworkTokenStore<
token.Authorization = await context.Set<TAuthorization>().FindAsync( token.Authorization = await context.Set<TAuthorization>().FindAsync(
cancellationToken, ConvertIdentifierFromString(identifier)) cancellationToken, ConvertIdentifierFromString(identifier))
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244));
} }
else else
@ -964,6 +991,40 @@ public class OpenIddictEntityFrameworkTokenStore<
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
/// <inheritdoc/>
public virtual async ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(token);
if (!string.IsNullOrEmpty(identifier))
{
var context = await Context.GetDbContextAsync(cancellationToken);
token.Session = await context.Set<TSession>().FindAsync(
cancellationToken, ConvertIdentifierFromString(identifier))
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244));
}
else
{
// If the session is not attached to the token, try to load it manually.
if (token.Session is null)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var reference = context.Entry(token).Reference(static entry => entry.Session);
if (reference.EntityEntry.State is EntityState.Detached)
{
return;
}
await reference.LoadAsync(cancellationToken);
}
token.Session = null;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken)
{ {

2
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs

@ -654,7 +654,7 @@ public class OpenIddictEntityFrameworkCoreSessionStore<
session.Authorization = await context.Set<TAuthorization>() session.Authorization = await context.Set<TAuthorization>()
.FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244));
} }
else else

63
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs

@ -418,6 +418,33 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
return new(token.ReferenceId); return new(token.ReferenceId);
} }
/// <inheritdoc/>
public virtual async ValueTask<string?> GetSessionIdAsync(TToken token, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(token);
// If the session is not attached to the token, try to load it manually.
if (token.Session is null)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var reference = context.Entry(token).Reference(static entry => entry.Session);
if (reference.EntityEntry.State is EntityState.Detached)
{
return null;
}
await reference.LoadAsync(cancellationToken);
}
if (token.Session is null)
{
return null;
}
return ConvertIdentifierToString(token.Session.Id);
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask<string?> GetStatusAsync(TToken token, CancellationToken cancellationToken) public virtual ValueTask<string?> GetStatusAsync(TToken token, CancellationToken cancellationToken)
{ {
@ -927,7 +954,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
token.Authorization = await context.Set<TAuthorization>() token.Authorization = await context.Set<TAuthorization>()
.FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244));
} }
else else
@ -950,6 +977,40 @@ public class OpenIddictEntityFrameworkCoreTokenStore<
} }
} }
/// <inheritdoc/>
public virtual async ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(token);
if (!string.IsNullOrEmpty(identifier))
{
var context = await Context.GetDbContextAsync(cancellationToken);
token.Session = await context.Set<TSession>()
.FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244));
}
else
{
// If the session is not attached to the token, try to load it manually.
if (token.Session is null)
{
var context = await Context.GetDbContextAsync(cancellationToken);
var reference = context.Entry(token).Reference(static entry => entry.Session);
if (reference.EntityEntry.State is EntityState.Detached)
{
return;
}
await reference.LoadAsync(cancellationToken);
}
token.Session = null;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask SetCreationDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken) public virtual ValueTask SetCreationDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken)
{ {

18
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs

@ -319,6 +319,14 @@ public class OpenIddictMongoDbTokenStore<
return new(token.ReferenceId); return new(token.ReferenceId);
} }
/// <inheritdoc/>
public virtual ValueTask<string?> GetSessionIdAsync(TToken token, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(token);
return new(token.SessionId != ObjectId.Empty ? token.SessionId.ToString() : null);
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask<string?> GetStatusAsync(TToken token, CancellationToken cancellationToken) public virtual ValueTask<string?> GetStatusAsync(TToken token, CancellationToken cancellationToken)
{ {
@ -622,6 +630,16 @@ public class OpenIddictMongoDbTokenStore<
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
/// <inheritdoc/>
public virtual ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(token);
token.SessionId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty;
return ValueTask.CompletedTask;
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken)
{ {

5
src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs

@ -189,6 +189,11 @@ public static partial class OpenIddictServerEvents
/// </summary> /// </summary>
public string? AuthorizationId { get; set; } public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the session entry identifier associated with the token, if applicable.
/// </summary>
public string? SessionId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the token entry identifier associated with the token, if applicable. /// Gets or sets the token entry identifier associated with the token, if applicable.
/// </summary> /// </summary>

1
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -82,6 +82,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireRevocationRequest>(); builder.Services.TryAddSingleton<RequireRevocationRequest>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>(); builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>(); builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireSessionIdResolved>();
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>(); builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();
builder.Services.TryAddSingleton<RequireSubjectTokenValidated>(); builder.Services.TryAddSingleton<RequireSubjectTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>(); builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();

14
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -655,6 +655,20 @@ public static class OpenIddictServerHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if no session identifier is resolved from the token.
/// </summary>
public sealed class RequireSessionIdResolved : IOpenIddictServerHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new(!string.IsNullOrEmpty(context.SessionId));
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if sliding refresh token expiration was disabled. /// Represents a filter that excludes the associated handlers if sliding refresh token expiration was disabled.
/// </summary> /// </summary>

47
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -44,6 +44,7 @@ public static partial class OpenIddictServerHandlers
ValidateProofOfPossession.Descriptor, ValidateProofOfPossession.Descriptor,
ValidateTokenEntry.Descriptor, ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor, ValidateAuthorizationEntry.Descriptor,
ValidateSessionEntry.Descriptor,
/* /*
* Token generation: * Token generation:
@ -832,6 +833,7 @@ public static partial class OpenIddictServerHandlers
.SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken)) .SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken))
.SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken)) .SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken))
.SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken)) .SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken))
.SetSessionId(context.SessionId = await manager.GetSessionIdAsync(token, context.CancellationToken))
.SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken)) .SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken))
.SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken)); .SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken));
} }
@ -1413,6 +1415,50 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting tokens whose
/// associated session entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class ValidateSessionEntry : IOpenIddictServerHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireSessionIdResolved>()
.UseSingletonHandler<ValidateSessionEntry>()
.SetOrder(ValidateAuthorizationEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
public async ValueTask HandleAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.SessionId), SR.GetResourceString(SR.ID4022));
var manager = context.ServiceProvider.GetService<IOpenIddictSessionManager>()
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
var session = await manager.FindByIdAsync(context.SessionId, context.CancellationToken);
if (session is null || !await manager.HasStatusAsync(session, Statuses.Valid, context.CancellationToken))
{
context.Logger.LogInformation(6297, SR.GetResourceString(SR.ID6297), context.SessionId);
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2210),
uri: SR.FormatID8000(SR.ID2210));
return;
}
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens.
/// </summary> /// </summary>
@ -1488,6 +1534,7 @@ public static partial class OpenIddictServerHandlers
CreationDate = context.Principal.GetCreationDate(), CreationDate = context.Principal.GetCreationDate(),
ExpirationDate = context.Principal.GetExpirationDate(), ExpirationDate = context.Principal.GetExpirationDate(),
Principal = context.Principal, Principal = context.Principal,
SessionId = context.Principal.GetSessionId(),
Type = context.TokenType Type = context.TokenType
}; };

10
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -592,6 +592,16 @@ public sealed class OpenIddictValidationBuilder
public OpenIddictValidationBuilder EnableAuthorizationEntryValidation() public OpenIddictValidationBuilder EnableAuthorizationEntryValidation()
=> Configure(options => options.EnableAuthorizationEntryValidation = true); => Configure(options => options.EnableAuthorizationEntryValidation = true);
/// <summary>
/// Enables session validation so that a database call is made for each API request
/// to ensure the session associated with the access token is still valid.
/// Note: enabling this option may have an impact on performance and
/// can only be used with an OpenIddict-based authorization server.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder EnableSessionEntryValidation()
=> Configure(options => options.EnableSessionEntryValidation = true);
/// <summary> /// <summary>
/// Enables token validation so that a database call is made for each API request /// Enables token validation so that a database call is made for each API request
/// to ensure the token entry associated with the access token is still valid. /// to ensure the token entry associated with the access token is still valid.

8
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -172,15 +172,11 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
builder.AddError(SR.GetResourceString(SR.ID0132)); builder.AddError(SR.GetResourceString(SR.ID0132));
} }
if (options.EnableAuthorizationEntryValidation) if (options.EnableAuthorizationEntryValidation ||
options.EnableSessionEntryValidation || options.EnableTokenEntryValidation)
{ {
builder.AddError(SR.GetResourceString(SR.ID0133)); builder.AddError(SR.GetResourceString(SR.ID0133));
} }
if (options.EnableTokenEntryValidation)
{
builder.AddError(SR.GetResourceString(SR.ID0134));
}
} }
var now = options.TimeProvider.GetUtcNow().LocalDateTime; var now = options.TimeProvider.GetUtcNow().LocalDateTime;

5
src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs

@ -178,6 +178,11 @@ public static partial class OpenIddictValidationEvents
/// </summary> /// </summary>
public string? AuthorizationId { get; set; } public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the session entry identifier associated with the token, if applicable.
/// </summary>
public string? SessionId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the token entry identifier associated with the token, if applicable. /// Gets or sets the token entry identifier associated with the token, if applicable.
/// </summary> /// </summary>

2
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -45,6 +45,8 @@ public static class OpenIddictValidationExtensions
builder.Services.TryAddSingleton<RequireIntrospectionRequest>(); builder.Services.TryAddSingleton<RequireIntrospectionRequest>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>(); builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireLocalValidation>(); builder.Services.TryAddSingleton<RequireLocalValidation>();
builder.Services.TryAddSingleton<RequireSessionEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireSessionIdResolved>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>(); builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>(); builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>(); builder.Services.TryAddSingleton<RequireTokenIdResolved>();

28
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -123,6 +123,34 @@ public static class OpenIddictValidationHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if session validation was not enabled.
/// </summary>
public sealed class RequireSessionEntryValidationEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new(context.Options.EnableSessionEntryValidation);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no session identifier is resolved from the token.
/// </summary>
public sealed class RequireSessionIdResolved : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new(!string.IsNullOrEmpty(context.SessionId));
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if token audience validation was disabled. /// Represents a filter that excludes the associated handlers if token audience validation was disabled.
/// </summary> /// </summary>

48
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -42,6 +42,7 @@ public static partial class OpenIddictValidationHandlers
ValidateProofOfPossession.Descriptor, ValidateProofOfPossession.Descriptor,
ValidateTokenEntry.Descriptor, ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor, ValidateAuthorizationEntry.Descriptor,
ValidateSessionEntry.Descriptor,
/* /*
* Token generation: * Token generation:
@ -586,6 +587,7 @@ public static partial class OpenIddictValidationHandlers
.SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken)) .SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken))
.SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken)) .SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken))
.SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken)) .SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken))
.SetSessionId(context.SessionId = await manager.GetSessionIdAsync(token, context.CancellationToken))
.SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken)) .SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken))
.SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken)); .SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken));
} }
@ -955,7 +957,7 @@ public static partial class OpenIddictValidationHandlers
Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationId), SR.GetResourceString(SR.ID4018)); Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationId), SR.GetResourceString(SR.ID4018));
var manager = context.ServiceProvider.GetService<IOpenIddictAuthorizationManager>() var manager = context.ServiceProvider.GetService<IOpenIddictAuthorizationManager>()
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0142)); ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0139));
var authorization = await manager.FindByIdAsync(context.AuthorizationId, context.CancellationToken); var authorization = await manager.FindByIdAsync(context.AuthorizationId, context.CancellationToken);
if (authorization is null || !await manager.HasStatusAsync(authorization, Statuses.Valid, context.CancellationToken)) if (authorization is null || !await manager.HasStatusAsync(authorization, Statuses.Valid, context.CancellationToken))
@ -972,6 +974,50 @@ public static partial class OpenIddictValidationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting tokens whose
/// associated session entry is no longer valid (e.g was revoked).
/// </summary>
public sealed class ValidateSessionEntry : IOpenIddictValidationHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireSessionEntryValidationEnabled>()
.AddFilter<RequireSessionIdResolved>()
.UseSingletonHandler<ValidateSessionEntry>()
.SetOrder(ValidateAuthorizationEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidateTokenContext context)
{
ArgumentNullException.ThrowIfNull(context);
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.SessionId), SR.GetResourceString(SR.ID4022));
var manager = context.ServiceProvider.GetService<IOpenIddictSessionManager>()
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0139));
var session = await manager.FindByIdAsync(context.SessionId, context.CancellationToken);
if (session is null || !await manager.HasStatusAsync(session, Statuses.Valid, context.CancellationToken))
{
context.Logger.LogInformation(6297, SR.GetResourceString(SR.ID6297), context.SessionId);
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2210),
uri: SR.FormatID8000(SR.ID2210));
return;
}
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens.
/// </summary> /// </summary>

8
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -101,6 +101,14 @@ public sealed class OpenIddictValidationOptions
/// </summary> /// </summary>
public bool EnableAuthorizationEntryValidation { get; set; } public bool EnableAuthorizationEntryValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a database call is made
/// to validate the session entry associated with the received tokens.
/// Note: enabling this option may have an impact on performance and
/// can only be used with an OpenIddict-based authorization server.
/// </summary>
public bool EnableSessionEntryValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether a database call is made /// Gets or sets a boolean indicating whether a database call is made
/// to validate the token entry associated with the received tokens. /// to validate the token entry associated with the received tokens.

1155
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

File diff suppressed because it is too large

316
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs

@ -630,4 +630,320 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.Equal(SR.FormatID0005(TokenTypeIdentifiers.Private.AuthorizationCode, TokenTypeIdentifiers.AccessToken), exception.Message); Assert.Equal(SR.FormatID0005(TokenTypeIdentifiers.Private.AuthorizationCode, TokenTypeIdentifiers.AccessToken), exception.Message);
} }
[Fact]
public async Task ValidateToken_RequestIsRejectedWhenAuthorizationAssociatedWithTokenCannotBeFound()
{
// Arrange
var manager = CreateAuthorizationManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateTokenManager(mock =>
{
var token = new OpenIddictToken();
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeIdentifiers.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.TokenExchange,
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateToken_RequestIsRejectedWhenAuthorizationAssociatedWithTokenIsInvalid()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var manager = CreateAuthorizationManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateTokenManager(mock =>
{
var token = new OpenIddictToken();
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeIdentifiers.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC",
ActorTokenType = TokenTypeIdentifiers.AccessToken,
GrantType = GrantTypes.TokenExchange,
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateToken_RequestIsRejectedWhenSessionAssociatedWithTokenCannotBeFound()
{
// Arrange
var manager = CreateSessionManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateTokenManager(mock =>
{
var token = new OpenIddictToken();
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeIdentifiers.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetSessionIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5");
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.TokenExchange,
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2210), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2210), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateToken_RequestIsRejectedWhenSessionAssociatedWithTokenIsInvalid()
{
// Arrange
var session = new OpenIddictSession();
var manager = CreateSessionManager(mock =>
{
mock.Setup(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny<CancellationToken>()))
.ReturnsAsync(session);
mock.Setup(manager => manager.HasStatusAsync(session, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetSessionId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return ValueTask.CompletedTask;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(CreateTokenManager(mock =>
{
var token = new OpenIddictToken();
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeIdentifiers.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetSessionIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5");
}));
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC",
ActorTokenType = TokenTypeIdentifiers.AccessToken,
GrantType = GrantTypes.TokenExchange,
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2210), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2210), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(session, Statuses.Valid, It.IsAny<CancellationToken>()), Times.Once());
}
} }

17
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -5203,12 +5203,14 @@ public abstract partial class OpenIddictServerIntegrationTests
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>() .SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultResourceEntity<OpenIddictResource>() .SetDefaultResourceEntity<OpenIddictResource>()
.SetDefaultScopeEntity<OpenIddictScope>() .SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultSessionEntity<OpenIddictSession>()
.SetDefaultTokenEntity<OpenIddictToken>(); .SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateApplicationManager()) options.Services.AddSingleton(CreateApplicationManager())
.AddSingleton(CreateAuthorizationManager()) .AddSingleton(CreateAuthorizationManager())
.AddSingleton(CreateResourceManager()) .AddSingleton(CreateResourceManager())
.AddSingleton(CreateScopeManager()) .AddSingleton(CreateScopeManager())
.AddSingleton(CreateSessionManager())
.AddSingleton(CreateTokenManager()); .AddSingleton(CreateTokenManager());
}) })
@ -5343,6 +5345,20 @@ public abstract partial class OpenIddictServerIntegrationTests
return manager.Object; return manager.Object;
} }
protected OpenIddictSessionManager<OpenIddictSession> CreateSessionManager(
Action<Mock<OpenIddictSessionManager<OpenIddictSession>>>? configuration = null)
{
var manager = new Mock<OpenIddictSessionManager<OpenIddictSession>>(
Mock.Of<IOpenIddictSessionCache<OpenIddictSession>>(),
OutputHelper.ToLogger<OpenIddictSessionManager<OpenIddictSession>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(),
Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>());
configuration?.Invoke(manager);
return manager.Object;
}
protected OpenIddictTokenManager<OpenIddictToken> CreateTokenManager( protected OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>>? configuration = null) Action<Mock<OpenIddictTokenManager<OpenIddictToken>>>? configuration = null)
{ {
@ -5361,5 +5377,6 @@ public abstract partial class OpenIddictServerIntegrationTests
public class OpenIddictAuthorization; public class OpenIddictAuthorization;
public class OpenIddictResource; public class OpenIddictResource;
public class OpenIddictScope; public class OpenIddictScope;
public class OpenIddictSession;
public class OpenIddictToken; public class OpenIddictToken;
} }

17
test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

@ -385,9 +385,11 @@ public abstract class OpenIddictValidationIntegrationTests
.AddCore(options => .AddCore(options =>
{ {
options.SetDefaultAuthorizationEntity<OpenIddictAuthorization>() options.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultSessionEntity<OpenIddictSession>()
.SetDefaultTokenEntity<OpenIddictToken>(); .SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateAuthorizationManager()) options.Services.AddSingleton(CreateAuthorizationManager())
.AddSingleton(CreateSessionManager())
.AddSingleton(CreateTokenManager()); .AddSingleton(CreateTokenManager());
}) })
@ -435,6 +437,20 @@ public abstract class OpenIddictValidationIntegrationTests
return manager.Object; return manager.Object;
} }
protected OpenIddictSessionManager<OpenIddictSession> CreateSessionManager(
Action<Mock<OpenIddictSessionManager<OpenIddictSession>>>? configuration = null)
{
var manager = new Mock<OpenIddictSessionManager<OpenIddictSession>>(
Mock.Of<IOpenIddictSessionCache<OpenIddictSession>>(),
OutputHelper.ToLogger<OpenIddictSessionManager<OpenIddictSession>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(),
Mock.Of<IOpenIddictSessionStore<OpenIddictSession>>());
configuration?.Invoke(manager);
return manager.Object;
}
protected OpenIddictTokenManager<OpenIddictToken> CreateTokenManager( protected OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>>? configuration = null) Action<Mock<OpenIddictTokenManager<OpenIddictToken>>>? configuration = null)
{ {
@ -450,5 +466,6 @@ public abstract class OpenIddictValidationIntegrationTests
} }
public class OpenIddictAuthorization; public class OpenIddictAuthorization;
public class OpenIddictSession;
public class OpenIddictToken; public class OpenIddictToken;
} }

46
test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs

@ -308,7 +308,7 @@ public class OpenIddictValidationConfigurationTests
} }
[Fact] [Fact]
public void Validate_ReturnsAnErrorWhenAuthorizationOrTokenEntryValidationIsEnabledInIntrospectionMode() public void Validate_ReturnsAnErrorWhenAuthorizationEntryValidationIsEnabledInIntrospectionMode()
{ {
// Arrange // Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
@ -320,6 +320,49 @@ public class OpenIddictValidationConfigurationTests
options.ClientId = "client_id"; options.ClientId = "client_id";
options.ClientSecret = "client_secret"; options.ClientSecret = "client_secret";
options.EnableAuthorizationEntryValidation = true; options.EnableAuthorizationEntryValidation = true;
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal);
}
[Fact]
public void Validate_ReturnsAnErrorWhenSessionEntryValidationIsEnabledInIntrospectionMode()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration");
options.ClientId = "client_id";
options.ClientSecret = "client_secret";
options.EnableSessionEntryValidation = true;
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
// Act
var result = configuration.Validate(name: null, options);
// Assert
Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal);
}
[Fact]
public void Validate_ReturnsAnErrorWhenTokenEntryValidationIsEnabledInIntrospectionMode()
{
// Arrange
var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider());
var options = CreateBaseOptions();
options.ValidationType = OpenIddictValidationType.Introspection;
options.Issuer = new Uri("https://www.contoso.com/");
options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration");
options.ClientId = "client_id";
options.ClientSecret = "client_secret";
options.EnableTokenEntryValidation = true; options.EnableTokenEntryValidation = true;
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration()); options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new OpenIddictConfiguration());
@ -328,7 +371,6 @@ public class OpenIddictValidationConfigurationTests
// Assert // Assert
Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal);
Assert.Contains(SR.GetResourceString(SR.ID0134), result.Failures!, StringComparer.Ordinal);
} }
[Fact] [Fact]

Loading…
Cancel
Save