Browse Source

Backport the redirect_uri/post_logout_redirect_uri storage changes to OpenIddict 1.x

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

15
samples/Mvc.Server/Startup.cs

@ -186,15 +186,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/",
RedirectUri = "http://localhost:53507/signin-oidc"
PostLogoutRedirectUris = { new Uri("http://localhost:53507/") },
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:
@ -208,14 +209,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; }
}
}

13
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,15 @@ 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.
/// </summary>
public string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the authorization.

5
src/OpenIddict.Core/Descriptors/OpenIddictTokenDescriptor.cs

@ -37,6 +37,11 @@ namespace OpenIddict.Core
/// </summary>
public string Hash { get; set; }
/// <summary>
/// Gets or sets the status associated with the token.
/// </summary>
public string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the token.
/// </summary>

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.");
}
}

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

@ -221,6 +221,7 @@ namespace OpenIddict.Core
var descriptor = new OpenIddictAuthorizationDescriptor
{
Status = await Store.GetStatusAsync(authorization, cancellationToken),
Subject = await Store.GetSubjectAsync(authorization, cancellationToken)
};
@ -242,11 +243,30 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(descriptor));
}
if (string.IsNullOrEmpty(descriptor.Status))
{
throw new ArgumentException("The status cannot be null or empty.");
}
if (string.IsNullOrEmpty(descriptor.Subject))
{
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.FromResult(0);
}
}

48
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -65,14 +65,15 @@ namespace OpenIddict.Core
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result returns the token.
/// </returns>
public virtual Task<TToken> CreateAsync([NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken)
public virtual async Task<TToken> CreateAsync([NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
return Store.CreateAsync(descriptor, cancellationToken);
await ValidateAsync(descriptor, cancellationToken);
return await Store.CreateAsync(descriptor, cancellationToken);
}
/// <summary>
@ -505,22 +506,53 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(token));
}
var type = await Store.GetTokenTypeAsync(token, cancellationToken);
if (string.IsNullOrEmpty(type))
var descriptor = new OpenIddictTokenDescriptor
{
throw new ArgumentException("The token type cannot be null or empty.", nameof(token));
Status = await Store.GetStatusAsync(token, cancellationToken),
Subject = await Store.GetSubjectAsync(token, cancellationToken),
Type = await Store.GetTokenTypeAsync(token, cancellationToken)
};
await ValidateAsync(descriptor, cancellationToken);
}
/// <summary>
/// Validates the token descriptor to ensure it's in a consistent state.
/// </summary>
/// <param name="descriptor">The token descriptor.</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>
protected virtual Task ValidateAsync([NotNull] OpenIddictTokenDescriptor descriptor, CancellationToken cancellationToken)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
if (string.IsNullOrEmpty(descriptor.Type))
{
throw new ArgumentException("The token type cannot be null or empty.", nameof(descriptor));
}
if (!string.Equals(type, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIdConnectConstants.TokenTypeHints.RefreshToken, StringComparison.OrdinalIgnoreCase))
if (!string.Equals(descriptor.Type, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(descriptor.Type, OpenIdConnectConstants.TokenTypeHints.RefreshToken, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("The specified token type is not supported by the default token manager.");
}
if (string.IsNullOrEmpty(await Store.GetSubjectAsync(token, cancellationToken)))
if (string.IsNullOrEmpty(descriptor.Status))
{
throw new ArgumentException("The status cannot be null or empty.");
}
if (string.IsNullOrEmpty(descriptor.Subject))
{
throw new ArgumentException("The subject cannot be null or empty.");
}
return Task.FromResult(0);
}
}
}

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.

231
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 new TApplication[0];
}
// 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() ?? new TApplication[0];
}
/// <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 new TApplication[0];
}
// 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() ?? new TApplication[0];
}
/// <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(new string[0]);
}
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(new string[0]);
}
return Task.FromResult(application.RedirectUris.Split(
OpenIdConnectConstants.Separators.Space,
StringSplitOptions.RemoveEmptyEntries));
}
/// <summary>
@ -356,6 +483,80 @@ namespace OpenIddict.Core
return Task.FromResult(0);
}
/// <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.FromResult(0);
}
/// <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.FromResult(0);
}
/// <summary>
/// Updates an existing application.
/// </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);
}

