Browse Source

Introduce the ability to specify multiple callback URLs (redirect_uri and post_logout_redirect_uri)

pull/474/head
Kévin Chalet 9 years ago
parent
commit
8488dd4f81
  1. 15
      samples/Mvc.Server/Startup.cs
  2. 21
      src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs
  3. 8
      src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs
  4. 169
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  5. 14
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  6. 5
      src/OpenIddict.Core/OpenIddictConstants.cs
  7. 46
      src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs
  8. 18
      src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
  9. 8
      src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
  10. 235
      src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs
  11. 19
      src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
  12. 28
      src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
  13. 23
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  14. 13
      src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
  15. 7
      src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
  16. 23
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  17. 13
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
  18. 7
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
  19. 14
      src/OpenIddict.Models/OpenIddictApplication.cs
  20. 2
      src/OpenIddict.Models/OpenIddictAuthorization.cs
  21. 19
      src/OpenIddict/OpenIddictProvider.Authentication.cs
  22. 28
      src/OpenIddict/OpenIddictProvider.Serialization.cs
  23. 42
      src/OpenIddict/OpenIddictProvider.Session.cs
  24. 62
      test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
  25. 18
      test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
  26. 37
      test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs

15
samples/Mvc.Server/Startup.cs

@ -145,15 +145,16 @@ namespace Mvc.Server
if (await manager.FindByClientIdAsync("mvc", cancellationToken) == null)
{
var application = new OpenIddictApplication
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
DisplayName = "MVC client application",
LogoutRedirectUri = "http://localhost:53507/signout-callback-oidc",
RedirectUri = "http://localhost:53507/signin-oidc"
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") }
};
await manager.CreateAsync(application, "901564A5-E7FE-42CB-B10D-61EF6A8F3654", cancellationToken);
await manager.CreateAsync(descriptor, cancellationToken);
}
// To test this sample with Postman, use the following settings:
@ -167,14 +168,14 @@ namespace Mvc.Server
// * Request access token locally: yes
if (await manager.FindByClientIdAsync("postman", cancellationToken) == null)
{
var application = new OpenIddictApplication
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "postman",
DisplayName = "Postman",
RedirectUri = "https://www.getpostman.com/oauth2/callback"
RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") }
};
await manager.CreateAsync(application, cancellationToken);
await manager.CreateAsync(descriptor, cancellationToken);
}
}
}

21
src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs

@ -1,4 +1,7 @@
namespace OpenIddict.Core
using System;
using System.Collections.Generic;
namespace OpenIddict.Core
{
/// <summary>
/// Represents an OpenIddict application descriptor.
@ -9,37 +12,37 @@
/// Gets or sets the client identifier
/// associated with the application.
/// </summary>
public virtual string ClientId { get; set; }
public string ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret associated with the application.
/// Note: depending on the application manager used when creating it,
/// this property may be hashed or encrypted for security reasons.
/// </summary>
public virtual string ClientSecret { get; set; }
public string ClientSecret { get; set; }
/// <summary>
/// Gets or sets the display name
/// associated with the application.
/// </summary>
public virtual string DisplayName { get; set; }
public string DisplayName { get; set; }
/// <summary>
/// Gets or sets the logout callback URL
/// Gets the logout callback URLs
/// associated with the application.
/// </summary>
public virtual string LogoutRedirectUri { get; set; }
public ISet<Uri> PostLogoutRedirectUris { get; } = new HashSet<Uri>();
/// <summary>
/// Gets or sets the callback URL
/// Gets the callback URLs
/// associated with the application.
/// </summary>
public virtual string RedirectUri { get; set; }
public ISet<Uri> RedirectUris { get; } = new HashSet<Uri>();
/// <summary>
/// Gets or sets the application type
/// associated with the application.
/// </summary>
public virtual string Type { get; set; }
public string Type { get; set; }
}
}

