Browse Source

Add client assertions support to the server stack

pull/1896/head
Kévin Chalet 2 years ago
parent
commit
d6c9c0b35c
  1. 42
      Directory.Packages.props
  2. 6
      sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config
  3. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  4. 6
      sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config
  5. 32
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  6. 34
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  7. 6
      src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
  8. 12
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  9. 2
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  10. 18
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  11. 21
      src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
  12. 8
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs
  13. 2
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
  14. 18
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  15. 12
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  16. 4
      src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs
  17. 52
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  18. 4
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  19. 12
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  20. 34
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  21. 112
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  22. 6
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  23. 70
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  24. 7
      src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs
  25. 41
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs
  26. 7
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs
  27. 41
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
  28. 6
      src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs
  29. 34
      src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs
  30. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  31. 15
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  32. 6
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
  33. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  34. 5
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  35. 7
      src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
  36. 51
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  37. 2
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  38. 34
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  39. 82
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  40. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  41. 92
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  42. 82
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  43. 213
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  44. 82
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  45. 510
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  46. 6
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs
  47. 5
      src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
  48. 6
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  49. 85
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
  50. 141
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  51. 127
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
  52. 1
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs
  53. 90
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs

42
Directory.Packages.props

@ -36,9 +36,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="2.1.6" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="2.1.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.Owin.Security" Version="4.2.2" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17763.1000" />
<PackageVersion Include="MongoDB.Bson" Version="2.11.6" />
@ -94,9 +94,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="2.1.6" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="2.1.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.Owin.Security" Version="4.2.2" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17763.1000" />
<PackageVersion Include="MongoDB.Bson" Version="2.11.6" />
@ -152,9 +152,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="2.1.6" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="2.1.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.Owin.Security" Version="4.2.2" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17763.1000" />
<PackageVersion Include="MongoDB.Bson" Version="2.11.6" />
@ -234,9 +234,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="6.0.16" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="MongoDB.Bson" Version="2.20.0" />
<PackageVersion Include="MongoDB.Driver" Version="2.20.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.5.0" />
@ -279,9 +279,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="7.0.5" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="MongoDB.Bson" Version="2.20.0" />
<PackageVersion Include="MongoDB.Driver" Version="2.20.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.5.0" />
@ -343,9 +343,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="2.1.6" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="2.1.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17763.1000" />
<PackageVersion Include="MongoDB.Bson" Version="2.11.6" />
<PackageVersion Include="MongoDB.Driver" Version="2.11.6" />
@ -395,9 +395,9 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="3.1.32" />
<PackageVersion Include="Microsoft.Extensions.WebEncoders" Version="3.1.32" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols" Version="7.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="7.0.0" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.17763.1000" />
<PackageVersion Include="MongoDB.Bson" Version="2.11.6" />
<PackageVersion Include="MongoDB.Driver" Version="2.11.6" />

6
sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
For more information on how to configure your ASP.NET application, please visit
https://go.microsoft.com/fwlink/?LinkId=301880
@ -72,8 +72,8 @@
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.1" newVersion="4.0.1.1" />
<assemblyIdentity name="System.Memory" culture="neutral" publicKeyToken="cc7b13ffcd2ddd51" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -183,6 +183,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
ApplicationType = ApplicationTypes.Web,
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ClientType = ClientTypes.Confidential,
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
RedirectUris =
@ -219,6 +220,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
{
ApplicationType = ApplicationTypes.Native,
ClientId = "postman",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris =

6
sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
For more information on how to configure your ASP.NET application, please visit
https://go.microsoft.com/fwlink/?LinkId=301880
@ -96,8 +96,8 @@
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.1" newVersion="4.0.1.1" />
<assemblyIdentity name="System.Memory" culture="neutral" publicKeyToken="cc7b13ffcd2ddd51" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

32
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs

@ -1,5 +1,7 @@
using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Client;
using OpenIddict.Sandbox.AspNetCore.Client.Models;
using Quartz;
@ -103,11 +105,29 @@ public class Startup
ProviderName = "Local",
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative)
PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),
// Instead of sending a client secret, this application authenticates by
// generating client assertions that are signed using a private signing key.
//
// As such, no client secret is set, but an ECDSA key is registered and used by
// the OpenIddict client to automatically generate client assertions when needed.
//
// Note: while the server only needs access to the public key, the client needs
// to know the private key to be able to generate and sign the client assertions.
SigningCredentials =
{
new SigningCredentials(GetECDsaSigningKey($"""
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMGxf/eMzKuW2F8KKWPJo3bwlrO68rK5+xCeO1atwja2oAoGCCqGSM49
AwEHoUQDQgAEI23kaVsRRAWIez/pqEZOByJFmlXda6iSQ4QqcH23Ir8aYPPX5lsV
nBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END EC PRIVATE KEY-----
"""), SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.Sha256)
}
});
// Register the Web providers integrations.
@ -138,6 +158,14 @@ public class Startup
.SetRedirectUri("callback/login/reddit")
.SetDuration("permanent");
});
static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<char> key)
{
var algorithm = ECDsa.Create();
algorithm.ImportFromPem(key);
return new ECDsaSecurityKey(algorithm);
}
});
services.AddHttpClient();

