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) if (await manager.FindByClientIdAsync("mvc", cancellationToken) == null)
{ {
var application = new OpenIddictApplication var descriptor = new OpenIddictApplicationDescriptor
{ {
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
DisplayName = "MVC client application", DisplayName = "MVC client application",
LogoutRedirectUri = "http://localhost:53507/signout-callback-oidc", PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUri = "http://localhost:53507/signin-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: // To test this sample with Postman, use the following settings:
@ -167,14 +168,14 @@ namespace Mvc.Server
// * Request access token locally: yes // * Request access token locally: yes
if (await manager.FindByClientIdAsync("postman", cancellationToken) == null) if (await manager.FindByClientIdAsync("postman", cancellationToken) == null)
{ {
var application = new OpenIddictApplication var descriptor = new OpenIddictApplicationDescriptor
{ {
ClientId = "postman", ClientId = "postman",
DisplayName = "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> /// <summary>
/// Represents an OpenIddict application descriptor. /// Represents an OpenIddict application descriptor.
@ -9,37 +12,37 @@
/// Gets or sets the client identifier /// Gets or sets the client identifier
/// associated with the application. /// associated with the application.
/// </summary> /// </summary>
public virtual string ClientId { get; set; } public string ClientId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the client secret associated with the application. /// Gets or sets the client secret associated with the application.
/// Note: depending on the application manager used when creating it, /// Note: depending on the application manager used when creating it,
/// this property may be hashed or encrypted for security reasons. /// this property may be hashed or encrypted for security reasons.
/// </summary> /// </summary>
public virtual string ClientSecret { get; set; } public string ClientSecret { get; set; }
/// <summary> /// <summary>
/// Gets or sets the display name /// Gets or sets the display name
/// associated with the application. /// associated with the application.
/// </summary> /// </summary>
public virtual string DisplayName { get; set; } public string DisplayName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the logout callback URL /// Gets the logout callback URLs
/// associated with the application. /// associated with the application.
/// </summary> /// </summary>
public virtual string LogoutRedirectUri { get; set; } public ISet<Uri> PostLogoutRedirectUris { get; } = new HashSet<Uri>();
/// <summary> /// <summary>
/// Gets or sets the callback URL /// Gets the callback URLs
/// associated with the application. /// associated with the application.
/// </summary> /// </summary>
public virtual string RedirectUri { get; set; } public ISet<Uri> RedirectUris { get; } = new HashSet<Uri>();
/// <summary> /// <summary>
/// Gets or sets the application type /// Gets or sets the application type
/// associated with the application. /// associated with the application.
/// </summary> /// </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 namespace OpenIddict.Core
{ {
@ -13,9 +14,10 @@ namespace OpenIddict.Core
public string ApplicationId { get; set; } public string ApplicationId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the scopes associated with the authorization. /// Gets the scopes associated with the authorization.
/// </summary> /// </summary>
public IEnumerable<string> Scopes { get; set; } public ISet<string> Scopes { get; } =
new HashSet<string>(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets or sets the status associated with the authorization. /// 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 /// 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 the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </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> /// <summary>
@ -220,8 +225,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// 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 the client applications corresponding to the specified redirect_uri.
/// </returns> /// </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); return Store.FindByRedirectUriAsync(address, cancellationToken);
} }
@ -245,6 +255,25 @@ namespace OpenIddict.Core
return Store.GetAsync(query, cancellationToken); 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> /// <summary>
/// Retrieves the client type associated with an application. /// Retrieves the client type associated with an application.
/// </summary> /// </summary>
@ -332,25 +361,6 @@ namespace OpenIddict.Core
return Store.GetTokensAsync(application, cancellationToken); 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> /// <summary>
/// Determines whether an application is a confidential client. /// Determines whether an application is a confidential client.
/// </summary> /// </summary>
@ -502,7 +512,8 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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. /// whose result returns a boolean indicating whether the client secret was valid.
/// </returns> /// </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) if (application == null)
{ {
@ -528,7 +539,7 @@ namespace OpenIddict.Core
if (!await ValidateClientSecretAsync(secret, value, cancellationToken)) if (!await ValidateClientSecretAsync(secret, value, cancellationToken))
{ {
Logger.LogWarning("Client authentication failed for {Client}.", Logger.LogWarning("Client authentication failed for {Client}.",
await GetDisplayNameAsync(application, cancellationToken)); await GetClientIdAsync(application, cancellationToken));
return false; return false;
} }
@ -545,16 +556,25 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// 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 a boolean indicating whether the post_logout_redirect_uri was valid.
/// </returns> /// </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. // 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. // 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". foreach (var uri in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
if (string.Equals(address, await Store.GetLogoutRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal))
{ {
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> /// <summary>
/// Validates the redirect_uri associated with an application. /// Validates the redirect_uri to ensure it's associated with an application.
/// </summary> /// </summary>
/// <param name="application">The application.</param> /// <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> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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. /// whose result returns a boolean indicating whether the redirect_uri was valid.
/// </returns> /// </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) if (application == null)
{ {
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
// Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison". if (string.IsNullOrEmpty(address))
// 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))
{ {
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 " + 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; return false;
} }
@ -609,11 +638,43 @@ namespace OpenIddict.Core
ClientId = await Store.GetClientIdAsync(application, cancellationToken), ClientId = await Store.GetClientIdAsync(application, cancellationToken),
ClientSecret = await Store.GetClientSecretAsync(application, cancellationToken), ClientSecret = await Store.GetClientSecretAsync(application, cancellationToken),
DisplayName = await Store.GetDisplayNameAsync(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) 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); 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)); 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. // 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. // Ensure the address is not null.
if (!Uri.TryCreate(descriptor.RedirectUri, UriKind.Absolute, out Uri uri)) if (uri == null)
{
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))
{ {
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. // Ensure the address is a valid and absolute URL.
if (!string.IsNullOrEmpty(descriptor.LogoutRedirectUri)) if (!uri.IsAbsoluteUri || !uri.IsWellFormedOriginalString())
{
// Ensure the post_logout_redirect_uri is a valid and absolute URL.
if (!Uri.TryCreate(descriptor.LogoutRedirectUri, UriKind.Absolute, out Uri uri))
{ {
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)) 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."); 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; return Task.CompletedTask;
} }
} }

5
src/OpenIddict.Core/OpenIddictConstants.cs

@ -37,6 +37,11 @@ namespace OpenIddict.Core
public const string TokenId = ".token_id"; public const string TokenId = ".token_id";
} }
public static class Separators
{
public const string Space = " ";
}
public static class Scopes public static class Scopes
{ {
public const string Roles = "roles"; 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </returns>
Task<TApplication> FindByIdAsync(string identifier, CancellationToken cancellationToken); Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves an application using its client identifier. /// 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </returns>
Task<TApplication> FindByClientIdAsync(string identifier, CancellationToken cancellationToken); Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri. /// 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 /// 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 the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </returns>
Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken); Task<TApplication[]> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves all the applications associated with the specified redirect_uri. /// 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 /// 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 the client applications corresponding to the specified redirect_uri.
/// </returns> /// </returns>
Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken); Task<TApplication[]> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Executes the specified query. /// Executes the specified query.
@ -163,26 +163,26 @@ namespace OpenIddict.Core
Task<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken); Task<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves the logout callback address associated with an application. /// Retrieves the logout callback addresses associated with an application.
/// </summary> /// </summary>
/// <param name="application">The application.</param> /// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose
/// whose result returns the post_logout_redirect_uri associated with the application. /// result returns all the post_logout_redirect_uri associated with the application.
/// </returns> /// </returns>
Task<string> GetLogoutRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken); Task<string[]> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves the callback address associated with an application. /// Retrieves the callback addresses associated with an application.
/// </summary> /// </summary>
/// <param name="application">The application.</param> /// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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> /// </returns>
Task<string> GetRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken); Task<string[]> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves the token identifiers associated with an application. /// Retrieves the token identifiers associated with an application.
@ -231,6 +231,30 @@ namespace OpenIddict.Core
/// </returns> /// </returns>
Task SetClientTypeAsync([NotNull] TApplication application, [NotNull] string type, CancellationToken cancellationToken); 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> /// <summary>
/// Updates an existing application. /// Updates an existing application.
/// </summary> /// </summary>

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

@ -39,27 +39,27 @@ namespace OpenIddict.Core
Task<TAuthorization> CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken); Task<TAuthorization> CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves an authorization using its unique identifier. /// Retrieves an authorization using its associated subject/client.
/// </summary> /// </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> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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> /// </returns>
Task<TAuthorization> FindByIdAsync(string identifier, CancellationToken cancellationToken); Task<TAuthorization> FindAsync([NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves an authorization using its associated subject/client. /// Retrieves an authorization using its unique identifier.
/// </summary> /// </summary>
/// <param name="subject">The subject associated with the authorization.</param> /// <param name="identifier">The unique identifier 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> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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> /// </returns>
Task<TAuthorization> FindAsync(string subject, string client, CancellationToken cancellationToken); Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Executes the specified query. /// 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization. /// whose result returns the tokens corresponding to the specified authorization.
/// </returns> /// </returns>
Task<TToken[]> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken); Task<TToken[]> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves the list of tokens corresponding to the specified hash. /// 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified hash. /// whose result returns the tokens corresponding to the specified hash.
/// </returns> /// </returns>
Task<TToken> FindByHashAsync(string hash, CancellationToken cancellationToken); Task<TToken> FindByHashAsync([NotNull] string hash, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves an token using its unique identifier. /// 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier. /// whose result returns the token corresponding to the unique identifier.
/// </returns> /// </returns>
Task<TToken> FindByIdAsync(string identifier, CancellationToken cancellationToken); Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Retrieves the list of tokens corresponding to the specified subject. /// 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject. /// whose result returns the tokens corresponding to the specified subject.
/// </returns> /// </returns>
Task<TToken[]> FindBySubjectAsync(string subject, CancellationToken cancellationToken); Task<TToken[]> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Executes the specified query. /// Executes the specified query.

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

@ -10,6 +10,7 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using JetBrains.Annotations; using JetBrains.Annotations;
using OpenIddict.Models; using OpenIddict.Models;
@ -68,8 +69,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </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); var key = ConvertIdentifierFromString(identifier);
return GetAsync(applications => applications.Where(application => application.Id.Equals(key)), cancellationToken); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </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); 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 /// 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 the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </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> /// <summary>
@ -112,9 +174,60 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// 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 the client applications corresponding to the specified redirect_uri.
/// </returns> /// </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> /// <summary>
@ -227,41 +340,55 @@ namespace OpenIddict.Core
} }
/// <summary> /// <summary>
/// Retrieves the logout callback address associated with an application. /// Retrieves the logout callback addresses associated with an application.
/// </summary> /// </summary>
/// <param name="application">The application.</param> /// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose
/// whose result returns the post_logout_redirect_uri associated with the application. /// result returns all the post_logout_redirect_uri associated with the application.
/// </returns> /// </returns>
public virtual Task<string> GetLogoutRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual Task<string[]> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
throw new ArgumentNullException(nameof(application)); 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> /// <summary>
/// Retrieves the callback address associated with an application. /// Retrieves the callback addresses associated with an application.
/// </summary> /// </summary>
/// <param name="application">The application.</param> /// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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> /// </returns>
public virtual Task<string> GetRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual Task<string[]> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
throw new ArgumentNullException(nameof(application)); 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> /// <summary>
@ -327,7 +454,7 @@ namespace OpenIddict.Core
application.ClientSecret = secret; application.ClientSecret = secret;
return Task.FromResult(0); return Task.CompletedTask;
} }
/// <summary> /// <summary>
@ -353,7 +480,81 @@ namespace OpenIddict.Core
application.Type = type; 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> /// <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, /// 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 subject/client.
/// </returns> /// </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); var key = ConvertIdentifierFromString(client);
return GetAsync(authorizations => return GetAsync(authorizations =>
@ -78,8 +88,13 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// 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 identifier.
/// </returns> /// </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); var key = ConvertIdentifierFromString(identifier);
return GetAsync(authorizations => authorizations.Where(authorization => authorization.Id.Equals(key)), cancellationToken); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization. /// whose result returns the tokens corresponding to the specified authorization.
/// </returns> /// </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); var key = ConvertIdentifierFromString(identifier);
return ListAsync(tokens => tokens.Where(token => token.Authorization.Id.Equals(key)), cancellationToken); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified hash. /// whose result returns the tokens corresponding to the specified hash.
/// </returns> /// </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); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier. /// whose result returns the token corresponding to the unique identifier.
/// </returns> /// </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); var key = ConvertIdentifierFromString(identifier);
return GetAsync(tokens => tokens.Where(token => token.Id.Equals(key)), cancellationToken); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject. /// whose result returns the tokens corresponding to the specified subject.
/// </returns> /// </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); 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, ClientId = descriptor.ClientId,
ClientSecret = descriptor.ClientSecret, ClientSecret = descriptor.ClientSecret,
DisplayName = descriptor.DisplayName, DisplayName = descriptor.DisplayName,
LogoutRedirectUri = descriptor.LogoutRedirectUri,
RedirectUri = descriptor.RedirectUri,
Type = descriptor.Type 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); return CreateAsync(application, cancellationToken);
} }
@ -165,8 +177,13 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </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)); return Applications.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
} }

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