8
src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace OpenIddict.Core
{
@ -13,9 +14,10 @@ namespace OpenIddict.Core
public string ApplicationId { get; set; }
/// <summary>
/// Gets or sets the scopes associated with the authorization.
/// Gets the scopes associated with the authorization.
/// </summary>
public IEnumerable<string> Scopes { get; set; }
public ISet<string> Scopes { get; } =
new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the status associated with the authorization.

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

@ -206,9 +206,14 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
public virtual Task<TApplication[]> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{
return Store.FindByLogoutRedirectUriAsync(address, cancellationToken);
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
return Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
}
/// <summary>
@ -220,8 +225,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken)
public virtual Task<TApplication[]> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
return Store.FindByRedirectUriAsync(address, cancellationToken);
}
@ -245,6 +255,25 @@ namespace OpenIddict.Core
return Store.GetAsync(query, cancellationToken);
}
/// <summary>
/// Retrieves the client identifier 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="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
/// </returns>
public virtual Task<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetClientIdAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the client type associated with an application.
/// </summary>
@ -332,25 +361,6 @@ namespace OpenIddict.Core
return Store.GetTokensAsync(application, cancellationToken);
}
/// <summary>
/// Determines whether the specified application has a redirect_uri.
/// </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="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether a redirect_uri is registered.
/// </returns>
public virtual async Task<bool> HasRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return !string.IsNullOrEmpty(await Store.GetRedirectUriAsync(application, cancellationToken));
}
/// <summary>
/// Determines whether an application is a confidential client.
/// </summary>
@ -502,7 +512,8 @@ namespace OpenIddict.Core
/// 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)
public virtual async Task<bool> ValidateClientSecretAsync(
[NotNull] TApplication application, string secret, CancellationToken cancellationToken)
{
if (application == null)
{
@ -528,7 +539,7 @@ namespace OpenIddict.Core
if (!await ValidateClientSecretAsync(secret, value, cancellationToken))
{
Logger.LogWarning("Client authentication failed for {Client}.",
await GetDisplayNameAsync(application, cancellationToken));
await GetClientIdAsync(application, cancellationToken));
return false;
}
@ -545,16 +556,25 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns a boolean indicating whether the post_logout_redirect_uri was valid.
/// </returns>
public virtual async Task<bool> ValidateLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
public virtual async Task<bool> ValidatePostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
// Warning: SQL engines like Microsoft SQL Server are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here.
foreach (var application in await Store.FindByLogoutRedirectUriAsync(address, cancellationToken))
foreach (var application in await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken))
{
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
if (string.Equals(address, await Store.GetLogoutRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal))
foreach (var uri in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
return true;
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
if (string.Equals(uri, address, StringComparison.Ordinal))
{
return true;
}
}
}
@ -565,31 +585,40 @@ namespace OpenIddict.Core
}
/// <summary>
/// Validates the redirect_uri associated with an application.
/// Validates the redirect_uri to ensure it's associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="address">The address that should be compared to the redirect_uri stored in the database.</param>
/// <param name="address">The address that should be compared to one of the redirect_uri 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,
/// whose result returns a boolean indicating whether the redirect_uri was valid.
/// </returns>
public virtual async Task<bool> ValidateRedirectUriAsync([NotNull] TApplication application, string address, CancellationToken cancellationToken)
public virtual async Task<bool> ValidateRedirectUriAsync(
[NotNull] TApplication application, [NotNull] string address, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
if (string.Equals(address, await Store.GetRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal))
if (string.IsNullOrEmpty(address))
{
return true;
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
foreach (var uri in await Store.GetRedirectUrisAsync(application, cancellationToken))
{
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
if (string.Equals(uri, address, StringComparison.Ordinal))
{
return true;
}
}
Logger.LogWarning("Client validation failed because '{RedirectUri}' was not a valid redirect_uri " +
"for '{Client}'.", address, await GetDisplayNameAsync(application, cancellationToken));
"for {Client}.", address, await GetClientIdAsync(application, cancellationToken));
return false;
}
@ -609,11 +638,43 @@ namespace OpenIddict.Core
ClientId = await Store.GetClientIdAsync(application, cancellationToken),
ClientSecret = await Store.GetClientSecretAsync(application, cancellationToken),
DisplayName = await Store.GetDisplayNameAsync(application, cancellationToken),
LogoutRedirectUri = await Store.GetLogoutRedirectUriAsync(application, cancellationToken),
RedirectUri = await Store.GetRedirectUriAsync(application, cancellationToken),
Type = await Store.GetClientTypeAsync(application, cancellationToken)
};
foreach (var address in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("Callback URLs cannot be null or empty.");
}
// Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
{
throw new ArgumentException("Callback URLs must be valid absolute URLs.");
}
descriptor.PostLogoutRedirectUris.Add(uri);
}
foreach (var address in await Store.GetRedirectUrisAsync(application, cancellationToken))
{
// Ensure the address is not null or empty.
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("Callback URLs cannot be null or empty.");
}
// Ensure the address is a valid absolute URL.
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
{
throw new ArgumentException("Callback URLs must be valid absolute URLs.");
}
descriptor.RedirectUris.Add(uri);
}
await ValidateAsync(descriptor, cancellationToken);
}
@ -665,36 +726,26 @@ namespace OpenIddict.Core
throw new ArgumentException("A client secret cannot be associated with a public application.", nameof(descriptor));
}
// When a redirect_uri is specified, ensure it is valid and spec-compliant.
// When callback URLs are specified, ensure they are valid and spec-compliant.
// See https://tools.ietf.org/html/rfc6749#section-3.1 for more information.
if (!string.IsNullOrEmpty(descriptor.RedirectUri))
foreach (var uri in descriptor.PostLogoutRedirectUris.Concat(descriptor.RedirectUris))
{
// Ensure the redirect_uri is a valid and absolute URL.
if (!Uri.TryCreate(descriptor.RedirectUri, UriKind.Absolute, out Uri uri))
{
throw new ArgumentException("The redirect_uri must be an absolute URL.");
}
// Ensure the redirect_uri doesn't contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
// Ensure the address is not null.
if (uri == null)
{
throw new ArgumentException("The redirect_uri cannot contain a fragment.");
throw new ArgumentException("Callback URLs cannot be null.");
}
}
// When a post_logout_redirect_uri is specified, ensure it is valid.
if (!string.IsNullOrEmpty(descriptor.LogoutRedirectUri))
{
// Ensure the post_logout_redirect_uri is a valid and absolute URL.
if (!Uri.TryCreate(descriptor.LogoutRedirectUri, UriKind.Absolute, out Uri uri))
// Ensure the address is a valid and absolute URL.
if (!uri.IsAbsoluteUri || !uri.IsWellFormedOriginalString())
{
throw new ArgumentException("The post_logout_redirect_uri must be an absolute URL.");
throw new ArgumentException("Callback URLs must be valid absolute URLs.");
}
// Ensure the post_logout_redirect_uri doesn't contain a fragment.
// Ensure the address doesn't contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
throw new ArgumentException("The post_logout_redirect_uri cannot contain a fragment.");
throw new ArgumentException("Callback URLs cannot contain a fragment.");
}
}

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

