diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs
index 24810e50..659b7c87 100644
--- a/src/OpenIddict/OpenIddictExtensions.cs
+++ b/src/OpenIddict/OpenIddictExtensions.cs
@@ -847,6 +847,34 @@ namespace Microsoft.AspNetCore.Builder
return builder.Configure(options => options.Issuer = address);
}
+ ///
+ /// Registers the specified claims as supported claims so
+ /// they can be returned as part of the discovery document.
+ ///
+ /// The services builder used by OpenIddict to register new services.
+ /// The supported claims.
+ /// The .
+ public static OpenIddictBuilder RegisterClaims(
+ [NotNull] this OpenIddictBuilder builder, [NotNull] params string[] claims)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (claims == null)
+ {
+ throw new ArgumentNullException(nameof(claims));
+ }
+
+ if (claims.Any(claim => string.IsNullOrEmpty(claim)))
+ {
+ throw new ArgumentException("Claims cannot be null or empty.", nameof(claims));
+ }
+
+ return builder.Configure(options => options.Claims.UnionWith(claims));
+ }
+
///
/// Registers the specified scopes as supported scopes so
/// they can be returned as part of the discovery document.
diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs
index 6e35223f..cf4c62e9 100644
--- a/src/OpenIddict/OpenIddictOptions.cs
+++ b/src/OpenIddict/OpenIddictOptions.cs
@@ -35,6 +35,11 @@ namespace OpenIddict
///
public IDistributedCache Cache { get; set; }
+ ///
+ /// Gets the OAuth2/OpenID Connect claims supported by this application.
+ ///
+ public ISet Claims { get; } = new HashSet(StringComparer.Ordinal);
+
///
/// Gets or sets a boolean indicating whether token revocation should be disabled.
/// When disabled, authorization code and refresh tokens are not stored
diff --git a/src/OpenIddict/OpenIddictProvider.Discovery.cs b/src/OpenIddict/OpenIddictProvider.Discovery.cs
index 87d54001..467c3503 100644
--- a/src/OpenIddict/OpenIddictProvider.Discovery.cs
+++ b/src/OpenIddict/OpenIddictProvider.Discovery.cs
@@ -39,6 +39,11 @@ namespace OpenIddict
context.Scopes.Clear();
context.Scopes.UnionWith(options.Scopes);
+ // Note: claims_supported is a recommended parameter but is not strictly required.
+ // If no claim was registered, the claims_supported property will be automatically
+ // excluded from the response by the OpenID Connect server middleware.
+ context.Metadata[OpenIdConnectConstants.Metadata.ClaimsSupported] = new JArray(options.Claims);
+
// Note: the optional "claims" parameter is not supported by OpenIddict,
// so a "false" flag is returned to encourage clients not to use it.
context.Metadata[OpenIdConnectConstants.Metadata.ClaimsParameterSupported] = false;
diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
index ee4da70e..d04ad101 100644
--- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
+++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
@@ -583,7 +583,24 @@ namespace OpenIddict.Tests
}
[Fact]
- public void RegisterScopes_ScopeIsAdded()
+ public void RegisterClaims_ClaimsAreAdded()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = new OpenIddictBuilder(services);
+
+ // Act
+ builder.RegisterClaims("custom_claim_1", "custom_claim_2");
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.Contains("custom_claim_1", options.Claims);
+ Assert.Contains("custom_claim_2", options.Claims);
+ }
+
+ [Fact]
+ public void RegisterScopes_ScopesAreAdded()
{
// Arrange
var services = CreateServices();
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
index 23f16b13..87d4e311 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
@@ -138,6 +138,39 @@ namespace OpenIddict.Tests
((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values());
}
+ [Fact]
+ public async Task HandleConfigurationRequest_NoSupportedClaimsPropertyIsReturnedWhenNoClaimIsConfigured()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer();
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.GetAsync(ConfigurationEndpoint);
+
+ // Assert
+ Assert.False(response.HasParameter(OpenIdConnectConstants.Metadata.ClaimsSupported));
+ }
+
+ [Fact]
+ public async Task HandleConfigurationRequest_ConfiguredClaimsAreReturned()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Configure(options => options.Claims.Add("custom_claim"));
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.GetAsync(ConfigurationEndpoint);
+
+ // Assert
+ Assert.Contains("custom_claim", ((JArray) response[OpenIdConnectConstants.Metadata.ClaimsSupported]).Values());
+ }
+
[Fact]
public async Task HandleConfigurationRequest_ClaimsParameterSupportedIsReturned()
{