Browse Source

Backport the scopes changes to OpenIddict 1.x

pull/553/head
Kévin Chalet 9 years ago
parent
commit
15982299ae
  1. 3
      samples/Mvc.Server/Startup.cs
  2. 90
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  3. 34
      src/OpenIddict/OpenIddictExtensions.cs
  4. 9
      src/OpenIddict/OpenIddictOptions.cs
  5. 24
      src/OpenIddict/OpenIddictProvider.Discovery.cs
  6. 20
      test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
  7. 29
      test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs
  8. 12
      test/OpenIddict.Tests/OpenIddictProviderTests.cs

3
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();

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

@ -412,6 +412,51 @@ namespace OpenIddict.Core
await UpdateAsync(application, cancellationToken);
}
/// <summary>
/// Validates the client_secret associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The secret that should be compared to the client_secret stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
/// </returns>
public virtual async Task<bool> 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;
}
/// <summary>
/// Validates the specified post_logout_redirect_uri.
/// </summary>
@ -470,51 +515,6 @@ namespace OpenIddict.Core
return false;
}
/// <summary>
/// Validates the client_secret associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The secret that should be compared to the client_secret stored in the database.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
/// </returns>
public virtual async Task<bool> 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;
}
/// <summary>
/// Validates the application to ensure it's in a consistent state.
/// </summary>

34
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);
}
/// <summary>
/// Registers the specified scopes as supported scopes so
/// they can be returned as part of the discovery document.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="scopes">The supported scopes.</param>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
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));
}
/// <summary>
/// Configures OpenIddict to use a specific data protection provider
/// instead of relying on the default instance provided by the DI container.

9
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
/// </summary>
public bool RequireClientIdentification { get; set; }
/// <summary>
/// Gets the OAuth2/OpenID Connect scopes enabled for this application.
/// </summary>
public ISet<string> Scopes { get; } = new HashSet<string>(StringComparer.Ordinal)
{
OpenIdConnectConstants.Scopes.OpenId
};
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens should be used.
/// When set to <c>true</c>, authorization codes, access tokens and refresh tokens

24
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()

20
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<IOptions<OpenIddictOptions>>();
// Assert
Assert.Contains("custom_scope_1", options.Value.Scopes);
Assert.Contains("custom_scope_2", options.Value.Scopes);
}
[Fact]
public void UseDataProtectionProvider_DefaultProviderIsReplaced()
{

29
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<string>());
}
[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<string>());
}
[Fact]
public async Task HandleConfigurationRequest_OfflineAccessScopeIsReturnedWhenRefreshTokenFlowIsEnabled()
{

12
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<OpenIddictScope> CreateScopeManager(Action<Mock<OpenIddictScopeManager<OpenIddictScope>>> setup = null)
{
var manager = new Mock<OpenIddictScopeManager<OpenIddictScope>>(
Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>(),
Mock.Of<ILogger<OpenIddictScopeManager<OpenIddictScope>>>());
setup?.Invoke(manager);
return manager.Object;
}
private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> setup = null)
{
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(

Loading…
Cancel
Save