@ -253,6 +253,20 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.");
}
// Ensure that the scopes are not null or empty and do not contain spaces.
foreach (var scope in descriptor.Scopes)
{
if (string.IsNullOrEmpty(scope))
{
throw new ArgumentException("Scopes cannot be null or empty.", nameof(descriptor));
}
if (scope.Contains(OpenIddictConstants.Separators.Space))
{
throw new ArgumentException("Scopes cannot contain spaces.", nameof(descriptor));
}
}
return Task.CompletedTask;
}
}

5
src/OpenIddict.Core/OpenIddictConstants.cs

@ -37,6 +37,11 @@ namespace OpenIddict.Core
public const string TokenId = ".token_id";
}
public static class Separators
{
public const string Space = " ";
}
public static class Scopes
{
public const string Roles = "roles";

46
src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs

@ -58,7 +58,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
Task<TApplication> FindByIdAsync(string identifier, CancellationToken cancellationToken);
Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an application using its client identifier.
@ -69,7 +69,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
Task<TApplication> FindByClientIdAsync(string identifier, CancellationToken cancellationToken);
Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
@ -80,7 +80,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken);
Task<TApplication[]> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
@ -91,7 +91,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken);
Task<TApplication[]> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query.
@ -163,26 +163,26 @@ namespace OpenIddict.Core
Task<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the logout callback address associated with an application.
/// Retrieves the logout callback addresses 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="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the post_logout_redirect_uri associated with the application.
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose
/// result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
Task<string> GetLogoutRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken);
Task<string[]> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the callback address associated with an application.
/// Retrieves the callback addresses 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="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the redirect_uri associated with the application.
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
Task<string> GetRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken);
Task<string[]> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the token identifiers associated with an application.
@ -231,6 +231,30 @@ namespace OpenIddict.Core
/// </returns>
Task SetClientTypeAsync([NotNull] TApplication application, [NotNull] string type, CancellationToken cancellationToken);
/// <summary>
/// Sets the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The logout callback addresses 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="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
[NotNull] string[] addresses, CancellationToken cancellationToken);
/// <summary>
/// Sets the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The callback addresses 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="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task SetRedirectUrisAsync([NotNull] TApplication application,
[NotNull] string[] addresses, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing application.
/// </summary>