34
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -1,4 +1,6 @@
using System.Globalization;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
using static OpenIddict.Abstractions.OpenIddictConstants;
@ -34,6 +36,7 @@ public class Worker : IHostedService
// to apply a relaxed redirect_uri validation policy that allows specifying a random port.
ApplicationType = ApplicationTypes.Native,
ClientId = "console",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "Console client application",
DisplayNames =
@ -73,13 +76,30 @@ public class Worker : IHostedService
{
ApplicationType = ApplicationTypes.Web,
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ClientType = ClientTypes.Confidential,
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
DisplayNames =
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente MVC"
},
JsonWebKeySet = new JsonWebKeySet
{
Keys =
{
// Instead of sending a client secret, this application authenticates by
// generating client assertions that are signed using an ECDSA signing key.
//
// Note: while the client needs access to the private key, the server only needs
// to know the public key to be able to validate the client assertions it receives.
JsonWebKeyConverter.ConvertFromECDsaSecurityKey(GetECDsaSigningKey($"""
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEI23kaVsRRAWIez/pqEZOByJFmlXd
a6iSQ4QqcH23Ir8aYPPX5lsVnBsExNsl7SOYOiIhgTaX6+PTS7yxTnmvSw==
-----END PUBLIC KEY-----
"""))
}
},
RedirectUris =
{
new Uri("https://localhost:44381/callback/login/local")
@ -114,6 +134,7 @@ public class Worker : IHostedService
{
ApplicationType = ApplicationTypes.Native,
ClientId = "winforms",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "WinForms client application",
DisplayNames =
@ -149,6 +170,7 @@ public class Worker : IHostedService
{
ApplicationType = ApplicationTypes.Native,
ClientId = "wpf",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "WPF client application",
DisplayNames =
@ -187,6 +209,7 @@ public class Worker : IHostedService
{
ClientId = "resource_server",
ClientSecret = "80B552BB-4CD8-48DA-946E-0815E0147DD2",
ClientType = ClientTypes.Confidential,
Permissions =
{
Permissions.Endpoints.Introspection
@ -209,6 +232,7 @@ public class Worker : IHostedService
{
ApplicationType = ApplicationTypes.Native,
ClientId = "postman",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris =
@ -236,6 +260,14 @@ public class Worker : IHostedService
}
});
}
static ECDsaSecurityKey GetECDsaSigningKey(ReadOnlySpan<char> key)
{
var algorithm = ECDsa.Create();
algorithm.ImportFromPem(key);
return new ECDsaSecurityKey(algorithm);
}
}
static async Task RegisterScopesAsync(IServiceProvider provider)

6
src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs

@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Abstractions;
@ -46,6 +47,11 @@ public class OpenIddictApplicationDescriptor
/// </summary>
public Dictionary<CultureInfo, string> DisplayNames { get; } = new();
/// <summary>
/// Gets or sets the JSON Web Key Set associated with the application.
/// </summary>
public JsonWebKeySet? JsonWebKeySet { get; set; }
/// <summary>
/// Gets the permissions associated with the application.
/// </summary>

12
src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs

@ -9,6 +9,7 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Abstractions;
@ -236,6 +237,17 @@ public interface IOpenIddictApplicationManager
/// </returns>
ValueTask<string?> GetIdAsync(object application, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the JSON Web Key Set associated with an application.
/// </summary>
/// <param name="application">The application.</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 JSON Web Key Set associated with the application.
/// </returns>
ValueTask<JsonWebKeySet?> GetJsonWebKeySetAsync(object application, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the localized display name associated with an application
/// and corresponding to the current UI culture or one of its parents.

2
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -537,7 +537,7 @@ public static class OpenIddictConstants
{
public const string AccessToken = "access_token";
public const string AuthorizationCode = "authorization_code";
public const string ClientAssertionToken = "client_assertion_token";
public const string ClientAssertion = "client_assertion";
public const string DeviceCode = "device_code";
public const string IdToken = "id_token";
public const string RefreshToken = "refresh_token";

18
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1735,7 +1735,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>This client application is not allowed to use the device endpoint.</value>
</data>
<data name="ID2057" xml:space="preserve">
<value>The '{0}' and '{1}' parameters are required when using the client credentials grant.</value>
<value>The '{0}' or '{1}' parameter must be specified when using the client credentials grant.</value>
</data>
<data name="ID2058" xml:space="preserve">
<value>The '{0}' parameter is required when using the device code grant.</value>
@ -1900,7 +1900,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>Only confidential or public applications are supported by the default application manager.</value>
</data>
<data name="ID2113" xml:space="preserve">
<value>The client secret cannot be null or empty for a confidential application.</value>
<value>The client secret cannot be null or empty for a confidential application. Alternatively, a RSA or ECDSA key (with the key use "sig") can be added to the JSON Web Key Set attached to the application if the client authenticates using client assertions.</value>
</data>
<data name="ID2114" xml:space="preserve">
<value>A client secret cannot be associated with a public application.</value>
@ -2073,6 +2073,15 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID2170" xml:space="preserve">
<value>The remote authorization server is currently unavailable or returned an invalid configuration.</value>
</data>
<data name="ID2171" xml:space="preserve">
<value>The '{0}' claim extracted from the specified client assertion is malformed or isn't of the expected type.</value>
</data>
<data name="ID2172" xml:space="preserve">
<value>The mandatory '{0}' claim cannot be found in the specified client assertion.</value>
</data>
<data name="ID2173" xml:space="preserve">
<value>The '{0}' claim returned in the specified client assertion doesn't match the expected value.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -2707,11 +2716,14 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<value>The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client secret.</value>
</data>
<data name="ID6224" xml:space="preserve">
<value>The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a client secret.</value>
<value>The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a client secret or a client assertion.</value>
</data>
<data name="ID6225" xml:space="preserve">
<value>The authentication demand was rejected because the confidential application '{ClientId}' didn't specify valid client credentials.</value>
</data>
<data name="ID6226" xml:space="preserve">
<value>The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client assertion.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

21
src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Abstractions;
@ -201,6 +202,17 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
/// </returns>
ValueTask<string?> GetIdAsync(TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the JSON Web Key Set associated with an application.
/// </summary>
/// <param name="application">The application.</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 JSON Web Key Set associated with the application.
/// </returns>
ValueTask<JsonWebKeySet?> GetJsonWebKeySetAsync(TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the permissions associated with an application.
/// </summary>
@ -365,6 +377,15 @@ public interface IOpenIddictApplicationStore<TApplication> where TApplication :
ValueTask SetDisplayNamesAsync(TApplication application,
ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken);
/// <summary>
/// Sets the JSON Web Key Set associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="set">The JSON Web Key Set associated with the application.</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 SetJsonWebKeySetAsync(TApplication application, JsonWebKeySet? set, CancellationToken cancellationToken);
/// <summary>
/// Sets the permissions associated with an application.
/// </summary>

8
src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs

@ -62,6 +62,12 @@ public static partial class OpenIddictClientDataProtectionHandlers
return default;
}
// If a specific token format is expected, return immediately if it doesn't match the expected value.
if (context.TokenFormat is not null && context.TokenFormat is not TokenFormats.Private.DataProtection)
{
return default;
}
// Note: ASP.NET Core Data Protection tokens created by the default implementation always start
// with "CfDJ8", that corresponds to the base64 representation of the "09 F0 C9 F0" value used
// by KeyRingBasedDataProtectionProvider as a Data Protection version identifier/magic header.
@ -89,7 +95,7 @@ public static partial class OpenIddictClientDataProtectionHandlers
_ when context.ValidTokenTypes.Contains(TokenTypeHints.StateToken)
=> ValidateToken(TokenTypeHints.StateToken),
// The token type is not supported by the Data Protection integration (e.g client assertion tokens).
// The token type is not supported by the Data Protection integration (e.g client assertions).
_ => null
};

2
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs

@ -295,7 +295,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// must be sent as a "dynamic" client secret using client_secret_post. Since the logic
// is the same as private_key_jwt, the configuration is amended to assume Apple supports
// private_key_jwt and an event handler is responsible for populating the client_secret
// parameter using the client assertion token once it has been generated by OpenIddict.
// parameter using the client assertion once it has been generated by OpenIddict.
if (context.Registration.ProviderType is ProviderTypes.Apple)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(

18
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -26,7 +26,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
HandleNonStandardFrontchannelErrorResponse.Descriptor,
ValidateNonStandardParameters.Descriptor,
OverrideTokenEndpoint.Descriptor,
AttachNonStandardClientAssertionTokenClaims.Descriptor,
AttachNonStandardClientAssertionClaims.Descriptor,
AttachTokenRequestNonStandardClientCredentials.Descriptor,
AdjustRedirectUriInTokenRequest.Descriptor,
OverrideValidatedBackchannelTokens.Descriptor,
@ -376,16 +376,16 @@ public static partial class OpenIddictClientWebIntegrationHandlers
/// Contains the logic responsible for amending the client
/// assertion methods for the providers that require it.
/// </summary>
public sealed class AttachNonStandardClientAssertionTokenClaims : IOpenIddictClientHandler<ProcessAuthenticationContext>
public sealed class AttachNonStandardClientAssertionClaims : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionTokenGenerated>()
.UseSingletonHandler<AttachNonStandardClientAssertionTokenClaims>()
.SetOrder(PrepareClientAssertionTokenPrincipal.Descriptor.Order + 500)
.AddFilter<RequireClientAssertionGenerated>()
.UseSingletonHandler<AttachNonStandardClientAssertionClaims>()
.SetOrder(PrepareClientAssertionPrincipal.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -397,7 +397,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.ClientAssertionTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// For client assertions to be considered valid by the Apple ID authentication service,
// the team identifier associated with the developer account MUST be used as the issuer
@ -409,8 +409,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers
{
var settings = context.Registration.GetAppleSettings();
context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, settings.TeamId);
context.ClientAssertionTokenPrincipal.SetAudiences("https://appleid.apple.com");
context.ClientAssertionPrincipal.SetClaim(Claims.Private.Issuer, settings.TeamId);
context.ClientAssertionPrincipal.SetAudiences("https://appleid.apple.com");
}
return default;
@ -450,7 +450,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// must be sent as a "dynamic" client secret using client_secret_post. Since the logic
// is the same as private_key_jwt, the configuration is amended to assume Apple supports
// private_key_jwt and an event handler is responsible for populating the client_secret
// parameter using the client assertion token once it has been generated by OpenIddict.
// parameter using the client assertion once it has been generated by OpenIddict.
if (context.Registration.ProviderType is ProviderTypes.Apple)
{
context.TokenRequest.ClientSecret = context.TokenRequest.ClientAssertion;

12
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -1131,15 +1131,15 @@ public sealed class OpenIddictClientBuilder
}
/// <summary>
/// Sets the client assertion token lifetime, after which backchannel requests
/// using an expired state token should be automatically rejected by the server.
/// Using long-lived state tokens or tokens that never expire is not recommended.
/// While discouraged, <see langword="null"/> can be specified to issue tokens that never expire.
/// Sets the client assertion lifetime, after which backchannel requests
/// using an expired client assertion should be automatically rejected by the server.
/// Using long-lived client assertion or assertions that never expire is not recommended.
/// While discouraged, <see langword="null"/> can be specified to issue assertions that never expire.
/// </summary>
/// <param name="lifetime">The access token lifetime.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder SetClientAssertionTokenLifetime(TimeSpan? lifetime)
=> Configure(options => options.ClientAssertionTokenLifetime = lifetime);
public OpenIddictClientBuilder SetClientAssertionLifetime(TimeSpan? lifetime)
=> Configure(options => options.ClientAssertionLifetime = lifetime);
/// <summary>
/// Sets the state token lifetime, after which authorization callbacks

4
src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs

@ -131,9 +131,9 @@ public static partial class OpenIddictClientEvents
public string Token { get; set; } = default!;
/// <summary>
/// Gets or sets the token type hint specified by the client, if applicable.
/// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to validate, if applicable.
/// </summary>
public string? TokenTypeHint { get; set; } = default!;
public string? TokenFormat { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the validated token is a reference token.

52
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -810,36 +810,36 @@ public static partial class OpenIddictClientEvents
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool GenerateClientAssertionToken { get; set; }
public bool GenerateClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated client
/// assertion token should be included as part of the request.
/// assertion should be included as part of the request.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool IncludeClientAssertionToken { get; set; }
public bool IncludeClientAssertion { get; set; }
/// <summary>
/// Gets or sets the generated client assertion token, if applicable.
/// The client assertion token will only be returned if
/// <see cref="IncludeClientAssertionToken"/> is set to <see langword="true"/>.
/// Gets or sets the generated client assertion, if applicable.
/// The client assertion will only be returned if
/// <see cref="IncludeClientAssertion"/> is set to <see langword="true"/>.
/// </summary>
public string? ClientAssertionToken { get; set; }
public string? ClientAssertion { get; set; }
/// <summary>
/// Gets or sets type of the generated client assertion token, if applicable.
/// The client assertion token type will only be returned if
/// <see cref="IncludeClientAssertionToken"/> is set to <see langword="true"/>.
/// Gets or sets type of the generated client assertion, if applicable.
/// The client assertion type will only be returned if
/// <see cref="IncludeClientAssertion"/> is set to <see langword="true"/>.
/// </summary>
public string? ClientAssertionTokenType { get; set; }
public string? ClientAssertionType { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that will be
/// used to create the client assertion token, if applicable.
/// used to create the client assertion, if applicable.
/// </summary>
public ClaimsPrincipal? ClientAssertionTokenPrincipal { get; set; }
public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether backchannel
@ -1055,36 +1055,36 @@ public static partial class OpenIddictClientEvents
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool GenerateClientAssertionToken { get; set; }
public bool GenerateClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated client
/// assertion token should be included as part of the request.
/// assertion should be included as part of the request.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool IncludeClientAssertionToken { get; set; }
public bool IncludeClientAssertion { get; set; }
/// <summary>
/// Gets or sets the generated client assertion token, if applicable.
/// The client assertion token will only be returned if
/// <see cref="IncludeClientAssertionToken"/> is set to <see langword="true"/>.
/// Gets or sets the generated client assertion, if applicable.
/// The client assertion will only be returned if
/// <see cref="IncludeClientAssertion"/> is set to <see langword="true"/>.
/// </summary>
public string? ClientAssertionToken { get; set; }
public string? ClientAssertion { get; set; }
/// <summary>
/// Gets or sets type of the generated client assertion token, if applicable.
/// The client assertion token type will only be returned if
/// <see cref="IncludeClientAssertionToken"/> is set to <see langword="true"/>.
/// Gets or sets type of the generated client assertion, if applicable.
/// The client assertion type will only be returned if
/// <see cref="IncludeClientAssertion"/> is set to <see langword="true"/>.
/// </summary>
public string? ClientAssertionTokenType { get; set; }
public string? ClientAssertionType { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that will be
/// used to create the client assertion token, if applicable.
/// used to create the client assertion, if applicable.
/// </summary>
public ClaimsPrincipal? ClientAssertionTokenPrincipal { get; set; }
public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that

4
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -41,8 +41,8 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenNonceValidationEnabled>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenPrincipal>();
builder.Services.TryAddSingleton<RequireChallengeClientAssertionTokenGenerated>();
builder.Services.TryAddSingleton<RequireClientAssertionTokenGenerated>();
builder.Services.TryAddSingleton<RequireChallengeClientAssertionGenerated>();
builder.Services.TryAddSingleton<RequireClientAssertionGenerated>();
builder.Services.TryAddSingleton<RequireDeviceAuthorizationGrantType>();
builder.Services.TryAddSingleton<RequireDeviceAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireFrontchannelAccessTokenValidated>();

12
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -97,9 +97,9 @@ public static class OpenIddictClientHandlerFilters
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no challenge client assertion token is generated.
/// Represents a filter that excludes the associated handlers if no challenge client assertion is generated.
/// </summary>
public sealed class RequireChallengeClientAssertionTokenGenerated : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
public sealed class RequireChallengeClientAssertionGenerated : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
@ -109,14 +109,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateClientAssertionToken);
return new(context.GenerateClientAssertion);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no client assertion token is generated.
/// Represents a filter that excludes the associated handlers if no client assertion is generated.
/// </summary>
public sealed class RequireClientAssertionTokenGenerated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public sealed class RequireClientAssertionGenerated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
@ -126,7 +126,7 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateClientAssertionToken);
return new(context.GenerateClientAssertion);
}
}

34
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -212,6 +212,13 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Note: reference tokens are only used for state tokens.
if (context.ValidTokenTypes.Count is not 1 ||
!context.ValidTokenTypes.Contains(TokenTypeHints.StateToken))
{
return;
}
// If the reference token cannot be found, don't return an error to allow another handler to validate it.
var token = await _tokenManager.FindByReferenceIdAsync(context.Token);
if (token is null)
@ -279,6 +286,12 @@ public static partial class OpenIddictClientHandlers
return;
}
// If a specific token format is expected, return immediately if it doesn't match the expected value.
if (context.TokenFormat is not null && context.TokenFormat is not TokenFormats.Jwt)
{
return;
}
// If the token cannot be read, don't return an error to allow another handler to validate it.
if (!context.SecurityTokenHandler.CanReadToken(context.Token))
{
@ -467,6 +480,13 @@ public static partial class OpenIddictClientHandlers
return;
}
// Note: token entries are only used for state tokens.
if (context.ValidTokenTypes.Count is not 1 ||
!context.ValidTokenTypes.Contains(TokenTypeHints.StateToken))
{
return;
}
// Extract the token identifier from the authentication principal.
//
// If no token identifier can be found, this indicates that the token
@ -695,7 +715,7 @@ public static partial class OpenIddictClientHandlers
{
// For client assertions, use the encryption credentials
// configured for the client registration, if available.
TokenTypeHints.ClientAssertionToken
TokenTypeHints.ClientAssertion
=> context.Registration.EncryptionCredentials.FirstOrDefault(),
// For other types of tokens, use the global encryption credentials.
@ -705,7 +725,7 @@ public static partial class OpenIddictClientHandlers
context.SigningCredentials = context.TokenType switch
{
// For client assertions, use the signing credentials configured for the client registration.
TokenTypeHints.ClientAssertionToken
TokenTypeHints.ClientAssertion
=> context.Registration.SigningCredentials.First(),
// For other types of tokens, use the global signing credentials.
@ -816,7 +836,7 @@ public static partial class OpenIddictClientHandlers
Claims.Private.Issuer or Claims.Private.TokenType => false,
Claims.Private.Audience when context.TokenType is
TokenTypeHints.ClientAssertionToken or TokenTypeHints.StateToken => false,
TokenTypeHints.ClientAssertion or TokenTypeHints.StateToken => false,
_ => true
});
@ -825,9 +845,9 @@ public static partial class OpenIddictClientHandlers
var claims = new Dictionary<string, object>(StringComparer.Ordinal);
// For client assertion tokens, set the public audience claims
// For client assertions, set the public audience claims
// using the private audience claims from the security principal.
if (context.TokenType is TokenTypeHints.ClientAssertionToken)
if (context.TokenType is TokenTypeHints.ClientAssertion)
{
var audiences = context.Principal.GetAudiences();
if (audiences.Any())
@ -853,8 +873,8 @@ public static partial class OpenIddictClientHandlers
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
// For client assertion tokens, use the generic "JWT" type.
TokenTypeHints.ClientAssertionToken => JsonWebTokenTypes.Jwt,
// For client assertions, use the generic "JWT" type.
TokenTypeHints.ClientAssertion => JsonWebTokenTypes.Jwt,
// For state tokens, use its private representation.
TokenTypeHints.StateToken => JsonWebTokenTypes.Private.StateToken,

112
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -63,9 +63,9 @@ public static partial class OpenIddictClientHandlers
ResolveTokenEndpoint.Descriptor,
EvaluateTokenRequest.Descriptor,
AttachTokenRequestParameters.Descriptor,
EvaluateGeneratedClientAssertionToken.Descriptor,
PrepareClientAssertionTokenPrincipal.Descriptor,
GenerateClientAssertionToken.Descriptor,
EvaluateGeneratedClientAssertion.Descriptor,
PrepareClientAssertionPrincipal.Descriptor,
GenerateClientAssertion.Descriptor,
AttachTokenRequestClientCredentials.Descriptor,
SendTokenRequest.Descriptor,
@ -118,9 +118,9 @@ public static partial class OpenIddictClientHandlers
ResolveDeviceAuthorizationEndpoint.Descriptor,
EvaluateDeviceAuthorizationRequest.Descriptor,
AttachDeviceAuthorizationRequestParameters.Descriptor,
EvaluateGeneratedChallengeClientAssertionToken.Descriptor,
PrepareChallengeClientAssertionTokenPrincipal.Descriptor,
GenerateChallengeClientAssertionToken.Descriptor,
EvaluateGeneratedChallengeClientAssertion.Descriptor,
PrepareChallengeClientAssertionPrincipal.Descriptor,
GenerateChallengeClientAssertion.Descriptor,
AttachDeviceAuthorizationRequestClientCredentials.Descriptor,
SendDeviceAuthorizationRequest.Descriptor,
@ -2307,7 +2307,7 @@ public static partial class OpenIddictClientHandlers
/// Contains the logic responsible for selecting the token types that should
/// be generated and optionally sent as part of the authentication demand.
/// </summary>
public sealed class EvaluateGeneratedClientAssertionToken : IOpenIddictClientHandler<ProcessAuthenticationContext>
public sealed class EvaluateGeneratedClientAssertion : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -2315,7 +2315,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<EvaluateGeneratedClientAssertionToken>()
.UseSingletonHandler<EvaluateGeneratedClientAssertion>()
.SetOrder(AttachTokenRequestParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -2328,8 +2328,8 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
(context.GenerateClientAssertionToken,
context.IncludeClientAssertionToken) = context.Registration.SigningCredentials.Count switch
(context.GenerateClientAssertion,
context.IncludeClientAssertion) = context.Registration.SigningCredentials.Count switch
{
// If a token request is going to be sent and if at least one signing key was
// attached to the client registration, generate and include a client assertion
@ -2346,18 +2346,18 @@ public static partial class OpenIddictClientHandlers
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the client assertion token, if one is going to be sent.
/// used to generate the client assertion, if one is going to be sent.
/// </summary>
public sealed class PrepareClientAssertionTokenPrincipal : IOpenIddictClientHandler<ProcessAuthenticationContext>
public sealed class PrepareClientAssertionPrincipal : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionTokenGenerated>()
.UseSingletonHandler<PrepareClientAssertionTokenPrincipal>()
.SetOrder(EvaluateGeneratedClientAssertionToken.Descriptor.Order + 1_000)
.AddFilter<RequireClientAssertionGenerated>()
.UseSingletonHandler<PrepareClientAssertionPrincipal>()
.SetOrder(EvaluateGeneratedClientAssertion.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -2375,7 +2375,7 @@ public static partial class OpenIddictClientHandlers
var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType));
principal.SetCreationDate(DateTimeOffset.UtcNow);
var lifetime = context.Options.ClientAssertionTokenLifetime;
var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue)
{
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
@ -2407,7 +2407,7 @@ public static partial class OpenIddictClientHandlers
// Use a random GUID as the JWT unique identifier.
principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString());
context.ClientAssertionTokenPrincipal = principal;
context.ClientAssertionPrincipal = principal;
return default;
}
@ -2415,13 +2415,13 @@ public static partial class OpenIddictClientHandlers
/// <summary>
/// Contains the logic responsible for generating a client
/// assertion token for the current authentication operation.
/// assertion for the current authentication operation.
/// </summary>
public sealed class GenerateClientAssertionToken : IOpenIddictClientHandler<ProcessAuthenticationContext>
public sealed class GenerateClientAssertion : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public GenerateClientAssertionToken(IOpenIddictClientDispatcher dispatcher)
public GenerateClientAssertion(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
@ -2429,9 +2429,9 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionTokenGenerated>()
.UseScopedHandler<GenerateClientAssertionToken>()
.SetOrder(PrepareClientAssertionTokenPrincipal.Descriptor.Order + 1_000)
.AddFilter<RequireClientAssertionGenerated>()
.UseScopedHandler<GenerateClientAssertion>()
.SetOrder(PrepareClientAssertionPrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -2448,9 +2448,9 @@ public static partial class OpenIddictClientHandlers
CreateTokenEntry = false,
IsReferenceToken = false,
PersistTokenPayload = false,
Principal = context.ClientAssertionTokenPrincipal!,
Principal = context.ClientAssertionPrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.ClientAssertionToken
TokenType = TokenTypeHints.ClientAssertion
};
await _dispatcher.DispatchAsync(notification);
@ -2476,8 +2476,8 @@ public static partial class OpenIddictClientHandlers
return;
}
context.ClientAssertionToken = notification.Token;
context.ClientAssertionTokenType = notification.TokenFormat switch
context.ClientAssertion = notification.Token;
context.ClientAssertionType = notification.TokenFormat switch
{
TokenFormats.Jwt => ClientAssertionTypes.JwtBearer,
TokenFormats.Saml2 => ClientAssertionTypes.Saml2Bearer,
@ -2499,7 +2499,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<AttachTokenRequestClientCredentials>()
.SetOrder(GenerateClientAssertionToken.Descriptor.Order + 1_000)
.SetOrder(GenerateClientAssertion.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
@ -2518,10 +2518,10 @@ public static partial class OpenIddictClientHandlers
// Note: client authentication methods are mutually exclusive so the client_assertion
// and client_secret parameters MUST never be sent at the same time. For more information,
// see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.
if (context.IncludeClientAssertionToken)
if (context.IncludeClientAssertion)
{
context.TokenRequest.ClientAssertion = context.ClientAssertionToken;
context.TokenRequest.ClientAssertionType = context.ClientAssertionTokenType;
context.TokenRequest.ClientAssertion = context.ClientAssertion;
context.TokenRequest.ClientAssertionType = context.ClientAssertionType;
}
// Note: the client_secret may be null at this point (e.g for a public
@ -5332,7 +5332,7 @@ public static partial class OpenIddictClientHandlers
/// Contains the logic responsible for selecting the token types that should
/// be generated and optionally sent as part of the challenge demand.
/// </summary>
public sealed class EvaluateGeneratedChallengeClientAssertionToken : IOpenIddictClientHandler<ProcessChallengeContext>
public sealed class EvaluateGeneratedChallengeClientAssertion : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -5340,7 +5340,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireDeviceAuthorizationRequest>()
.UseSingletonHandler<EvaluateGeneratedChallengeClientAssertionToken>()
.UseSingletonHandler<EvaluateGeneratedChallengeClientAssertion>()
.SetOrder(AttachDeviceAuthorizationRequestParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -5353,8 +5353,8 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
(context.GenerateClientAssertionToken,
context.IncludeClientAssertionToken) = context.Registration.SigningCredentials.Count switch
(context.GenerateClientAssertion,
context.IncludeClientAssertion) = context.Registration.SigningCredentials.Count switch
{
// If a device authorization request is going to be sent and if at least one signing key
// was attached to the client registration, generate and include a client assertion
@ -5371,18 +5371,18 @@ public static partial class OpenIddictClientHandlers
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the client assertion token, if one is going to be sent.
/// used to generate the client assertion, if one is going to be sent.
/// </summary>
public sealed class PrepareChallengeClientAssertionTokenPrincipal : IOpenIddictClientHandler<ProcessChallengeContext>
public sealed class PrepareChallengeClientAssertionPrincipal : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireChallengeClientAssertionTokenGenerated>()
.UseSingletonHandler<PrepareChallengeClientAssertionTokenPrincipal>()
.SetOrder(EvaluateGeneratedChallengeClientAssertionToken.Descriptor.Order + 1_000)
.AddFilter<RequireChallengeClientAssertionGenerated>()
.UseSingletonHandler<PrepareChallengeClientAssertionPrincipal>()
.SetOrder(EvaluateGeneratedChallengeClientAssertion.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -5400,7 +5400,7 @@ public static partial class OpenIddictClientHandlers
var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType));
principal.SetCreationDate(DateTimeOffset.UtcNow);
var lifetime = context.Options.ClientAssertionTokenLifetime;
var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue)
{
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
@ -5417,7 +5417,7 @@ public static partial class OpenIddictClientHandlers
// Use a random GUID as the JWT unique identifier.
principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString());
context.ClientAssertionTokenPrincipal = principal;
context.ClientAssertionPrincipal = principal;
return default;
}
@ -5425,13 +5425,13 @@ public static partial class OpenIddictClientHandlers
/// <summary>
/// Contains the logic responsible for generating a client
/// assertion token for the current challenge operation.
/// assertion for the current challenge operation.
/// </summary>
public sealed class GenerateChallengeClientAssertionToken : IOpenIddictClientHandler<ProcessChallengeContext>
public sealed class GenerateChallengeClientAssertion : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public GenerateChallengeClientAssertionToken(IOpenIddictClientDispatcher dispatcher)
public GenerateChallengeClientAssertion(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
@ -5439,9 +5439,9 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireChallengeClientAssertionTokenGenerated>()
.UseScopedHandler<GenerateChallengeClientAssertionToken>()
.SetOrder(PrepareChallengeClientAssertionTokenPrincipal.Descriptor.Order + 1_000)
.AddFilter<RequireChallengeClientAssertionGenerated>()
.UseScopedHandler<GenerateChallengeClientAssertion>()
.SetOrder(PrepareChallengeClientAssertionPrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -5458,9 +5458,9 @@ public static partial class OpenIddictClientHandlers
CreateTokenEntry = false,
IsReferenceToken = false,
PersistTokenPayload = false,
Principal = context.ClientAssertionTokenPrincipal!,
Principal = context.ClientAssertionPrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.ClientAssertionToken
TokenType = TokenTypeHints.ClientAssertion
};
await _dispatcher.DispatchAsync(notification);
@ -5486,8 +5486,8 @@ public static partial class OpenIddictClientHandlers
return;
}
context.ClientAssertionToken = notification.Token;
context.ClientAssertionTokenType = notification.TokenFormat switch
context.ClientAssertion = notification.Token;
context.ClientAssertionType = notification.TokenFormat switch
{
TokenFormats.Jwt => ClientAssertionTypes.JwtBearer,
TokenFormats.Saml2 => ClientAssertionTypes.Saml2Bearer,
@ -5509,7 +5509,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireDeviceAuthorizationRequest>()
.UseSingletonHandler<AttachDeviceAuthorizationRequestClientCredentials>()
.SetOrder(GenerateChallengeClientAssertionToken.Descriptor.Order + 1_000)
.SetOrder(GenerateChallengeClientAssertion.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
@ -5528,10 +5528,10 @@ public static partial class OpenIddictClientHandlers
// Note: client authentication methods are mutually exclusive so the client_assertion
// and client_secret parameters MUST never be sent at the same time. For more information,
// see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.
if (context.IncludeClientAssertionToken)
if (context.IncludeClientAssertion)
{
context.DeviceAuthorizationRequest.ClientAssertion = context.ClientAssertionToken;
context.DeviceAuthorizationRequest.ClientAssertionType = context.ClientAssertionTokenType;
context.DeviceAuthorizationRequest.ClientAssertion = context.ClientAssertion;
context.DeviceAuthorizationRequest.ClientAssertionType = context.ClientAssertionType;
}
// Note: the client_secret may be null at this point (e.g for a public

6
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -66,10 +66,10 @@ public sealed class OpenIddictClientOptions
public List<SigningCredentials> SigningCredentials { get; } = new();
/// <summary>
/// Gets or sets the period of time client assertion tokens remain valid after being issued. The default value is 5 minutes.
/// While not recommended, this property can be set to <see langword="null"/> to issue client assertion tokens that never expire.
/// Gets or sets the period of time client assertions remain valid after being issued. The default value is 5 minutes.
/// While not recommended, this property can be set to <see langword="null"/> to issue client assertions that never expire.
/// </summary>
public TimeSpan? ClientAssertionTokenLifetime { get; set; } = TimeSpan.FromMinutes(5);
public TimeSpan? ClientAssertionLifetime { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets or sets the period of time state tokens remain valid after being issued. The default value is 15 minutes.

70
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -15,6 +15,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException;
@ -136,12 +137,31 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
throw new ArgumentException(SR.GetResourceString(SR.ID0206), nameof(application));
}
// If no client type was specified, assume it's a public application if no secret was provided.
// If no client type was specified, assume it's a confidential application if a secret was
// provided or a JSON Web Key Set was attached and contains at least one RSA/ECDSA signing key.
var type = await Store.GetClientTypeAsync(application, cancellationToken);
if (string.IsNullOrEmpty(type))
{
await Store.SetClientTypeAsync(application, string.IsNullOrEmpty(secret) ?
ClientTypes.Public : ClientTypes.Confidential, cancellationToken);
if (!string.IsNullOrEmpty(secret))
{
await Store.SetClientTypeAsync(application, ClientTypes.Confidential, cancellationToken);
}
else
{
var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
if (set is not null && set.Keys.Any(static key =>
key.Kty is JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Use is JsonWebKeyUseNames.Sig or null))
{
await Store.SetClientTypeAsync(application, ClientTypes.Confidential, cancellationToken);
}
else
{
await Store.SetClientTypeAsync(application, ClientTypes.Public, cancellationToken);
}
}
}
// If a client secret was provided, obfuscate it.
@ -609,6 +629,25 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
return Store.GetIdAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the JSON Web Key Set associated with an application.
/// </summary>
/// <param name="application">The application.</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 JSON Web Key Set associated with the application.
/// </returns>
public virtual ValueTask<JsonWebKeySet?> GetJsonWebKeySetAsync(TApplication application, CancellationToken cancellationToken = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetJsonWebKeySetAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the localized display name associated with an application
/// and corresponding to the current UI culture or one of its parents.
@ -984,6 +1023,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
await Store.SetConsentTypeAsync(application, descriptor.ConsentType, cancellationToken);
await Store.SetDisplayNameAsync(application, descriptor.DisplayName, cancellationToken);
await Store.SetDisplayNamesAsync(application, descriptor.DisplayNames.ToImmutableDictionary(), cancellationToken);
await Store.SetJsonWebKeySetAsync(application, descriptor.JsonWebKeySet, cancellationToken);
await Store.SetPermissionsAsync(application, descriptor.Permissions.ToImmutableArray(), cancellationToken);
await Store.SetPostLogoutRedirectUrisAsync(application, ImmutableArray.CreateRange(
descriptor.PostLogoutRedirectUris.Select(uri => uri.OriginalString)), cancellationToken);
@ -1023,6 +1063,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
descriptor.ClientType = await Store.GetClientTypeAsync(application, cancellationToken);
descriptor.ConsentType = await Store.GetConsentTypeAsync(application, cancellationToken);
descriptor.DisplayName = await Store.GetDisplayNameAsync(application, cancellationToken);
descriptor.JsonWebKeySet = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
descriptor.Permissions.Clear();
descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken));
descriptor.Requirements.Clear();
@ -1260,17 +1301,24 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
yield return new ValidationResult(SR.GetResourceString(SR.ID2112));
}
// Ensure a client secret was specified if the client is a confidential application.
// Ensure no client secret was specified if the client is a public application.
var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2113));
yield return new ValidationResult(SR.GetResourceString(SR.ID2114));
}
// Ensure no client secret was specified if the client is a public application.
else if (!string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
// Ensure a client secret or a JSON Web Key suitable for signing
// was specified if the client is a confidential application.
if (string.IsNullOrEmpty(secret) && string.Equals(type, ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2114));
var set = await Store.GetJsonWebKeySetAsync(application, cancellationToken);
if (set?.Keys is null || !set.Keys.Any(static key =>
key.Kty is JsonWebAlgorithmsKeyTypes.EllipticCurve or JsonWebAlgorithmsKeyTypes.RSA &&
key.Use is JsonWebKeyUseNames.Sig or null))
{
yield return new ValidationResult(SR.GetResourceString(SR.ID2113));
}
}
}
@ -1762,6 +1810,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
ValueTask<string?> IOpenIddictApplicationManager.GetIdAsync(object application, CancellationToken cancellationToken)
=> GetIdAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<JsonWebKeySet?> IOpenIddictApplicationManager.GetJsonWebKeySetAsync(object application, CancellationToken cancellationToken)
=> GetJsonWebKeySetAsync((TApplication) application, cancellationToken);
/// <inheritdoc/>
ValueTask<string?> IOpenIddictApplicationManager.GetLocalizedDisplayNameAsync(object application, CancellationToken cancellationToken)
=> GetLocalizedDisplayNameAsync((TApplication) application, cancellationToken);