35
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);
}
@ -156,6 +168,25 @@ namespace OpenIddict.EntityFramework
catch (DbUpdateConcurrencyException) { }
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier 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,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
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));
}
/// <summary>
/// Executes the specified query.
/// </summary>

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

@ -125,16 +125,19 @@ 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))
{
var key = ConvertIdentifierFromString(descriptor.ApplicationId);
var application = await Applications.SingleOrDefaultAsync(entity => entity.Id.Equals(key));
var application = await Applications.FindAsync(cancellationToken, ConvertIdentifierFromString(descriptor.ApplicationId));
if (application == null)
{
throw new InvalidOperationException("The application associated with the authorization cannot be found.");
@ -146,6 +149,25 @@ namespace OpenIddict.EntityFramework
return await CreateAsync(authorization, cancellationToken);
}
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <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 identifier.
/// </returns>
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));
}
/// <summary>
/// Executes the specified query.
/// </summary>

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

@ -134,6 +134,7 @@ namespace OpenIddict.EntityFramework
CreationDate = descriptor.CreationDate,
ExpirationDate = descriptor.ExpirationDate,
Hash = descriptor.Hash,
Status = descriptor.Status,
Subject = descriptor.Subject,
Type = descriptor.Type
};
@ -141,9 +142,7 @@ namespace OpenIddict.EntityFramework
// Bind the token to the specified client application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId))
{
var key = ConvertIdentifierFromString(descriptor.ApplicationId);
var application = await Applications.SingleOrDefaultAsync(entity => entity.Id.Equals(key));
var application = await Applications.FindAsync(cancellationToken, ConvertIdentifierFromString(descriptor.ApplicationId));
if (application == null)
{
throw new InvalidOperationException("The application associated with the token cannot be found.");
@ -155,9 +154,7 @@ namespace OpenIddict.EntityFramework
// Bind the token to the specified authorization, if applicable.
if (!string.IsNullOrEmpty(descriptor.AuthorizationId))
{
var key = ConvertIdentifierFromString(descriptor.AuthorizationId);
var authorization = await Authorizations.SingleOrDefaultAsync(entity => entity.Id.Equals(key));
var authorization = await Authorizations.FindAsync(cancellationToken, ConvertIdentifierFromString(descriptor.AuthorizationId));
if (authorization == null)
{
throw new InvalidOperationException("The authorization associated with the token cannot be found.");
@ -192,6 +189,25 @@ namespace OpenIddict.EntityFramework
catch (DbUpdateConcurrencyException) { }
}
/// <summary>
/// Retrieves an token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</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 token corresponding to the unique identifier.
/// </returns>
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));
}
/// <summary>
/// Executes the specified query.
/// </summary>
@ -250,15 +266,13 @@ namespace OpenIddict.EntityFramework
if (!string.IsNullOrEmpty(identifier))
{
var key = ConvertIdentifierFromString(identifier);
var authorization = await Authorizations.SingleOrDefaultAsync(element => element.Id.Equals(key));
var authorization = await Authorizations.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
if (authorization == null)
{
throw new InvalidOperationException("The authorization associated with the token cannot be found.");
}
authorization.Tokens.Add(token);
token.Authorization = authorization;
}
else
@ -267,7 +281,7 @@ namespace OpenIddict.EntityFramework
// Try to retrieve the authorization associated with the token.
// If none can be found, assume that no authorization is attached.
var authorization = await Authorizations.SingleOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
var authorization = await Authorizations.FirstOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
if (authorization != null)
{
authorization.Tokens.Remove(token);
@ -293,15 +307,13 @@ namespace OpenIddict.EntityFramework
if (!string.IsNullOrEmpty(identifier))
{
var key = ConvertIdentifierFromString(identifier);
var application = await Applications.SingleOrDefaultAsync(element => element.Id.Equals(key));
var application = await Applications.FindAsync(cancellationToken, ConvertIdentifierFromString(identifier));
if (application == null)
{
throw new InvalidOperationException("The application associated with the token cannot be found.");
}
application.Tokens.Add(token);
token.Application = application;
}
else
@ -310,7 +322,7 @@ namespace OpenIddict.EntityFramework
// Try to retrieve the application associated with the token.
// If none can be found, assume that no application is attached.
var application = await Applications.SingleOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
var application = await Applications.FirstOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
if (application != null)
{
application.Tokens.Remove(token);

16
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);
}

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