18
src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs

@ -39,27 +39,27 @@ namespace OpenIddict.Core
Task<TAuthorization> CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// Retrieves an authorization using its associated subject/client.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</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,
/// whose result returns the authorization corresponding to the identifier.
/// whose result returns the authorization corresponding to the subject/client.
/// </returns>
Task<TAuthorization> FindByIdAsync(string identifier, CancellationToken cancellationToken);
Task<TAuthorization> FindAsync([NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an authorization using its associated subject/client.
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="identifier">The unique identifier associated with the authorization.</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,
/// whose result returns the authorization corresponding to the subject/client.
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
Task<TAuthorization> FindAsync(string subject, string client, CancellationToken cancellationToken);
Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query.

8
src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs

@ -55,7 +55,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
Task<TToken[]> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
Task<TToken[]> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified hash.
@ -66,7 +66,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified hash.
/// </returns>
Task<TToken> FindByHashAsync(string hash, CancellationToken cancellationToken);
Task<TToken> FindByHashAsync([NotNull] string hash, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an token using its unique identifier.
@ -77,7 +77,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
Task<TToken> FindByIdAsync(string identifier, CancellationToken cancellationToken);
Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
@ -88,7 +88,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
Task<TToken[]> FindBySubjectAsync(string subject, CancellationToken cancellationToken);
Task<TToken[]> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query.

235
src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs

@ -10,6 +10,7 @@ using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using JetBrains.Annotations;
using OpenIddict.Models;
@ -68,8 +69,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public virtual Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var key = ConvertIdentifierFromString(identifier);
return GetAsync(applications => applications.Where(application => application.Id.Equals(key)), cancellationToken);
@ -84,8 +90,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> FindByClientIdAsync(string identifier, CancellationToken cancellationToken)
public virtual Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return GetAsync(applications => applications.Where(application => application.ClientId.Equals(identifier)), cancellationToken);
}
@ -98,9 +109,60 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
public virtual async Task<TApplication[]> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{
return ListAsync(applications => applications.Where(application => application.LogoutRedirectUri == address), cancellationToken);
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
// To optimize the efficiency of the query, only the applications whose stringified
// LogoutRedirectUris property contains the specified address are returned. Once the
// applications are retrieved, the LogoutRedirectUri property is manually split.
var candidates = await ListAsync(applications => applications.Where(application =>
application.PostLogoutRedirectUris.Contains(address)), cancellationToken);
if (candidates.Length == 0)
{
return Array.Empty<TApplication>();
}
// Optimization: to save an allocation when no application matches
// the specified address, the results list is lazily initialized
// when at least one matching application was found in the database.
List<TApplication> results = null;
foreach (var candidate in candidates)
{
var uris = candidate.PostLogoutRedirectUris?.Split(
OpenIdConnectConstants.Separators.Space,
StringSplitOptions.RemoveEmptyEntries);
if (uris == null)
{
continue;
}
foreach (var uri in uris)
{
// Note: the post_logout_redirect_uri must be compared
// using case-sensitive "Simple String Comparison".
if (!string.Equals(uri, address, StringComparison.Ordinal))
{
continue;
}
// Ensure the results list was initialized before using it.
if (results == null)
{
results = new List<TApplication>(capacity: 1);
}
results.Add(candidate);
}
}
return results?.ToArray() ?? Array.Empty<TApplication>();
}
/// <summary>
@ -112,9 +174,60 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken)
public virtual async Task<TApplication[]> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{
return ListAsync(applications => applications.Where(application => application.RedirectUri == address), cancellationToken);
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("The address cannot be null or empty.", nameof(address));
}
// To optimize the efficiency of the query, only the applications whose stringified
// RedirectUris property contains the specified address are returned. Once the
// applications are retrieved, the RedirectUri property is manually split.
var candidates = await ListAsync(applications => applications.Where(application =>
application.RedirectUris.Contains(address)), cancellationToken);
if (candidates.Length == 0)
{
return Array.Empty<TApplication>();
}
// Optimization: to save an allocation when no application matches
// the specified address, the results list is lazily initialized
// when at least one matching application was found in the database.
List<TApplication> results = null;
foreach (var candidate in candidates)
{
var uris = candidate.RedirectUris?.Split(
OpenIdConnectConstants.Separators.Space,
StringSplitOptions.RemoveEmptyEntries);
if (uris == null)
{
continue;
}
foreach (var uri in uris)
{
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison".
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
if (!string.Equals(uri, address, StringComparison.Ordinal))
{
continue;
}
// Ensure the results list was initialized before using it.
if (results == null)
{
results = new List<TApplication>(capacity: 1);
}
results.Add(candidate);
}
}
return results?.ToArray() ?? Array.Empty<TApplication>();
}
/// <summary>
@ -227,41 +340,55 @@ namespace OpenIddict.Core
}
/// <summary>
/// Retrieves the logout callback address associated with an application.
/// Retrieves the logout callback addresses 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="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the post_logout_redirect_uri associated with the application.
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose
/// result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
public virtual Task<string> GetLogoutRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken)
public virtual Task<string[]> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Task.FromResult(application.LogoutRedirectUri);
if (string.IsNullOrEmpty(application.PostLogoutRedirectUris))
{
return Task.FromResult(Array.Empty<string>());
}
return Task.FromResult(application.PostLogoutRedirectUris.Split(
OpenIdConnectConstants.Separators.Space,
StringSplitOptions.RemoveEmptyEntries));
}
/// <summary>
/// Retrieves the callback address associated with an application.
/// Retrieves the callback addresses 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="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the redirect_uri associated with the application.
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
public virtual Task<string> GetRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken)
public virtual Task<string[]> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Task.FromResult(application.RedirectUri);
if (string.IsNullOrEmpty(application.RedirectUris))
{
return Task.FromResult(Array.Empty<string>());
}
return Task.FromResult(application.RedirectUris.Split(
OpenIdConnectConstants.Separators.Space,
StringSplitOptions.RemoveEmptyEntries));
}
/// <summary>
@ -327,7 +454,7 @@ namespace OpenIddict.Core
application.ClientSecret = secret;
return Task.FromResult(0);
return Task.CompletedTask;
}
/// <summary>
@ -353,7 +480,81 @@ namespace OpenIddict.Core
application.Type = type;
return Task.FromResult(0);
return Task.CompletedTask;
}
/// <summary>
/// Sets the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The logout callback addresses 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="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
[NotNull] string[] addresses, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentException(nameof(application));
}
if (addresses == null)
{
throw new ArgumentException(nameof(addresses));
}
if (addresses.Any(address => string.IsNullOrEmpty(address)))
{
throw new ArgumentException("Callback addresses cannot be null or empty.", nameof(addresses));
}
if (addresses.Any(address => address.Contains(OpenIddictConstants.Separators.Space)))
{
throw new ArgumentException("Callback addresses cannot contain spaces.", nameof(addresses));
}
application.PostLogoutRedirectUris = string.Join(OpenIddictConstants.Separators.Space, addresses);
return Task.CompletedTask;
}
/// <summary>
/// Sets the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The callback addresses 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="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetRedirectUrisAsync([NotNull] TApplication application,
[NotNull] string[] addresses, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentException(nameof(application));
}
if (addresses == null)
{
throw new ArgumentException(nameof(addresses));
}
if (addresses.Any(address => string.IsNullOrEmpty(address)))
{
throw new ArgumentException("Callback addresses cannot be null or empty.", nameof(addresses));
}
if (addresses.Any(address => address.Contains(OpenIddictConstants.Separators.Space)))
{
throw new ArgumentException("Callback addresses cannot contain spaces.", nameof(addresses));
}
application.RedirectUris = string.Join(OpenIddictConstants.Separators.Space, addresses);
return Task.CompletedTask;
}
/// <summary>