@ -125,11 +125,15 @@ namespace OpenIddict.EntityFramework
var authorization = new TAuthorization var authorization = new TAuthorization
{ {
Scope = string.Join(" ", descriptor.Scopes),
Status = descriptor.Status, Status = descriptor.Status,
Subject = descriptor.Subject 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. // Bind the authorization to the specified application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId)) 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, /// 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 identifier.
/// </returns> /// </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)); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier. /// whose result returns the token corresponding to the unique identifier.
/// </returns> /// </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)); return Tokens.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
} }

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

@ -122,11 +122,23 @@ namespace OpenIddict.EntityFrameworkCore
ClientId = descriptor.ClientId, ClientId = descriptor.ClientId,
ClientSecret = descriptor.ClientSecret, ClientSecret = descriptor.ClientSecret,
DisplayName = descriptor.DisplayName, DisplayName = descriptor.DisplayName,
LogoutRedirectUri = descriptor.LogoutRedirectUri,
RedirectUri = descriptor.RedirectUri,
Type = descriptor.Type 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); return CreateAsync(application, cancellationToken);
} }
@ -164,8 +176,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </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); 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 var authorization = new TAuthorization
{ {
Scope = string.Join(" ", descriptor.Scopes),
Status = descriptor.Status, Status = descriptor.Status,
Subject = descriptor.Subject 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. // Bind the authorization to the specified application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId)) 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, /// 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 identifier.
/// </returns> /// </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); 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, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier. /// whose result returns the token corresponding to the unique identifier.
/// </returns> /// </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); 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; } public virtual TKey Id { get; set; }
/// <summary> /// <summary>
/// Gets or sets the logout callback URL /// Gets or sets the logout callback URLs
/// associated with the current application. /// associated with the current application,
/// stored as a unique space-separated string.
/// </summary> /// </summary>
public virtual string LogoutRedirectUri { get; set; } public virtual string PostLogoutRedirectUris { get; set; }
/// <summary> /// <summary>
/// Gets or sets the callback URL /// Gets or sets the callback URLs
/// associated with the current application. /// associated with the current application,
/// stored as a unique space-separated string.
/// </summary> /// </summary>
public virtual string RedirectUri { get; set; } public virtual string RedirectUris { get; set; }
/// <summary> /// <summary>
/// Gets the list of the tokens associated with this application. /// 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 /// Gets or sets the space-delimited scopes
/// associated with the current authorization. /// associated with the current authorization.
/// </summary> /// </summary>
public virtual string Scope { get; set; } public virtual string Scopes { get; set; }
/// <summary> /// <summary>
/// Gets or sets the status of the current authorization. /// 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); "application was not found: '{ClientId}'.", context.ClientId);
context.Reject( context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient, error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Application not found in the database: ensure that your client_id is correct."); description: "Application not found in the database: ensure that your client_id is correct.");
return; return;
} }
// Ensure a redirect_uri was associated with the application. // Ensure that the specified redirect_uri is valid and is associated with the client 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.
if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted)) if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted))
{ {
Logger.LogError("The authorization request was rejected because the redirect_uri " + Logger.LogError("The authorization request was rejected because the redirect_uri " +
"was invalid: '{RedirectUri}'.", context.RedirectUri); "was invalid: '{RedirectUri}'.", context.RedirectUri);
context.Reject( context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient, error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid redirect_uri."); description: "Invalid redirect_uri.");
return; 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."); Debug.Assert(!string.IsNullOrEmpty(descriptor.ApplicationId), "The client identifier shouldn't be null.");
var authorization = await Authorizations.CreateAsync(new OpenIddictAuthorizationDescriptor var authorization = await CreateAuthorizationAsync(descriptor, context, request);
{
ApplicationId = descriptor.ApplicationId,
Scopes = request.GetScopes(),
Status = OpenIddictConstants.Statuses.Valid,
Subject = descriptor.Subject
}, context.RequestAborted);
if (authorization != null) if (authorization != null)
{ {
descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted); descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted);
@ -399,5 +392,24 @@ namespace OpenIddict
return ticket; 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) public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context)
{ {
// If an optional post_logout_redirect_uri was provided, validate it. // If an optional post_logout_redirect_uri was provided, validate it.
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri) && if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri))
!await Applications.ValidateLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted))
{ {
Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " + if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
"was invalid: '{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri); {
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( context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient, error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid post_logout_redirect_uri."); 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(); context.Validate();

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

@ -337,50 +337,12 @@ namespace OpenIddict.Tests
}); });
// Assert // 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); 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()); 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] [Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid() public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid()
{ {
@ -392,9 +354,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(false); .ReturnsAsync(false);
}); });
@ -415,11 +374,10 @@ namespace OpenIddict.Tests
}); });
// Assert // Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Invalid redirect_uri.", response.ErrorDescription); 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.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.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>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .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); 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.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.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()); 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>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -549,9 +500,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -628,9 +576,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -672,9 +617,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .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>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -300,9 +297,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -374,9 +368,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -446,9 +437,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -507,9 +495,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
@ -573,9 +558,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .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>())) instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);

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

@ -61,13 +61,36 @@ namespace OpenIddict.Tests
Assert.Equal("Invalid request: timeout expired.", response.ErrorDescription); 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] [Fact]
public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid() public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsUnknown()
{ {
// Arrange // Arrange
var manager = CreateApplicationManager(instance => 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); .ReturnsAsync(false);
}); });
@ -85,10 +108,10 @@ namespace OpenIddict.Tests
}); });
// Assert // Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Invalid post_logout_redirect_uri.", response.ErrorDescription); 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] [Fact]
@ -101,7 +124,7 @@ namespace OpenIddict.Tests
{ {
builder.Services.AddSingleton(CreateApplicationManager(instance => 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); .ReturnsAsync(true);
})); }));
@ -139,7 +162,7 @@ namespace OpenIddict.Tests
{ {
builder.Services.AddSingleton(CreateApplicationManager(instance => 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); .ReturnsAsync(true);
})); }));
}); });
@ -165,7 +188,7 @@ namespace OpenIddict.Tests
{ {
builder.Services.AddSingleton(CreateApplicationManager(instance => 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); .ReturnsAsync(false);
})); }));

Loading…
Cancel
Save