@ -124,10 +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))
{

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

@ -133,6 +133,7 @@ namespace OpenIddict.EntityFrameworkCore
CreationDate = descriptor.CreationDate,
ExpirationDate = descriptor.ExpirationDate,
Hash = descriptor.Hash,
Status = descriptor.Status,
Subject = descriptor.Subject,
Type = descriptor.Type
};
@ -257,7 +258,7 @@ namespace OpenIddict.EntityFrameworkCore
throw new InvalidOperationException("The authorization associated with the token cannot be found.");
}
authorization.Tokens.Add(token);
token.Authorization = authorization;
}
else
@ -266,7 +267,7 @@ namespace OpenIddict.EntityFrameworkCore
// Try to retrieve the authorization associated with the token.
// If none can be found, assume that no authorization is attached.
var authorization = await Authorizations.SingleOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
var authorization = await Authorizations.FirstOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
if (authorization != null)
{
authorization.Tokens.Remove(token);
@ -300,7 +301,7 @@ namespace OpenIddict.EntityFrameworkCore
throw new InvalidOperationException("The application associated with the token cannot be found.");
}
application.Tokens.Add(token);
token.Application = application;
}
else
@ -309,7 +310,7 @@ namespace OpenIddict.EntityFrameworkCore
// Try to retrieve the application associated with the token.
// If none can be found, assume that no application is attached.
var application = await Applications.SingleOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
var application = await Applications.FirstOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key)));
if (application != null)
{
application.Tokens.Remove(token);

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.

4
src/OpenIddict.Models/OpenIddictAuthorization.cs

@ -48,12 +48,12 @@ 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.
/// </summary>
public virtual string Status { get; set; } = "valid";
public virtual string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the current authorization.

2
src/OpenIddict.Models/OpenIddictToken.cs

@ -78,7 +78,7 @@ namespace OpenIddict.Models
/// <summary>
/// Gets or sets the status of the current token.
/// </summary>
public virtual string Status { get; set; } = "valid";
public virtual string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the current token.

19
src/OpenIddict/OpenIddictProvider.Authentication.cs

@ -266,33 +266,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;

30
src/OpenIddict/OpenIddictProvider.Serialization.cs

@ -217,6 +217,7 @@ namespace OpenIddict
{
CreationDate = ticket.Properties.IssuedUtc,
ExpirationDate = ticket.Properties.ExpiresUtc,
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = type
};
@ -288,13 +289,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(),
Subject = descriptor.Subject
}, context.RequestAborted);
var authorization = await CreateAuthorizationAsync(descriptor, context, request);
if (authorization != null)
{
descriptor.AuthorizationId = await authorizations.GetIdAsync(authorization, context.RequestAborted);
@ -433,5 +428,26 @@ namespace OpenIddict
return ticket;
}
private Task<TAuthorization> CreateAuthorizationAsync(
[NotNull] OpenIddictTokenDescriptor token,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request)
{
var authorizations = context.RequestServices.GetRequiredService<OpenIddictAuthorizationManager<TAuthorization>>();
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

@ -88,17 +88,43 @@ namespace OpenIddict
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
// 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

@ -338,50 +338,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()
{
@ -393,9 +355,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);
});
@ -416,11 +375,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());
}
@ -440,9 +398,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);
@ -472,7 +427,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());
}
@ -492,9 +446,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);
@ -627,9 +575,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);
@ -671,9 +616,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

@ -238,9 +238,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);
@ -301,9 +298,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);
@ -375,9 +369,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);
@ -447,9 +438,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);
@ -508,9 +496,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);
@ -574,9 +559,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

@ -62,13 +62,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);
});
@ -86,10 +109,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]
@ -102,7 +125,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