19
src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs

@ -58,8 +58,18 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the subject/client.
/// </returns>
public virtual Task<TAuthorization> FindAsync(string subject, string client, CancellationToken cancellationToken)
public virtual Task<TAuthorization> FindAsync([NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
if (string.IsNullOrEmpty(client))
{
throw new ArgumentException("The client cannot be null or empty.", nameof(client));
}
var key = ConvertIdentifierFromString(client);
return GetAsync(authorizations =>
@ -78,8 +88,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public virtual Task<TAuthorization> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public virtual Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var key = ConvertIdentifierFromString(identifier);
return GetAsync(authorizations => authorizations.Where(authorization => authorization.Id.Equals(key)), cancellationToken);

28
src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs

@ -65,8 +65,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
public virtual Task<TToken[]> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
public virtual Task<TToken[]> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var key = ConvertIdentifierFromString(identifier);
return ListAsync(tokens => tokens.Where(token => token.Authorization.Id.Equals(key)), cancellationToken);
@ -81,8 +86,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified hash.
/// </returns>
public virtual Task<TToken> FindByHashAsync(string hash, CancellationToken cancellationToken)
public virtual Task<TToken> FindByHashAsync([NotNull] string hash, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(hash))
{
throw new ArgumentException("The hash cannot be null or empty.", nameof(hash));
}
return GetAsync(tokens => tokens.Where(token => token.Hash == hash), cancellationToken);
}
@ -95,8 +105,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public virtual Task<TToken> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public virtual Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var key = ConvertIdentifierFromString(identifier);
return GetAsync(tokens => tokens.Where(token => token.Id.Equals(key)), cancellationToken);
@ -111,8 +126,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public virtual Task<TToken[]> FindBySubjectAsync(string subject, CancellationToken cancellationToken)
public virtual Task<TToken[]> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
return ListAsync(tokens => tokens.Where(token => token.Subject == subject), cancellationToken);
}

23
src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs

@ -123,11 +123,23 @@ namespace OpenIddict.EntityFramework
ClientId = descriptor.ClientId,
ClientSecret = descriptor.ClientSecret,
DisplayName = descriptor.DisplayName,
LogoutRedirectUri = descriptor.LogoutRedirectUri,
RedirectUri = descriptor.RedirectUri,
Type = descriptor.Type
};
if (descriptor.PostLogoutRedirectUris.Count != 0)
{
application.PostLogoutRedirectUris = string.Join(
OpenIddictConstants.Separators.Space,
descriptor.PostLogoutRedirectUris.Select(uri => uri.OriginalString));
}
if (descriptor.RedirectUris.Count != 0)
{
application.RedirectUris = string.Join(
OpenIddictConstants.Separators.Space,
descriptor.RedirectUris.Select(uri => uri.OriginalString));
}
return CreateAsync(application, cancellationToken);
}
@ -165,8 +177,13 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public override Task<TApplication> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public override Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Applications.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
}