7
src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs

@ -85,6 +85,13 @@ public class OpenIddictEntityFrameworkApplication<TKey, TAuthorization, TToken>
/// </summary>
public virtual TKey? Id { get; set; }
/// <summary>
/// Gets or sets the JSON Web Key Set associated with
/// the application, serialized as a JSON object.
/// </summary>
[StringSyntax(StringSyntaxAttribute.Json)]
public virtual string? JsonWebKeySet { get; set; }
/// <summary>
/// Gets or sets the permissions associated with the
/// current application, serialized as a JSON array.

41
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs

@ -16,6 +16,7 @@ using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.EntityFramework.Models;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -435,6 +436,33 @@ public class OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorizat
return new(ConvertIdentifierToString(application.Id));
}
/// <inheritdoc/>
public virtual ValueTask<JsonWebKeySet?> GetJsonWebKeySetAsync(TApplication application, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.JsonWebKeySet))
{
return new(result: null);
}
// Note: parsing the stringified JSON Web Key Set is an expensive operation.
// To mitigate that, the resulting object is stored in the memory cache.
var key = string.Concat("1e0a697d-0623-481a-927a-5e6c31458782", "\x1e", application.JsonWebKeySet);
var set = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromMinutes(1));
return JsonWebKeySet.Create(application.JsonWebKeySet);
})!;
return new(set);
}
/// <inheritdoc/>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(TApplication application, CancellationToken cancellationToken)
{
@ -840,6 +868,19 @@ public class OpenIddictEntityFrameworkApplicationStore<TApplication, TAuthorizat
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetJsonWebKeySetAsync(TApplication application, JsonWebKeySet? set, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
application.JsonWebKeySet = set is not null ? JsonSerializer.Serialize(set) : null;
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetPermissionsAsync(TApplication application, ImmutableArray<string> permissions, CancellationToken cancellationToken)
{

7
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs

@ -93,6 +93,13 @@ public class OpenIddictEntityFrameworkCoreApplication<TKey, TAuthorization, TTok
/// </summary>
public virtual TKey? Id { get; set; }
/// <summary>
/// Gets or sets the JSON Web Key Set associated with
/// the application, serialized as a JSON object.
/// </summary>
[StringSyntax(StringSyntaxAttribute.Json)]
public virtual string? JsonWebKeySet { get; set; }
/// <summary>
/// Gets or sets the permissions associated with the
/// current application, serialized as a JSON array.

41
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs

@ -15,6 +15,7 @@ using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -477,6 +478,33 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthor
return new(ConvertIdentifierToString(application.Id));
}
/// <inheritdoc/>
public virtual ValueTask<JsonWebKeySet?> GetJsonWebKeySetAsync(TApplication application, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.JsonWebKeySet))
{
return new(result: null);
}
// Note: parsing the stringified JSON Web Key Set is an expensive operation.
// To mitigate that, the resulting object is stored in the memory cache.
var key = string.Concat("1e0a697d-0623-481a-927a-5e6c31458782", "\x1e", application.JsonWebKeySet);
var set = Cache.GetOrCreate(key, entry =>
{
entry.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromMinutes(1));
return JsonWebKeySet.Create(application.JsonWebKeySet);
})!;
return new(set);
}
/// <inheritdoc/>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(TApplication application, CancellationToken cancellationToken)
{
@ -881,6 +909,19 @@ public class OpenIddictEntityFrameworkCoreApplicationStore<TApplication, TAuthor
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetJsonWebKeySetAsync(TApplication application, JsonWebKeySet? set, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
application.JsonWebKeySet = set is not null ? JsonSerializer.Serialize(set) : null;
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetPermissionsAsync(TApplication application, ImmutableArray<string> permissions, CancellationToken cancellationToken)
{

6
src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs

@ -72,6 +72,12 @@ public class OpenIddictMongoDbApplication
[BsonId, BsonRequired]
public virtual ObjectId Id { get; set; }
/// <summary>
/// Gets or sets the JSON Web Key Set associated with the application.
/// </summary>
[BsonElement("json_web_key_set"), BsonIgnoreIfNull]
public virtual BsonDocument? JsonWebKeySet { get; set; }
/// <summary>
/// Gets or sets the permissions associated with the current application.
/// </summary>

34
src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs

@ -12,6 +12,7 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.MongoDb.Models;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -295,6 +296,22 @@ public class OpenIddictMongoDbApplicationStore<TApplication> : IOpenIddictApplic
return new(application.Id.ToString());
}
/// <inheritdoc/>
public virtual ValueTask<JsonWebKeySet?> GetJsonWebKeySetAsync(TApplication application, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (application.JsonWebKeySet is null)
{
return new(result: null);
}
return new(JsonWebKeySet.Create(application.JsonWebKeySet.ToJson()));
}
/// <inheritdoc/>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(
TApplication application, CancellationToken cancellationToken)
@ -574,7 +591,22 @@ public class OpenIddictMongoDbApplicationStore<TApplication> : IOpenIddictApplic
}
/// <inheritdoc/>
public virtual ValueTask SetPermissionsAsync(TApplication application, ImmutableArray<string> permissions, CancellationToken cancellationToken)
public virtual ValueTask SetJsonWebKeySetAsync(TApplication application,
JsonWebKeySet? set, CancellationToken cancellationToken)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
application.JsonWebKeySet = set is not null ? BsonDocument.Parse(JsonSerializer.Serialize(set)) : null;
return default;
}
/// <inheritdoc/>
public virtual ValueTask SetPermissionsAsync(TApplication application,
ImmutableArray<string> permissions, CancellationToken cancellationToken)
{
if (application is null)
{

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs

@ -29,6 +29,7 @@ public static class OpenIddictServerAspNetCoreConstants
public static class Properties
{
public const string AccessTokenPrincipal = ".access_token_principal";
public const string ClientAssertionPrincipal = ".client_assertion_principal";
public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string DeviceCodePrincipal = ".device_code_principal";
public const string Error = ".error";
@ -44,6 +45,7 @@ public static class OpenIddictServerAspNetCoreConstants
{
public const string AccessToken = "access_token";
public const string AuthorizationCode = "authorization_code";
public const string ClientAssertion = "client_assertion";
public const string DeviceCode = "device_code";
public const string IdentityToken = "id_token";
public const string RefreshToken = "refresh_token";

15
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -221,6 +221,16 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
});
}
if (!string.IsNullOrEmpty(context.ClientAssertion))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.ClientAssertion,
Value = context.ClientAssertion
});
}
if (!string.IsNullOrEmpty(context.DeviceCode))
{
tokens ??= new(capacity: 1);
@ -271,6 +281,11 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
}
if (context.ClientAssertionPrincipal is not null)
{
properties.SetParameter(Properties.ClientAssertionPrincipal, context.ClientAssertionPrincipal);
}
if (context.DeviceCodePrincipal is not null)
{
properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal);

