diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs
index 4b9d6c7f..4cef05d4 100644
--- a/samples/Mvc.Server/Startup.cs
+++ b/samples/Mvc.Server/Startup.cs
@@ -75,6 +75,9 @@ namespace Mvc.Server
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
+ // Mark the "profile" scope as a supported scope in the discovery document.
+ options.RegisterScopes(OpenIdConnectConstants.Scopes.Profile);
+
// Make the "client_id" parameter mandatory when sending a token request.
options.RequireClientIdentification();
diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index 6733c2f6..d3bc6abe 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -412,6 +412,51 @@ namespace OpenIddict.Core
await UpdateAsync(application, cancellationToken);
}
+ ///
+ /// Validates the client_secret associated with an application.
+ ///
+ /// The application.
+ /// The secret that should be compared to the client_secret stored in the database.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns a boolean indicating whether the client secret was valid.
+ ///
+ public virtual async Task ValidateClientSecretAsync([NotNull] TApplication application, string secret, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (!await IsConfidentialAsync(application, cancellationToken))
+ {
+ Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications.");
+
+ return false;
+ }
+
+ var value = await Store.GetClientSecretAsync(application, cancellationToken);
+ if (string.IsNullOrEmpty(value))
+ {
+ Logger.LogError("Client authentication failed for {Client} because " +
+ "no client secret was associated with the application.");
+
+ return false;
+ }
+
+ if (!await ValidateClientSecretAsync(secret, value, cancellationToken))
+ {
+ Logger.LogWarning("Client authentication failed for {Client}.",
+ await GetDisplayNameAsync(application, cancellationToken));
+
+ return false;
+ }
+
+ return true;
+ }
+
///
/// Validates the specified post_logout_redirect_uri.
///
@@ -470,51 +515,6 @@ namespace OpenIddict.Core
return false;
}
- ///
- /// Validates the client_secret associated with an application.
- ///
- /// The application.
- /// The secret that should be compared to the client_secret stored in the database.
- /// The that can be used to abort the operation.
- /// A that can be used to monitor the asynchronous operation.
- ///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns a boolean indicating whether the client secret was valid.
- ///
- public virtual async Task ValidateClientSecretAsync([NotNull] TApplication application, string secret, CancellationToken cancellationToken)
- {
- if (application == null)
- {
- throw new ArgumentNullException(nameof(application));
- }
-
- if (!await IsConfidentialAsync(application, cancellationToken))
- {
- Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications.");
-
- return false;
- }
-
- var value = await Store.GetClientSecretAsync(application, cancellationToken);
- if (string.IsNullOrEmpty(value))
- {
- Logger.LogError("Client authentication failed for {Client} because " +
- "no client secret was associated with the application.");
-
- return false;
- }
-
- if (!await ValidateClientSecretAsync(secret, value, cancellationToken))
- {
- Logger.LogWarning("Client authentication failed for {Client}.",
- await GetDisplayNameAsync(application, cancellationToken));
-
- return false;
- }
-
- return true;
- }
-
///
/// Validates the application to ensure it's in a consistent state.
///
diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs
index 55eec5dd..7b489070 100644
--- a/src/OpenIddict/OpenIddictExtensions.cs
+++ b/src/OpenIddict/OpenIddictExtensions.cs
@@ -177,6 +177,12 @@ namespace Microsoft.AspNetCore.Builder
"or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.");
}
+ // Automatically add the offline_access scope if the refresh token grant has been enabled.
+ if (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
+ {
+ options.Scopes.Add(OpenIdConnectConstants.Scopes.OfflineAccess);
+ }
+
return app.UseOpenIdConnectServer(options);
}
@@ -901,6 +907,34 @@ namespace Microsoft.AspNetCore.Builder
return builder.Configure(options => options.Issuer = address);
}
+ ///
+ /// Registers the specified scopes as supported scopes so
+ /// they can be returned as part of the discovery document.
+ ///
+ /// The services builder used by OpenIddict to register new services.
+ /// The supported scopes.
+ /// The .
+ public static OpenIddictBuilder RegisterScopes(
+ [NotNull] this OpenIddictBuilder builder, [NotNull] params string[] scopes)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (scopes == null)
+ {
+ throw new ArgumentNullException(nameof(scopes));
+ }
+
+ if (scopes.Any(scope => string.IsNullOrEmpty(scope)))
+ {
+ throw new ArgumentException("Scopes cannot be null or empty.", nameof(scopes));
+ }
+
+ return builder.Configure(options => options.Scopes.UnionWith(scopes));
+ }
+
///
/// Configures OpenIddict to use a specific data protection provider
/// instead of relying on the default instance provided by the DI container.
diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs
index d8a9297d..511e24bc 100644
--- a/src/OpenIddict/OpenIddictOptions.cs
+++ b/src/OpenIddict/OpenIddictOptions.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
+using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.Extensions.Caching.Distributed;
@@ -61,6 +62,14 @@ namespace OpenIddict
///
public bool RequireClientIdentification { get; set; }
+ ///
+ /// Gets the OAuth2/OpenID Connect scopes enabled for this application.
+ ///
+ public ISet Scopes { get; } = new HashSet(StringComparer.Ordinal)
+ {
+ OpenIdConnectConstants.Scopes.OpenId
+ };
+
///
/// Gets or sets a boolean indicating whether reference tokens should be used.
/// When set to true, authorization codes, access tokens and refresh tokens
diff --git a/src/OpenIddict/OpenIddictProvider.Discovery.cs b/src/OpenIddict/OpenIddictProvider.Discovery.cs
index 59a54852..302db4d9 100644
--- a/src/OpenIddict/OpenIddictProvider.Discovery.cs
+++ b/src/OpenIddict/OpenIddictProvider.Discovery.cs
@@ -30,20 +30,16 @@ namespace OpenIddict
// Note: the OpenID Connect server middleware automatically populates grant_types_supported
// by determining whether the authorization and token endpoints are enabled or not but
// OpenIddict uses a different approach and relies on a configurable "grants list".
- context.GrantTypes.IntersectWith(options.GrantTypes);
-
- // Note: the "openid" scope is automatically
- // added by the OpenID Connect server middleware.
- context.Scopes.Add(OpenIdConnectConstants.Scopes.Profile);
- context.Scopes.Add(OpenIdConnectConstants.Scopes.Email);
- context.Scopes.Add(OpenIdConnectConstants.Scopes.Phone);
- context.Scopes.Add(OpenIddictConstants.Scopes.Roles);
-
- // Only add the "offline_access" scope if the refresh token grant is enabled.
- if (context.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
- {
- context.Scopes.Add(OpenIdConnectConstants.Scopes.OfflineAccess);
- }
+ context.GrantTypes.Clear();
+ context.GrantTypes.UnionWith(options.GrantTypes);
+
+ // Only return the scopes configured by the developer.
+ context.Scopes.Clear();
+ context.Scopes.UnionWith(options.Scopes);
+
+ // 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.ClaimsSupported] = false;
context.Metadata[OpenIddictConstants.Metadata.ExternalProvidersSupported] = new JArray(
from provider in context.HttpContext.Authentication.GetAuthenticationSchemes()
diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
index f8ea2a56..f166184d 100644
--- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
+++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
@@ -771,6 +771,26 @@ namespace OpenIddict.Tests
Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Value.Issuer);
}
+ [Fact]
+ public void RegisterScopes_ScopeIsAdded()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddOptions();
+
+ var builder = new OpenIddictBuilder(services);
+
+ // Act
+ builder.RegisterScopes("custom_scope_1", "custom_scope_2");
+
+ var provider = services.BuildServiceProvider();
+ var options = provider.GetRequiredService>();
+
+ // Assert
+ Assert.Contains("custom_scope_1", options.Value.Scopes);
+ Assert.Contains("custom_scope_2", options.Value.Scopes);
+ }
+
[Fact]
public void UseDataProtectionProvider_DefaultProviderIsReplaced()
{
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
index 79740b26..b8e4b691 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
@@ -61,11 +61,8 @@ namespace OpenIddict.Tests
}
[Theory]
- [InlineData(OpenIdConnectConstants.Scopes.Profile)]
- [InlineData(OpenIdConnectConstants.Scopes.Email)]
- [InlineData(OpenIdConnectConstants.Scopes.Phone)]
- [InlineData(OpenIddictConstants.Scopes.Roles)]
- public async Task HandleConfigurationRequest_StandardScopesAreExposed(string scope)
+ [InlineData(OpenIdConnectConstants.Scopes.OpenId)]
+ public async Task HandleConfigurationRequest_DefaultScopesAreAutomaticallyReturned(string scope)
{
// Arrange
var server = CreateAuthorizationServer();
@@ -79,6 +76,28 @@ namespace OpenIddict.Tests
Assert.Contains(scope, ((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values());
}
+ [Fact]
+ public async Task HandleConfigurationRequest_CustomScopeIsReturned()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Configure(options =>
+ {
+ options.Scopes.Clear();
+ options.Scopes.Add("custom_scope");
+ });
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.GetAsync(ConfigurationEndpoint);
+
+ // Assert
+ Assert.Contains("custom_scope", ((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values());
+ }
+
[Fact]
public async Task HandleConfigurationRequest_OfflineAccessScopeIsReturnedWhenRefreshTokenFlowIsEnabled()
{
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs
index 619b112d..153ca18a 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs
@@ -50,6 +50,7 @@ namespace OpenIddict.Tests
// Replace the default OpenIddict managers.
services.AddSingleton(CreateApplicationManager());
services.AddSingleton(CreateAuthorizationManager());
+ services.AddSingleton(CreateScopeManager());
services.AddSingleton(CreateTokenManager());
services.AddOpenIddict(options =>
@@ -201,6 +202,17 @@ namespace OpenIddict.Tests
return manager.Object;
}
+ private static OpenIddictScopeManager CreateScopeManager(Action>> setup = null)
+ {
+ var manager = new Mock>(
+ Mock.Of>(),
+ Mock.Of>>());
+
+ setup?.Invoke(manager);
+
+ return manager.Object;
+ }
+
private static OpenIddictTokenManager CreateTokenManager(Action>> setup = null)
{
var manager = new Mock>(