13
src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs

@ -125,11 +125,15 @@ namespace OpenIddict.EntityFramework
var authorization = new TAuthorization
{
Scope = string.Join(" ", descriptor.Scopes),
Status = descriptor.Status,
Subject = descriptor.Subject
};
if (descriptor.Scopes.Count != 0)
{
authorization.Scopes = string.Join(OpenIddictConstants.Separators.Space, descriptor.Scopes);
}
// Bind the authorization to the specified application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId))
{
@ -154,8 +158,13 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public override Task<TAuthorization> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public override Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Authorizations.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
}

7
src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs

@ -198,8 +198,13 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public override Task<TToken> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public override Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Tokens.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
}

23
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -122,11 +122,23 @@ namespace OpenIddict.EntityFrameworkCore
ClientId = descriptor.ClientId,
ClientSecret = descriptor.ClientSecret,
DisplayName = descriptor.DisplayName,
LogoutRedirectUri = descriptor.LogoutRedirectUri,
RedirectUri = descriptor.RedirectUri,
Type = descriptor.Type
};
if (descriptor.PostLogoutRedirectUris.Count != 0)
{
application.PostLogoutRedirectUris = string.Join(
OpenIddictConstants.Separators.Space,
descriptor.PostLogoutRedirectUris.Select(uri => uri.OriginalString));
}
if (descriptor.RedirectUris.Count != 0)
{
application.RedirectUris = string.Join(
OpenIddictConstants.Separators.Space,
descriptor.RedirectUris.Select(uri => uri.OriginalString));
}
return CreateAsync(application, cancellationToken);
}
@ -164,8 +176,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public override Task<TApplication> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public override Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Applications.FindAsync(new object[] { ConvertIdentifierFromString(identifier) }, cancellationToken);
}