6
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs

@ -62,6 +62,12 @@ public static partial class OpenIddictServerDataProtectionHandlers
return default;
}
// If a specific token format is expected, return immediately if it doesn't match the expected value.
if (context.TokenFormat is not null && context.TokenFormat is not TokenFormats.Private.DataProtection)
{
return default;
}
// Note: ASP.NET Core Data Protection tokens created by the default implementation always start
// with "CfDJ8", that corresponds to the base64 representation of the "09 F0 C9 F0" value used
// by KeyRingBasedDataProtectionProvider as a Data Protection version identifier/magic header.

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs

@ -63,6 +63,7 @@ public static class OpenIddictServerOwinConstants
{
public const string AccessToken = "access_token";
public const string AuthorizationCode = "authorization_code";
public const string ClientAssertion = "client_assertion";
public const string DeviceCode = "device_code";
public const string IdentityToken = "id_token";
public const string RefreshToken = "refresh_token";

5
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -215,6 +215,11 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
properties.Dictionary[Tokens.AuthorizationCode] = context.AuthorizationCode;
}
if (!string.IsNullOrEmpty(context.ClientAssertion))
{
properties.Dictionary[Tokens.ClientAssertion] = context.ClientAssertion;
}
if (!string.IsNullOrEmpty(context.DeviceCode))
{
properties.Dictionary[Tokens.DeviceCode] = context.DeviceCode;

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

@ -136,10 +136,15 @@ public static partial class OpenIddictServerEvents
/// </summary>
public string Token { get; set; } = default!;
/// <summary>
/// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to validate, if applicable.
/// </summary>
public string? TokenFormat { get; set; }
/// <summary>
/// Gets or sets the token type hint specified by the client, if applicable.
/// </summary>
public string? TokenTypeHint { get; set; } = default!;
public string? TokenTypeHint { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the validated token is a reference token.

51
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -331,6 +331,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ExtractAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a client assertion
/// token should be extracted from the current context.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ExtractClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a device
/// code should be extracted from the current context.
@ -394,6 +403,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RequireAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a client assertion
/// must be resolved for the authentication to be considered valid.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RequireClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a device code
/// must be resolved for the authentication to be considered valid.
@ -457,6 +475,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ValidateAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the client assertion
/// token extracted from the current request should be validated.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ValidateClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the device
/// code extracted from the current request should be validated.
@ -520,6 +547,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RejectAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid client assertion
/// will cause the authentication demand to be rejected or will be ignored.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid device code
/// will cause the authentication demand to be rejected or will be ignored.
@ -585,6 +621,21 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the client assertion to validate, if applicable.
/// </summary>
public string? ClientAssertion { get; set; }
/// <summary>
/// Gets or sets the type of the client assertion to validate, if applicable.
/// </summary>
public string? ClientAssertionType { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the client assertion, if applicable.
/// </summary>
public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
/// <summary>
/// Gets or sets the device code to validate, if applicable.
/// </summary>

2
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -47,6 +47,8 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
builder.Services.TryAddSingleton<RequireAuthorizationStorageEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireClientAssertionPrincipal>();
builder.Services.TryAddSingleton<RequireClientAssertionValidated>();
builder.Services.TryAddSingleton<RequireClientIdParameter>();
builder.Services.TryAddSingleton<RequireClientSecretParameter>();
builder.Services.TryAddSingleton<RequireConfigurationRequest>();

34
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -130,6 +130,40 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no client assertion principal is available.
/// </summary>
public sealed class RequireClientAssertionPrincipal : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ClientAssertionPrincipal is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no client assertion is validated.
/// </summary>
public sealed class RequireClientAssertionValidated : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ValidateClientAssertion);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers when no client identifier is received.
/// </summary>

82
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -34,6 +34,7 @@ public static partial class OpenIddictServerHandlers
* Device request validation:
*/
ValidateScopeParameter.Descriptor,
ValidateClientCredentialsParameters.Descriptor,
ValidateScopes.Descriptor,
ValidateDeviceAuthentication.Descriptor,
ValidateEndpointPermissions.Descriptor,
@ -367,6 +368,85 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting device requests that specify invalid client credentials parameters.
/// </summary>
public sealed class ValidateClientCredentialsParameters : IOpenIddictServerHandler<ValidateDeviceRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateDeviceRequestContext>()
.UseSingletonHandler<ValidateClientCredentialsParameters>()
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateDeviceRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Ensure a client_assertion_type is specified when a client_assertion was attached.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure a client_assertion is specified when a client_assertion_type was attached.
if (string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Reject requests that use multiple client authentication methods.
//
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientSecret))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6140));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2087),
uri: SR.FormatID8000(SR.ID2087));
return default;
}
// Ensure the specified client_assertion_type is supported.
if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) &&
!string.Equals(context.Request.ClientAssertionType, ClientAssertionTypes.JwtBearer, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting authorization requests that use unregistered scopes.
/// Note: this handler partially works with the degraded mode but is not used when scope validation is disabled.
@ -395,7 +475,7 @@ public static partial class OpenIddictServerHandlers
new ValidateScopes(provider.GetService<IOpenIddictScopeManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

4
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -512,24 +512,28 @@ public static partial class OpenIddictServerHandlers
{
context.DeviceEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
context.DeviceEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.DeviceEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
}
if (context.IntrospectionEndpoint is not null)
{
context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
}
if (context.RevocationEndpoint is not null)
{
context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
}
if (context.TokenEndpoint is not null)
{
context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
}
return default;

92
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -422,17 +422,36 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
// Reject grant_type=authorization_code requests that don't specify a client_id, as the client
// identifier MUST be sent by the client application in the request body if it cannot be
// inferred from the client authentication method (e.g the username when using basic).
// Reject grant_type=authorization_code requests that don't specify a client_id or a client_assertion,
// as the client identifier MUST be sent by the client application in the request body if it cannot
// be inferred from the client authentication method (e.g the username when using basic).
//
// See https://tools.ietf.org/html/rfc6749#section-4.1.3 for more information.
if (string.IsNullOrEmpty(context.ClientId) && context.Request.IsAuthorizationCodeGrantType())
if (context.Request.IsAuthorizationCodeGrantType() &&
string.IsNullOrEmpty(context.Request.ClientId) &&
string.IsNullOrEmpty(context.Request.ClientAssertion))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.ClientId);
context.Reject(
error: Errors.InvalidClient,
error: Errors.InvalidRequest,
description: SR.FormatID2029(Parameters.ClientId),
uri: SR.FormatID8000(SR.ID2029));
return default;
}
// Reject grant_type=client_credentials requests that don't specify a client_id or a client_assertion.
//
// See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.
if (context.Request.IsClientCredentialsGrantType() &&
string.IsNullOrEmpty(context.Request.ClientId) &&
string.IsNullOrEmpty(context.Request.ClientAssertion))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.ClientId);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2029(Parameters.ClientId),
uri: SR.FormatID8000(SR.ID2029));
@ -486,8 +505,7 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting token requests that don't
/// specify client credentials for the client credentials grant type.
/// Contains the logic responsible for rejecting token requests that specify invalid client credentials parameters.
/// </summary>
public sealed class ValidateClientCredentialsParameters : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
@ -509,14 +527,68 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
// Ensure a client_assertion_type is specified when a client_assertion was attached.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure a client_assertion is specified when a client_assertion_type was attached.
if (string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure the specified client_assertion_type is supported.
if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) &&
!string.Equals(context.Request.ClientAssertionType, ClientAssertionTypes.JwtBearer, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
// Reject requests that use multiple client authentication methods.
//
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientSecret))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6140));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2087),
uri: SR.FormatID8000(SR.ID2087));
return default;
}
// Reject grant_type=client_credentials requests missing the client credentials.
//
// See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.
if (context.Request.IsClientCredentialsGrantType() && (string.IsNullOrEmpty(context.Request.ClientId) ||
string.IsNullOrEmpty(context.Request.ClientSecret)))
if (context.Request.IsClientCredentialsGrantType() &&
string.IsNullOrEmpty(context.Request.ClientAssertion) &&
string.IsNullOrEmpty(context.Request.ClientSecret))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2057(Parameters.ClientId, Parameters.ClientSecret),
description: SR.FormatID2057(Parameters.ClientSecret, Parameters.ClientAssertion),
uri: SR.FormatID8000(SR.ID2057));
return default;