13
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs

@ -124,11 +124,15 @@ namespace OpenIddict.EntityFrameworkCore
var authorization = new TAuthorization
{
Scope = string.Join(" ", descriptor.Scopes),
Status = descriptor.Status,
Subject = descriptor.Subject
};
if (descriptor.Scopes.Count != 0)
{
authorization.Scopes = string.Join(OpenIddictConstants.Separators.Space, descriptor.Scopes);
}
// Bind the authorization to the specified application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId))
{
@ -153,8 +157,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public override Task<TAuthorization> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public override Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Authorizations.FindAsync(new object[] { ConvertIdentifierFromString(identifier) }, cancellationToken);
}

7
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs

@ -197,8 +197,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public override Task<TToken> FindByIdAsync(string identifier, CancellationToken cancellationToken)
public override Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
return Tokens.FindAsync(new object[] { ConvertIdentifierFromString(identifier) }, cancellationToken);
}

14
src/OpenIddict.Models/OpenIddictApplication.cs

@ -64,16 +64,18 @@ namespace OpenIddict.Models
public virtual TKey Id { get; set; }
/// <summary>
/// Gets or sets the logout callback URL
/// associated with the current application.
/// Gets or sets the logout callback URLs
/// associated with the current application,
/// stored as a unique space-separated string.
/// </summary>
public virtual string LogoutRedirectUri { get; set; }
public virtual string PostLogoutRedirectUris { get; set; }
/// <summary>
/// Gets or sets the callback URL
/// associated with the current application.
/// Gets or sets the callback URLs
/// associated with the current application,
/// stored as a unique space-separated string.
/// </summary>
public virtual string RedirectUri { get; set; }
public virtual string RedirectUris { get; set; }
/// <summary>
/// Gets the list of the tokens associated with this application.

2
src/OpenIddict.Models/OpenIddictAuthorization.cs

@ -48,7 +48,7 @@ namespace OpenIddict.Models
/// Gets or sets the space-delimited scopes
/// associated with the current authorization.
/// </summary>
public virtual string Scope { get; set; }
public virtual string Scopes { get; set; }
/// <summary>
/// Gets or sets the status of the current authorization.

19
src/OpenIddict/OpenIddictProvider.Authentication.cs

@ -260,33 +260,20 @@ namespace OpenIddict
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Application not found in the database: ensure that your client_id is correct.");
return;
}
// Ensure a redirect_uri was associated with the application.
if (!await Applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted))
{
Logger.LogError("The authorization request was rejected because no redirect_uri " +
"was registered with the application '{ClientId}'.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "The client application is not allowed to use interactive flows.");
return;
}
// Ensure the redirect_uri is valid.
// Ensure that the specified redirect_uri is valid and is associated with the client application.
if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted))
{
Logger.LogError("The authorization request was rejected because the redirect_uri " +
"was invalid: '{RedirectUri}'.", context.RedirectUri);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid redirect_uri.");
return;

28
src/OpenIddict/OpenIddictProvider.Serialization.cs

@ -256,14 +256,7 @@ namespace OpenIddict
{
Debug.Assert(!string.IsNullOrEmpty(descriptor.ApplicationId), "The client identifier shouldn't be null.");
var authorization = await Authorizations.CreateAsync(new OpenIddictAuthorizationDescriptor
{
ApplicationId = descriptor.ApplicationId,
Scopes = request.GetScopes(),
Status = OpenIddictConstants.Statuses.Valid,
Subject = descriptor.Subject
}, context.RequestAborted);
var authorization = await CreateAuthorizationAsync(descriptor, context, request);
if (authorization != null)
{
descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted);
@ -399,5 +392,24 @@ namespace OpenIddict
return ticket;
}
private Task<TAuthorization> CreateAuthorizationAsync(
[NotNull] OpenIddictTokenDescriptor token,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request)
{
var descriptor = new OpenIddictAuthorizationDescriptor
{
ApplicationId = token.ApplicationId,
Status = OpenIddictConstants.Statuses.Valid,
Subject = token.Subject
};
foreach (var scope in request.GetScopes())
{
descriptor.Scopes.Add(scope);
}
return Authorizations.CreateAsync(descriptor, context.RequestAborted);
}
}
}

42
src/OpenIddict/OpenIddictProvider.Session.cs

@ -82,17 +82,43 @@ namespace OpenIddict
public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context)
{
// If an optional post_logout_redirect_uri was provided, validate it.
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri) &&
!await Applications.ValidateLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted))
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " +
"was invalid: '{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri);
if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
{
Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri was not " +
"a valid absolute URL: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Invalid post_logout_redirect_uri.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.");
return;
return;
}
if (!string.IsNullOrEmpty(uri.Fragment))
{
Logger.LogError("The logout request was rejected because the 'post_logout_redirect_uri' contained " +
"a URL fragment: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'post_logout_redirect_uri' parameter must not include a fragment.");
return;
}
if (!await Applications.ValidatePostLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted))
{
Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " +
"was unknown: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid post_logout_redirect_uri.");
return;
}
}
context.Validate();

62
test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs

@ -337,50 +337,12 @@ namespace OpenIddict.Tests
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error);
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Application not found in the database: ensure that your client_id is correct.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenClientHasNoRedirectUri()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = OpenIdConnectConstants.ResponseTypes.Code
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error);
Assert.Equal("The client application is not allowed to use interactive flows.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid()
{
@ -392,9 +354,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
@ -415,11 +374,10 @@ namespace OpenIddict.Tests
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error);
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Invalid redirect_uri.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
}
@ -439,9 +397,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -471,7 +426,6 @@ namespace OpenIddict.Tests
Assert.Equal("Confidential clients are not allowed to retrieve a token from the authorization endpoint.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once());
}
@ -491,9 +445,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -549,9 +500,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -628,9 +576,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -672,9 +617,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);

18
test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs

@ -237,9 +237,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -300,9 +297,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -374,9 +368,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -446,9 +437,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -507,9 +495,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -573,9 +558,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);

37
test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs

@ -61,13 +61,36 @@ namespace OpenIddict.Tests
Assert.Equal("Invalid request: timeout expired.", response.ErrorDescription);
}
[Theory]
[InlineData("/path", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")]
[InlineData("/tmp/file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")]
[InlineData("C:\\tmp\\file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")]
[InlineData("http://www.fabrikam.com/path#param=value", "The 'post_logout_redirect_uri' parameter must not include a fragment.")]
public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid(string address, string message)
{
// Arrange
var server = CreateAuthorizationServer();
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest
{
PostLogoutRedirectUri = address
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal(message, response.ErrorDescription);
}
[Fact]
public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid()
public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsUnknown()
{
// Arrange
var manager = CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
@ -85,10 +108,10 @@ namespace OpenIddict.Tests
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error);
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Invalid post_logout_redirect_uri.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
@ -101,7 +124,7 @@ namespace OpenIddict.Tests
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
@ -139,7 +162,7 @@ namespace OpenIddict.Tests
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
});
@ -165,7 +188,7 @@ namespace OpenIddict.Tests
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
}));

Loading…
Cancel
Save