82
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -35,6 +35,7 @@ public static partial class OpenIddictServerHandlers
* Introspection request validation:
*/
ValidateTokenParameter.Descriptor,
ValidateClientCredentialsParameters.Descriptor,
ValidateAuthentication.Descriptor,
ValidateEndpointPermissions.Descriptor,
ValidateTokenType.Descriptor,
@ -366,6 +367,85 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting introspection requests that specify invalid client credentials parameters.
/// </summary>
public sealed class ValidateClientCredentialsParameters : IOpenIddictServerHandler<ValidateIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateIntrospectionRequestContext>()
.UseSingletonHandler<ValidateClientCredentialsParameters>()
.SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateIntrospectionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Ensure a client_assertion_type is specified when a client_assertion was attached.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure a client_assertion is specified when a client_assertion_type was attached.
if (string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure the specified client_assertion_type is supported.
if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) &&
!string.Equals(context.Request.ClientAssertionType, ClientAssertionTypes.JwtBearer, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
// Reject requests that use multiple client authentication methods.
//
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientSecret))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6140));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2087),
uri: SR.FormatID8000(SR.ID2087));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for applying the authentication logic to introspection requests.
/// </summary>
@ -382,7 +462,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateIntrospectionRequestContext>()
.UseScopedHandler<ValidateAuthentication>()
.SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000)
.SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

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

@ -9,7 +9,9 @@ using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
@ -36,8 +38,8 @@ public static partial class OpenIddictServerHandlers
ValidateAuthorizationEntry.Descriptor,
/*
* Token generation:
*/
* Token generation:
*/
AttachSecurityCredentials.Descriptor,
CreateTokenEntry.Descriptor,
GenerateIdentityModelToken.Descriptor,
@ -49,12 +51,27 @@ public static partial class OpenIddictServerHandlers
/// </summary>
public sealed class ResolveTokenValidationParameters : IOpenIddictServerHandler<ValidateTokenContext>
{
private readonly IOpenIddictApplicationManager? _applicationManager;
public ResolveTokenValidationParameters(IOpenIddictApplicationManager? applicationManager = null)
=> _applicationManager = applicationManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ResolveTokenValidationParameters>()
.UseScopedHandler<ResolveTokenValidationParameters>(static provider =>
{
// Note: the application manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
return options.EnableDegradedMode ?
new ResolveTokenValidationParameters() :
new ResolveTokenValidationParameters(provider.GetService<IOpenIddictApplicationManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -67,74 +84,143 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
var parameters = context.Options.TokenValidationParameters.Clone();
// The OpenIddict server is expected to validate tokens it creates (e.g access tokens)
// and tokens that are created by one or multiple clients (e.g client assertions).
//
// To simplify the token validation parameters selection logic, an exception is thrown
// if multiple token types are considered valid and contain tokens issued by the
// authorization server and tokens issued by the client (e.g client assertions).
if (context.ValidTokenTypes.Count > 1 &&
context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0308));
}
parameters.ValidIssuers ??= (context.Options.Issuer ?? context.BaseUri) switch
var parameters = context.ValidTokenTypes.Count switch
{
null => null,
// When only client assertions are considered valid, create dynamic token validation
// parameters using the encryption keys/signing keys attached to the specific client.
1 when context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion)
=> GetClientTokenValidationParameters(),
// Otherwise, use the token validation parameters of the authorization server.
_ => GetServerTokenValidationParameters()
};
// If the issuer URI doesn't contain any query/fragment, allow both http://www.fabrikam.com
// and http://www.fabrikam.com/ (the recommended URI representation) to be considered valid.
// See https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.3 for more information.
{ AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 } uri => new[]
TokenValidationParameters GetClientTokenValidationParameters()
{
// Note: the audience/issuer/lifetime are manually validated by OpenIddict itself.
var parameters = new TokenValidationParameters
{
uri.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash.
uri.AbsoluteUri[..^1]
},
// When properly normalized, Uri.AbsolutePath should never be empty and should at least
// contain a leading slash. While dangerous, System.Uri now offers a way to create a URI
// instance without applying the default canonicalization logic. To support such URIs,
// a special case is added here to add back the missing trailing slash when necessary.
{ AbsolutePath.Length: 0, Query.Length: 0, Fragment.Length: 0 } uri => new[]
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = false
};
// Only provide a signing key resolver if the degraded mode was not enabled.
//
// Applications that opt for the degraded mode and need client assertions support
// need to implement a custom event handler thats a issuer signing key resolver.
if (!context.Options.EnableDegradedMode)
{
uri.AbsoluteUri,
uri.AbsoluteUri + "/"
},
if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
Uri uri => new[] { uri.AbsoluteUri }
};
parameters.IssuerSigningKeyResolver = (_, token, _, _) => Task.Run(async () =>
{
// Resolve the client application corresponding to the token issuer and retrieve
// the signing keys from to the JSON Web Key set attached to the client application.
//
// Important: at this stage, the issuer isn't guaranteed to be valid or legitimate.
var application = await _applicationManager.FindByClientIdAsync(token.Issuer);
if (application is not null && await _applicationManager.GetJsonWebKeySetAsync(application)
is JsonWebKeySet set)
{
return set.GetSigningKeys();
}
parameters.ValidateIssuer = parameters.ValidIssuers is not null;
return Array.Empty<SecurityKey>();
}).GetAwaiter().GetResult();
}
parameters.ValidTypes = context.ValidTokenTypes.Count switch
return parameters;
}
TokenValidationParameters GetServerTokenValidationParameters()
{
// If no specific token type is expected, accept all token types at this stage.
// Additional filtering can be made based on the resolved/actual token type.
0 => null,
var parameters = context.Options.TokenValidationParameters.Clone();
// Otherwise, map the token types to their JWT public or internal representation.
_ => context.ValidTokenTypes.SelectMany(type => type switch
parameters.ValidIssuers ??= (context.Options.Issuer ?? context.BaseUri) switch
{
// For access tokens, both "at+jwt" and "application/at+jwt" are valid.
TokenTypeHints.AccessToken => new[]
null => null,
// If the issuer URI doesn't contain any query/fragment, allow both http://www.fabrikam.com
// and http://www.fabrikam.com/ (the recommended URI representation) to be considered valid.
// See https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.3 for more information.
{ AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 } uri => new[]
{
JsonWebTokenTypes.AccessToken,
JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken
uri.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash.
uri.AbsoluteUri[..^1]
},
// For identity tokens, both "JWT" and "application/jwt" are valid.
TokenTypeHints.IdToken => new[]
// When properly normalized, Uri.AbsolutePath should never be empty and should at least
// contain a leading slash. While dangerous, System.Uri now offers a way to create a URI
// instance without applying the default canonicalization logic. To support such URIs,
// a special case is added here to add back the missing trailing slash when necessary.
{ AbsolutePath.Length: 0, Query.Length: 0, Fragment.Length: 0 } uri => new[]
{
JsonWebTokenTypes.Jwt,
JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.Jwt
uri.AbsoluteUri,
uri.AbsoluteUri + "/"
},
// For authorization codes, only the short "oi_auc+jwt" form is valid.
TokenTypeHints.AuthorizationCode => new[] { JsonWebTokenTypes.Private.AuthorizationCode },
Uri uri => new[] { uri.AbsoluteUri }
};
// For device codes, only the short "oi_dvc+jwt" form is valid.
TokenTypeHints.DeviceCode => new[] { JsonWebTokenTypes.Private.DeviceCode },
parameters.ValidateIssuer = parameters.ValidIssuers is not null;
// For refresh tokens, only the short "oi_reft+jwt" form is valid.
TokenTypeHints.RefreshToken => new[] { JsonWebTokenTypes.Private.RefreshToken },
parameters.ValidTypes = context.ValidTokenTypes.Count switch
{
// If no specific token type is expected, accept all token types at this stage.
// Additional filtering can be made based on the resolved/actual token type.
0 => null,
// For user codes, only the short "oi_usrc+jwt" form is valid.
TokenTypeHints.UserCode => new[] { JsonWebTokenTypes.Private.UserCode },
// Otherwise, map the token types to their JWT public or internal representation.
_ => context.ValidTokenTypes.SelectMany(type => type switch
{
// For access tokens, both "at+jwt" and "application/at+jwt" are valid.
TokenTypeHints.AccessToken => new[]
{
JsonWebTokenTypes.AccessToken,
JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken
},
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
})
};
// For identity tokens, both "JWT" and "application/jwt" are valid.
TokenTypeHints.IdToken => new[]
{
JsonWebTokenTypes.Jwt,
JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.Jwt
},
// For authorization codes, only the short "oi_auc+jwt" form is valid.
TokenTypeHints.AuthorizationCode => new[] { JsonWebTokenTypes.Private.AuthorizationCode },
// For device codes, only the short "oi_dvc+jwt" form is valid.
TokenTypeHints.DeviceCode => new[] { JsonWebTokenTypes.Private.DeviceCode },
// For refresh tokens, only the short "oi_reft+jwt" form is valid.
TokenTypeHints.RefreshToken => new[] { JsonWebTokenTypes.Private.RefreshToken },
// For user codes, only the short "oi_usrc+jwt" form is valid.
TokenTypeHints.UserCode => new[] { JsonWebTokenTypes.Private.UserCode },
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
})
};
return parameters;
}
context.SecurityTokenHandler = context.Options.JsonWebTokenHandler;
context.TokenValidationParameters = parameters;
@ -175,6 +261,13 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
// Note: reference tokens are never used for client assertions.
if (context.ValidTokenTypes.Count is 1 &&
context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion))
{
return;
}
var token = context.Token.Length switch
{
// 12 may correspond to a normalized user code and 43 to any
@ -295,6 +388,12 @@ public static partial class OpenIddictServerHandlers
return;
}
// If a specific token format is expected, return immediately if it doesn't match the expected value.
if (context.TokenFormat is not null && context.TokenFormat is not TokenFormats.Jwt)
{
return;
}
// If the token cannot be read, don't return an error to allow another handler to validate it.
if (!context.SecurityTokenHandler.CanReadToken(context.Token))
{
@ -389,6 +488,13 @@ public static partial class OpenIddictServerHandlers
// the token type (resolved from "typ" or "token_usage") as a special private claim.
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity).SetTokenType(result.TokenType switch
{
// Client assertions are typically created by client libraries with either a missing "typ" header
// or a generic value like "JWT". Since the type defined by the client cannot be used as-is,
// validation is bypassed and tokens used as client assertions are assumed to be client assertions.
_ when context.ValidTokenTypes.Count is 1 &&
context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion)
=> TokenTypeHints.ClientAssertion,
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
// Both at+jwt and application/at+jwt are supported for access tokens.
@ -606,6 +712,13 @@ public static partial class OpenIddictServerHandlers
return;
}
// Note: token entries are never used for client assertions.
if (context.ValidTokenTypes.Count is 1 &&
context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion))
{
return;
}
// Extract the token identifier from the authentication principal.
//
// If no token identifier can be found, this indicates that the token

82
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -29,6 +29,7 @@ public static partial class OpenIddictServerHandlers
* Revocation request validation:
*/
ValidateTokenParameter.Descriptor,
ValidateClientCredentialsParameters.Descriptor,
ValidateAuthentication.Descriptor,
ValidateEndpointPermissions.Descriptor,
ValidateTokenType.Descriptor,
@ -313,6 +314,85 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting revocation requests that specify invalid client credentials parameters.
/// </summary>
public sealed class ValidateClientCredentialsParameters : IOpenIddictServerHandler<ValidateRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateRevocationRequestContext>()
.UseSingletonHandler<ValidateClientCredentialsParameters>()
.SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateRevocationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Ensure a client_assertion_type is specified when a client_assertion was attached.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure a client_assertion is specified when a client_assertion_type was attached.
if (string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientAssertionType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Ensure the specified client_assertion_type is supported.
if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) &&
!string.Equals(context.Request.ClientAssertionType, ClientAssertionTypes.JwtBearer, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
// Reject requests that use multiple client authentication methods.
//
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
if (!string.IsNullOrEmpty(context.Request.ClientAssertion) &&
!string.IsNullOrEmpty(context.Request.ClientSecret))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6140));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2087),
uri: SR.FormatID8000(SR.ID2087));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for applying the authentication logic to revocation requests.
/// </summary>
@ -329,7 +409,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateRevocationRequestContext>()
.UseScopedHandler<ValidateAuthentication>()
.SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000)
.SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

510
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -37,6 +37,10 @@ public static partial class OpenIddictServerHandlers
ValidateClientId.Descriptor,
ValidateClientType.Descriptor,
ValidateClientSecret.Descriptor,
ValidateClientAssertion.Descriptor,
ValidateClientAssertionWellknownClaims.Descriptor,
ValidateClientAssertionIssuer.Descriptor,
ValidateClientAssertionAudience.Descriptor,
ValidateAccessToken.Descriptor,
ValidateAuthorizationCode.Descriptor,
ValidateDeviceCode.Descriptor,
@ -286,6 +290,21 @@ public static partial class OpenIddictServerHandlers
_ => (false, false, false, false)
};
(context.ExtractClientAssertion,
context.RequireClientAssertion,
context.ValidateClientAssertion,
context.RejectClientAssertion) = context.EndpointType switch
{
// Client assertions can be used with all the endpoints that support client authentication.
// By default, client assertions are not required, but they are extracted and validated if
// present and invalid client assertions are always automatically rejected by OpenIddict.
OpenIddictServerEndpointType.Device or OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token
=> (true, false, true, true),
_ => (false, false, false, false)
};
(context.ExtractDeviceCode,
context.RequireDeviceCode,
context.ValidateDeviceCode,
@ -303,8 +322,8 @@ public static partial class OpenIddictServerHandlers
context.ValidateGenericToken,
context.RejectGenericToken) = context.EndpointType switch
{
// Tokens received by the introspection and revocation endpoints can be of any type.
// Additional token type filtering is made by the endpoint themselves, if needed.
// Tokens received by the introspection and revocation endpoints can be of any supported type.
// Additional token type filtering is typically performed by the endpoint themselves when needed.
OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation
=> (true, true, true, true),
@ -394,6 +413,16 @@ public static partial class OpenIddictServerHandlers
_ => null
};
(context.ClientAssertion, context.ClientAssertionType) = context.EndpointType switch
{
OpenIddictServerEndpointType.Device or OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token
when context.ExtractClientAssertion
=> (context.Request.ClientAssertion, context.Request.ClientAssertionType),
_ => (null, null)
};
context.DeviceCode = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.ExtractDeviceCode
@ -467,6 +496,7 @@ public static partial class OpenIddictServerHandlers
if ((context.RequireAccessToken && string.IsNullOrEmpty(context.AccessToken)) ||
(context.RequireAuthorizationCode && string.IsNullOrEmpty(context.AuthorizationCode)) ||
(context.RequireClientAssertion && string.IsNullOrEmpty(context.ClientAssertion)) ||
(context.RequireDeviceCode && string.IsNullOrEmpty(context.DeviceCode)) ||
(context.RequireGenericToken && string.IsNullOrEmpty(context.GenericToken)) ||
(context.RequireIdentityToken && string.IsNullOrEmpty(context.IdentityToken)) ||
@ -485,6 +515,450 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the client assertion resolved from the context.
/// </summary>
public sealed class ValidateClientAssertion : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
public ValidateClientAssertion(IOpenIddictServerDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionValidated>()
.UseScopedHandler<ValidateClientAssertion>()
.SetOrder(ValidateRequiredTokens.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.ClientAssertionPrincipal is not null || string.IsNullOrEmpty(context.ClientAssertion))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Token = context.ClientAssertion,
TokenFormat = context.ClientAssertionType switch
{
ClientAssertionTypes.JwtBearer => TokenFormats.Jwt,
_ => null
},
ValidTokenTypes = { TokenTypeHints.ClientAssertion }
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
if (context.RejectClientAssertion)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
return;
}
context.ClientAssertionPrincipal = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible for validating the well-known claims contained in the client assertion principal.
/// </summary>
public sealed class ValidateClientAssertionWellknownClaims : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionPrincipal>()
.UseSingletonHandler<ValidateClientAssertionWellknownClaims>()
.SetOrder(ValidateClientAssertion.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
foreach (var group in context.ClientAssertionPrincipal.Claims
.GroupBy(claim => claim.Type)
.ToDictionary(group => group.Key, group => group.ToList()))
{
if (ValidateClaimGroup(group))
{
continue;
}
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2171(group.Key),
uri: SR.FormatID8000(SR.ID2171));
return default;
}
// Client assertions MUST contain an "iss" claim. For more information,
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Issuer))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2172(Claims.Issuer),
uri: SR.FormatID8000(SR.ID2172));
return default;
}
// Client assertions MUST contain a "sub" claim. For more information,
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Subject))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2172(Claims.Subject),
uri: SR.FormatID8000(SR.ID2172));
return default;
}
// Client assertions MUST contain at least one "aud" claim. For more information,
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Audience))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2172(Claims.Audience),
uri: SR.FormatID8000(SR.ID2172));
return default;
}
// Client assertions MUST contain contain a "exp" claim. For more information,
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
if (!context.ClientAssertionPrincipal.HasClaim(Claims.ExpiresAt))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2172(Claims.ExpiresAt),
uri: SR.FormatID8000(SR.ID2172));
return default;
}
// Client assertions MUST contain contain an "iat" claim. For more information,
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
if (!context.ClientAssertionPrincipal.HasClaim(Claims.IssuedAt))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2172(Claims.IssuedAt),
uri: SR.FormatID8000(SR.ID2172));
return default;
}
return default;
static bool ValidateClaimGroup(KeyValuePair<string, List<Claim>> claims) => claims switch
{
// The following JWT claims MUST be represented as unique strings.
{
Key: Claims.AuthorizedParty or Claims.Issuer or Claims.JwtId or Claims.Subject,
Value: List<Claim> values
} => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String,
// The following JWT claims MUST be represented as unique strings or array of strings.
{
Key: Claims.Audience,
Value: List<Claim> values
} => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
// The following JWT claims MUST be represented as unique numeric dates.
{
Key: Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore,
Value: List<Claim> values
} => values.Count is 1 && values[0].ValueType is ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64,
// Claims that are not in the well-known list can be of any type.
_ => true
};
}
}
/// <summary>
/// Contains the logic responsible for validating the issuer contained in the client assertion principal.
/// </summary>
public sealed class ValidateClientAssertionIssuer : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionPrincipal>()
.UseSingletonHandler<ValidateClientAssertionIssuer>()
.SetOrder(ValidateClientAssertionWellknownClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Ensure the subject represented by the client assertion matches its issuer.
var (issuer, subject) = (
context.ClientAssertionPrincipal.GetClaim(Claims.Issuer),
context.ClientAssertionPrincipal.GetClaim(Claims.Subject));
if (!string.Equals(issuer, subject, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidGrant,
description: SR.FormatID2173(Claims.Subject),
uri: SR.FormatID8000(SR.ID2173));
return default;
}
// If a client identifier was also specified in the request, ensure the
// value matches the application represented by the client assertion.
if (!string.IsNullOrEmpty(context.ClientId))
{
if (!string.Equals(context.ClientId, issuer, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidGrant,
description: SR.FormatID2173(Claims.Issuer),
uri: SR.FormatID8000(SR.ID2173));
return default;
}
if (!string.Equals(context.ClientId, subject, StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidGrant,
description: SR.FormatID2173(Claims.Subject),
uri: SR.FormatID8000(SR.ID2173));
return default;
}
}
// Otherwise, use the issuer resolved from the client assertion principal as the client identifier.
else if (context.Request is OpenIddictRequest request)
{
request.ClientId = context.ClientAssertionPrincipal.GetClaim(Claims.Issuer);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the audience contained in the client assertion principal.
/// </summary>
public sealed class ValidateClientAssertionAudience : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionPrincipal>()
.UseSingletonHandler<ValidateClientAssertionAudience>()
.SetOrder(ValidateClientAssertionIssuer.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Ensure at least one non-empty audience was specified (note: in
// the most common case, a single audience is generally specified).
var audiences = context.ClientAssertionPrincipal.GetClaims(Claims.Audience);
if (!audiences.Any(static audience => !string.IsNullOrEmpty(audience)))
{
context.Reject(
error: Errors.InvalidGrant,
description: SR.FormatID2172(Claims.Audience),
uri: SR.FormatID8000(SR.ID2172));
return default;
}
// Ensure at least one of the audiences points to the current authorization server.
if (!ValidateAudiences(audiences))
{
context.Reject(
error: Errors.InvalidGrant,
description: SR.FormatID2173(Claims.Audience),
uri: SR.FormatID8000(SR.ID2173));
return default;
}
return default;
bool ValidateAudiences(ImmutableArray<string> audiences)
{
foreach (var audience in audiences)
{
// Ignore the iterated audience if it's not a valid absolute URI.
if (!Uri.TryCreate(audience, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
continue;
}
// Consider the audience valid if it matches the issuer value assigned to the current instance.
//
// See https://datatracker.ietf.org/doc/html/rfc7523#section-3 for more information.
if (context.Options.Issuer is not null && UriEquals(uri, context.Options.Issuer))
{
return true;
}
// At this point, ignore the rest of the validation logic if the current base URI is not known.
if (context.BaseUri is null)
{
continue;
}
// Consider the audience valid if it matches the current base URI, unless an explicit issuer was set.
if (context.Options.Issuer is null && UriEquals(uri, context.BaseUri))
{
return true;
}
// Consider the audience valid if it matches one of the URIs assigned to the token
// endpoint, independently of whether the request is a token request or not.
if (MatchesAnyUri(uri, context.Options.TokenEndpointUris))
{
return true;
}
// If the current request is a device request, consider the audience valid
// if the address matches one of the URIs assigned to the device endpoint.
if (context.EndpointType is OpenIddictServerEndpointType.Device &&
MatchesAnyUri(uri, context.Options.DeviceEndpointUris))
{
return true;
}
// If the current request is an introspection request, consider the audience valid
// if the address matches one of the URIs assigned to the introspection endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.Introspection &&
MatchesAnyUri(uri, context.Options.IntrospectionEndpointUris))
{
return true;
}
// If the current request is a revocation request, consider the audience valid
// if the address matches one of the URIs assigned to the revocation endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.Revocation &&
MatchesAnyUri(uri, context.Options.RevocationEndpointUris))
{
return true;
}
}
return false;
}
bool MatchesAnyUri(Uri uri, List<Uri> uris)
{
for (var index = 0; index < uris.Count; index++)
{
if (UriEquals(uri, OpenIddictHelpers.CreateAbsoluteUri(context.BaseUri, uris[index])))
{
return true;
}
}
return false;
}
static bool UriEquals(Uri left, Uri right)
{
if (string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.Ordinal))
{
return true;
}
// Consider the two URIs identical if they only differ by the trailing slash.
if (left.AbsolutePath.Length == right.AbsolutePath.Length + 1 &&
left.AbsolutePath.StartsWith(right.AbsolutePath, StringComparison.Ordinal) &&
left.AbsolutePath[^1] is '/')
{
return true;
}
return right.AbsolutePath.Length == left.AbsolutePath.Length + 1 &&
right.AbsolutePath.StartsWith(left.AbsolutePath, StringComparison.Ordinal) &&
right.AbsolutePath[^1] is '/';
}
}
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands that use an invalid client_id.
/// </summary>
@ -511,7 +985,7 @@ public static partial class OpenIddictServerHandlers
new ValidateClientId(provider.GetService<IOpenIddictApplicationManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(ValidateRequiredTokens.Descriptor.Order + 1_000)
.SetOrder(ValidateClientAssertionAudience.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -523,7 +997,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
// Don't validate the client identifier on endpoint that don't support client identification.
// Don't validate the client identifier on endpoints that don't support client identification.
if (context.EndpointType is OpenIddictServerEndpointType.Userinfo or OpenIddictServerEndpointType.Verification)
{
return;
@ -652,6 +1126,19 @@ public static partial class OpenIddictServerHandlers
return;
}
// Reject requests containing a client_assertion when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientAssertion))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6226), context.ClientId);
context.Reject(
error: Errors.InvalidClient,
description: SR.FormatID2053(Parameters.ClientAssertion),
uri: SR.FormatID8000(SR.ID2053));
return;
}
// Reject requests containing a client_secret when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientSecret))
{
@ -669,7 +1156,7 @@ public static partial class OpenIddictServerHandlers
}
// Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks.
if (string.IsNullOrEmpty(context.ClientSecret))
if (context.ClientAssertionPrincipal is null && string.IsNullOrEmpty(context.ClientSecret))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6224), context.ClientId);
@ -1012,7 +1499,18 @@ public static partial class OpenIddictServerHandlers
//
// Additional token type filtering is made by the endpoint themselves, if needed.
// As such, the valid token types list is deliberately left empty in this case.
ValidTokenTypes = { }
//
// Note: tokens not created by the server stack (e.g client assertions)
// are deliberately excluded and not present in the following list:
ValidTokenTypes =
{
TokenTypeHints.AccessToken,
TokenTypeHints.AuthorizationCode,
TokenTypeHints.DeviceCode,
TokenTypeHints.IdToken,
TokenTypeHints.RefreshToken,
TokenTypeHints.UserCode
}
};
await _dispatcher.DispatchAsync(notification);

6
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs

@ -57,6 +57,12 @@ public static partial class OpenIddictValidationDataProtectionHandlers
return default;
}
// If a specific token format is expected, return immediately if it doesn't match the expected value.
if (context.TokenFormat is not null && context.TokenFormat is not TokenFormats.Private.DataProtection)
{
return default;
}
// Note: ASP.NET Core Data Protection tokens created by the default implementation always start
// with "CfDJ8", that corresponds to the base64 representation of the "09 F0 C9 F0" value used
// by KeyRingBasedDataProtectionProvider as a Data Protection version identifier/magic header.

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

@ -49,6 +49,11 @@ public static partial class OpenIddictValidationEvents
/// </summary>
public string Token { get; set; } = default!;
/// <summary>
/// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to validate, if applicable.
/// </summary>
public string? TokenFormat { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the validated token is a reference token.
/// </summary>

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

@ -235,6 +235,12 @@ public static partial class OpenIddictValidationHandlers
return;
}
// If a specific token format is expected, return immediately if it doesn't match the expected value.
if (context.TokenFormat is not null && context.TokenFormat is not TokenFormats.Jwt)
{
return;
}
// If the token cannot be read, don't return an error to allow another handler to validate it.
if (!context.SecurityTokenHandler.CanReadToken(context.Token))
{

85
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs

@ -131,6 +131,91 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Magnifique", (string?) response["name"]);
}
[Fact]
public async Task ValidateDeviceRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = null,
ClientId = "Fabrikam"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateDeviceRequest_RequestIsRejectedWhenClientAssertionTypeIsSpecifiedWithoutAssertion()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateDeviceRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown",
ClientId = "Fabrikam"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
}
[Fact]
public async Task ValidateDeviceRequest_RequestIsRejectedWhenMultipleCredentialsAreSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/device", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2087), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2087), response.ErrorUri);
}
[Fact]
public async Task ValidateDeviceRequest_MissingClientIdCausesAnError()
{

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

@ -166,13 +166,13 @@ public abstract partial class OpenIddictServerIntegrationTests
});
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_MissingCodeCausesAnError()
public async Task ValidateTokenRequest_MissingClientIdCausesAnErrorForClientCredentialsRequests()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
@ -181,19 +181,20 @@ public abstract partial class OpenIddictServerIntegrationTests
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
Code = null,
GrantType = GrantTypes.AuthorizationCode
ClientAssertion = null,
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = null,
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2029(Parameters.Code), response.ErrorDescription);
Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_MissingRefreshTokenCausesAnError()
public async Task ValidateTokenRequest_MissingCodeCausesAnError()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
@ -202,21 +203,19 @@ public abstract partial class OpenIddictServerIntegrationTests
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.RefreshToken,
RefreshToken = null
ClientId = "Fabrikam",
Code = null,
GrantType = GrantTypes.AuthorizationCode
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2029(Parameters.RefreshToken), response.ErrorDescription);
Assert.Equal(SR.FormatID2029(Parameters.Code), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
[Theory]
[InlineData(null, null)]
[InlineData("client_id", null)]
[InlineData(null, "client_secret")]
public async Task ValidateTokenRequest_MissingClientCredentialsCauseAnError(string identifier, string secret)
[Fact]
public async Task ValidateTokenRequest_MissingRefreshTokenCausesAnError()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
@ -225,15 +224,14 @@ public abstract partial class OpenIddictServerIntegrationTests
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = identifier,
ClientSecret = secret,
GrantType = GrantTypes.ClientCredentials
GrantType = GrantTypes.RefreshToken,
RefreshToken = null
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2057(Parameters.ClientId, Parameters.ClientSecret), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2057), response.ErrorUri);
Assert.Equal(SR.FormatID2029(Parameters.RefreshToken), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
[Theory]
@ -1372,10 +1370,30 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.AccessToken);
}
[Theory]
[InlineData("client_id", "")]
[InlineData("", "client_secret")]
public async Task ValidateTokenRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing(string identifier, string secret)
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = null,
ClientId = "Fabrikam",
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenClientAssertionTypeIsSpecifiedWithoutAssertion()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
@ -1384,14 +1402,83 @@ public abstract partial class OpenIddictServerIntegrationTests
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = identifier,
ClientSecret = secret,
ClientAssertion = null,
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown",
ClientId = "Fabrikam",
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenMultipleCredentialsAreSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2087), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2087), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = null,
ClientId = "Fabrikam",
ClientSecret = null,
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2057(Parameters.ClientId, Parameters.ClientSecret), response.ErrorDescription);
Assert.Equal(SR.FormatID2057(Parameters.ClientSecret, Parameters.ClientAssertion), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2057), response.ErrorUri);
}

127
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using System.Net.Http;
using System.Security.Claims;
using System.Text.Json;
@ -151,6 +152,95 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
[Fact]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = null,
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientAssertionTypeIsSpecifiedWithoutAssertion()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown",
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
}
[Fact]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenMultipleCredentialsAreSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2087), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2087), response.ErrorUri);
}
[Fact]
public async Task ValidateIntrospectionRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired()
{
@ -457,7 +547,6 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData(TokenTypeHints.DeviceCode)]
[InlineData(TokenTypeHints.IdToken)]
[InlineData(TokenTypeHints.UserCode)]
[InlineData("custom_token")]
public async Task ValidateIntrospectionRequest_UnsupportedTokenTypeCausesAnError(string type)
{
// Arrange
@ -1163,6 +1252,15 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasTypeAsync(token, ImmutableArray.Create(
TokenTypeHints.AccessToken,
TokenTypeHints.AuthorizationCode,
TokenTypeHints.DeviceCode,
TokenTypeHints.IdToken,
TokenTypeHints.RefreshToken,
TokenTypeHints.UserCode), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
}));
@ -1255,6 +1353,15 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasTypeAsync(token, ImmutableArray.Create(
TokenTypeHints.AccessToken,
TokenTypeHints.AuthorizationCode,
TokenTypeHints.DeviceCode,
TokenTypeHints.IdToken,
TokenTypeHints.RefreshToken,
TokenTypeHints.UserCode), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
}));
@ -1354,6 +1461,15 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasTypeAsync(token, ImmutableArray.Create(
TokenTypeHints.AccessToken,
TokenTypeHints.AuthorizationCode,
TokenTypeHints.DeviceCode,
TokenTypeHints.IdToken,
TokenTypeHints.RefreshToken,
TokenTypeHints.UserCode), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
}));
@ -1407,6 +1523,15 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasTypeAsync(token, ImmutableArray.Create(
TokenTypeHints.AccessToken,
TokenTypeHints.AuthorizationCode,
TokenTypeHints.DeviceCode,
TokenTypeHints.IdToken,
TokenTypeHints.RefreshToken,
TokenTypeHints.UserCode), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
await using var server = await CreateServerAsync(options =>

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

@ -480,7 +480,6 @@ public abstract partial class OpenIddictServerIntegrationTests
builder.UseInlineHandler(context =>
{
Assert.Equal("access_token", context.Token);
Assert.Equal(Array.Empty<string>(), context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(null)

90
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs

@ -151,6 +151,95 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri);
}
[Fact]
public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = null,
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientAssertionTypeIsSpecifiedWithoutAssertion()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientAssertion = null,
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2037), response.ErrorUri);
}
[Fact]
public async Task ValidateRevocationRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown",
ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
}
[Fact]
public async Task ValidateRevocationRequest_RequestIsRejectedWhenMultipleCredentialsAreSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
{
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2087), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2087), response.ErrorUri);
}
[Fact]
public async Task ValidateRevocationRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired()
{
@ -398,7 +487,6 @@ public abstract partial class OpenIddictServerIntegrationTests
[InlineData(TokenTypeHints.DeviceCode)]
[InlineData(TokenTypeHints.IdToken)]
[InlineData(TokenTypeHints.UserCode)]
[InlineData("custom_token")]
public async Task ValidateRevocationRequest_UnsupportedTokenTypeCausesAnError(string type)
{
// Arrange

Loading…
Cancel
Save