diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs
index df5a8cb7..d6bad2d6 100644
--- a/samples/Mvc.Server/Startup.cs
+++ b/samples/Mvc.Server/Startup.cs
@@ -145,15 +145,16 @@ namespace Mvc.Server
if (await manager.FindByClientIdAsync("mvc", cancellationToken) == null)
{
- var application = new OpenIddictApplication
+ var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
+ ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
DisplayName = "MVC client application",
- LogoutRedirectUri = "http://localhost:53507/signout-callback-oidc",
- RedirectUri = "http://localhost:53507/signin-oidc"
+ PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
+ RedirectUris = { new Uri("http://localhost:53507/signin-oidc") }
};
- await manager.CreateAsync(application, "901564A5-E7FE-42CB-B10D-61EF6A8F3654", cancellationToken);
+ await manager.CreateAsync(descriptor, cancellationToken);
}
// To test this sample with Postman, use the following settings:
@@ -167,14 +168,14 @@ namespace Mvc.Server
// * Request access token locally: yes
if (await manager.FindByClientIdAsync("postman", cancellationToken) == null)
{
- var application = new OpenIddictApplication
+ var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "postman",
DisplayName = "Postman",
- RedirectUri = "https://www.getpostman.com/oauth2/callback"
+ RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") }
};
- await manager.CreateAsync(application, cancellationToken);
+ await manager.CreateAsync(descriptor, cancellationToken);
}
}
}
diff --git a/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs
index 5738113a..11cbcdae 100644
--- a/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs
+++ b/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs
@@ -1,4 +1,7 @@
-namespace OpenIddict.Core
+using System;
+using System.Collections.Generic;
+
+namespace OpenIddict.Core
{
///
/// Represents an OpenIddict application descriptor.
@@ -9,37 +12,37 @@
/// Gets or sets the client identifier
/// associated with the application.
///
- public virtual string ClientId { get; set; }
+ public string ClientId { get; set; }
///
/// 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.
///
- public virtual string ClientSecret { get; set; }
+ public string ClientSecret { get; set; }
///
/// Gets or sets the display name
/// associated with the application.
///
- public virtual string DisplayName { get; set; }
+ public string DisplayName { get; set; }
///
- /// Gets or sets the logout callback URL
+ /// Gets the logout callback URLs
/// associated with the application.
///
- public virtual string LogoutRedirectUri { get; set; }
+ public ISet PostLogoutRedirectUris { get; } = new HashSet();
///
- /// Gets or sets the callback URL
+ /// Gets the callback URLs
/// associated with the application.
///
- public virtual string RedirectUri { get; set; }
+ public ISet RedirectUris { get; } = new HashSet();
///
/// Gets or sets the application type
/// associated with the application.
///
- public virtual string Type { get; set; }
+ public string Type { get; set; }
}
}
diff --git a/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs b/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs
index a9338799..04b129ec 100644
--- a/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs
+++ b/src/OpenIddict.Core/Descriptors/OpenIddictAuthorizationDescriptor.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace OpenIddict.Core
{
@@ -13,9 +14,10 @@ namespace OpenIddict.Core
public string ApplicationId { get; set; }
///
- /// Gets or sets the scopes associated with the authorization.
+ /// Gets the scopes associated with the authorization.
///
- public IEnumerable Scopes { get; set; }
+ public ISet Scopes { get; } =
+ new HashSet(StringComparer.Ordinal);
///
/// Gets or sets the status associated with the authorization.
diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index 6a784249..8a510128 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -206,9 +206,14 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
///
- public virtual Task FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
+ public virtual Task 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);
}
///
@@ -220,8 +225,13 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
///
- public virtual Task FindByRedirectUriAsync(string address, CancellationToken cancellationToken)
+ public virtual Task 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);
}
+ ///
+ /// Retrieves the client identifier associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the client identifier associated with the application.
+ ///
+ public virtual Task GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return Store.GetClientIdAsync(application, cancellationToken);
+ }
+
///
/// Retrieves the client type associated with an application.
///
@@ -332,25 +361,6 @@ namespace OpenIddict.Core
return Store.GetTokensAsync(application, cancellationToken);
}
- ///
- /// Determines whether the specified application has a redirect_uri.
- ///
- /// The application.
- /// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns a boolean indicating whether a redirect_uri is registered.
- ///
- public virtual async Task HasRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken)
- {
- if (application == null)
- {
- throw new ArgumentNullException(nameof(application));
- }
-
- return !string.IsNullOrEmpty(await Store.GetRedirectUriAsync(application, cancellationToken));
- }
-
///
/// Determines whether an application is a confidential client.
///
@@ -502,7 +512,8 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the client secret was valid.
///
- public virtual async Task ValidateClientSecretAsync([NotNull] TApplication application, string secret, CancellationToken cancellationToken)
+ public virtual async Task 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 that can be used to monitor the asynchronous operation, whose result
/// returns a boolean indicating whether the post_logout_redirect_uri was valid.
///
- public virtual async Task ValidateLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
+ public virtual async Task 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
}
///
- /// Validates the redirect_uri associated with an application.
+ /// Validates the redirect_uri to ensure it's associated with an application.
///
/// The application.
- /// The address that should be compared to the redirect_uri stored in the database.
+ /// The address that should be compared to one of the redirect_uri stored in the database.
/// The that can be used to abort the operation.
///
/// A that can be used to monitor the asynchronous operation,
/// whose result returns a boolean indicating whether the redirect_uri was valid.
///
- public virtual async Task ValidateRedirectUriAsync([NotNull] TApplication application, string address, CancellationToken cancellationToken)
+ public virtual async Task 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.");
}
}
diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index 34f4c18f..284d7ba4 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
@@ -253,6 +253,20 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.");
}
+ // Ensure that the scopes are not null or empty and do not contain spaces.
+ foreach (var scope in descriptor.Scopes)
+ {
+ if (string.IsNullOrEmpty(scope))
+ {
+ throw new ArgumentException("Scopes cannot be null or empty.", nameof(descriptor));
+ }
+
+ if (scope.Contains(OpenIddictConstants.Separators.Space))
+ {
+ throw new ArgumentException("Scopes cannot contain spaces.", nameof(descriptor));
+ }
+ }
+
return Task.CompletedTask;
}
}
diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs
index 0b3a02e0..4ba722e7 100644
--- a/src/OpenIddict.Core/OpenIddictConstants.cs
+++ b/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";
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs
index 08965a07..76de9dbe 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs
+++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs
@@ -58,7 +58,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- Task FindByIdAsync(string identifier, CancellationToken cancellationToken);
+ Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
///
/// Retrieves an application using its client identifier.
@@ -69,7 +69,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- Task FindByClientIdAsync(string identifier, CancellationToken cancellationToken);
+ Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
///
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
@@ -80,7 +80,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
///
- Task FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken);
+ Task FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken);
///
/// Retrieves all the applications associated with the specified redirect_uri.
@@ -91,7 +91,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
///
- Task FindByRedirectUriAsync(string address, CancellationToken cancellationToken);
+ Task FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken);
///
/// Executes the specified query.
@@ -163,26 +163,26 @@ namespace OpenIddict.Core
Task GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken);
///
- /// Retrieves the logout callback address associated with an application.
+ /// Retrieves the logout callback addresses associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns the post_logout_redirect_uri associated with the application.
+ /// A that can be used to monitor the asynchronous operation, whose
+ /// result returns all the post_logout_redirect_uri associated with the application.
///
- Task GetLogoutRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken);
+ Task GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
///
- /// Retrieves the callback address associated with an application.
+ /// Retrieves the callback addresses associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A 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.
///
- Task GetRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken);
+ Task GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
///
/// Retrieves the token identifiers associated with an application.
@@ -231,6 +231,30 @@ namespace OpenIddict.Core
///
Task SetClientTypeAsync([NotNull] TApplication application, [NotNull] string type, CancellationToken cancellationToken);
+ ///
+ /// Sets the logout callback addresses associated with an application.
+ ///
+ /// The application.
+ /// The logout callback addresses associated with the application
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
+ [NotNull] string[] addresses, CancellationToken cancellationToken);
+
+ ///
+ /// Sets the callback addresses associated with an application.
+ ///
+ /// The application.
+ /// The callback addresses associated with the application
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task SetRedirectUrisAsync([NotNull] TApplication application,
+ [NotNull] string[] addresses, CancellationToken cancellationToken);
+
///
/// Updates an existing application.
///
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
index aaf3c41f..44b6b506 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
@@ -39,27 +39,27 @@ namespace OpenIddict.Core
Task CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken);
///
- /// Retrieves an authorization using its unique identifier.
+ /// Retrieves an authorization using its associated subject/client.
///
- /// The unique identifier associated with the authorization.
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
/// The that can be used to abort the operation.
///
/// A 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.
///
- Task FindByIdAsync(string identifier, CancellationToken cancellationToken);
+ Task FindAsync([NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken);
///
- /// Retrieves an authorization using its associated subject/client.
+ /// Retrieves an authorization using its unique identifier.
///
- /// The subject associated with the authorization.
- /// The client associated with the authorization.
+ /// The unique identifier associated with the authorization.
/// The that can be used to abort the operation.
///
/// A 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.
///
- Task FindAsync(string subject, string client, CancellationToken cancellationToken);
+ Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
///
/// Executes the specified query.
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
index 97512467..db25f7e5 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
@@ -55,7 +55,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
///
- Task FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
+ Task FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
///
/// Retrieves the list of tokens corresponding to the specified hash.
@@ -66,7 +66,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified hash.
///
- Task FindByHashAsync(string hash, CancellationToken cancellationToken);
+ Task FindByHashAsync([NotNull] string hash, CancellationToken cancellationToken);
///
/// Retrieves an token using its unique identifier.
@@ -77,7 +77,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
///
- Task FindByIdAsync(string identifier, CancellationToken cancellationToken);
+ Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
///
/// Retrieves the list of tokens corresponding to the specified subject.
@@ -88,7 +88,7 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
///
- Task FindBySubjectAsync(string subject, CancellationToken cancellationToken);
+ Task FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
///
/// Executes the specified query.
diff --git a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs
index 80dcaf0f..bec50e1f 100644
--- a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs
+++ b/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 that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public virtual Task 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 that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public virtual Task FindByClientIdAsync(string identifier, CancellationToken cancellationToken)
+ public virtual Task 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 that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
///
- public virtual Task FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
+ public virtual async Task 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();
+ }
+
+ // 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 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(capacity: 1);
+ }
+
+ results.Add(candidate);
+ }
+ }
+
+ return results?.ToArray() ?? Array.Empty();
}
///
@@ -112,9 +174,60 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
///
- public virtual Task FindByRedirectUriAsync(string address, CancellationToken cancellationToken)
+ public virtual async Task 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();
+ }
+
+ // 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 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(capacity: 1);
+ }
+
+ results.Add(candidate);
+ }
+ }
+
+ return results?.ToArray() ?? Array.Empty();
}
///
@@ -227,41 +340,55 @@ namespace OpenIddict.Core
}
///
- /// Retrieves the logout callback address associated with an application.
+ /// Retrieves the logout callback addresses associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation,
- /// whose result returns the post_logout_redirect_uri associated with the application.
+ /// A that can be used to monitor the asynchronous operation, whose
+ /// result returns all the post_logout_redirect_uri associated with the application.
///
- public virtual Task GetLogoutRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ public virtual Task GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
- return Task.FromResult(application.LogoutRedirectUri);
+ if (string.IsNullOrEmpty(application.PostLogoutRedirectUris))
+ {
+ return Task.FromResult(Array.Empty());
+ }
+
+ return Task.FromResult(application.PostLogoutRedirectUris.Split(
+ OpenIdConnectConstants.Separators.Space,
+ StringSplitOptions.RemoveEmptyEntries));
}
///
- /// Retrieves the callback address associated with an application.
+ /// Retrieves the callback addresses associated with an application.
///
/// The application.
/// The that can be used to abort the operation.
///
/// A 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.
///
- public virtual Task GetRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ public virtual Task GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
- return Task.FromResult(application.RedirectUri);
+ if (string.IsNullOrEmpty(application.RedirectUris))
+ {
+ return Task.FromResult(Array.Empty());
+ }
+
+ return Task.FromResult(application.RedirectUris.Split(
+ OpenIdConnectConstants.Separators.Space,
+ StringSplitOptions.RemoveEmptyEntries));
}
///
@@ -327,7 +454,7 @@ namespace OpenIddict.Core
application.ClientSecret = secret;
- return Task.FromResult(0);
+ return Task.CompletedTask;
}
///
@@ -353,7 +480,81 @@ namespace OpenIddict.Core
application.Type = type;
- return Task.FromResult(0);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Sets the logout callback addresses associated with an application.
+ ///
+ /// The application.
+ /// The logout callback addresses associated with the application
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ 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;
+ }
+
+ ///
+ /// Sets the callback addresses associated with an application.
+ ///
+ /// The application.
+ /// The callback addresses associated with the application
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ 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;
}
///
diff --git a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
index 4d158503..75a1bc80 100644
--- a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs
@@ -58,8 +58,18 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the subject/client.
///
- public virtual Task FindAsync(string subject, string client, CancellationToken cancellationToken)
+ public virtual Task 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 that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
///
- public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public virtual Task 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);
diff --git a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
index 12a2cda0..d98fafdf 100644
--- a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs
@@ -65,8 +65,13 @@ namespace OpenIddict.Core
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
///
- public virtual Task FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ public virtual Task 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 that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified hash.
///
- public virtual Task FindByHashAsync(string hash, CancellationToken cancellationToken)
+ public virtual Task 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 that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
///
- public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public virtual Task 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 that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
///
- public virtual Task FindBySubjectAsync(string subject, CancellationToken cancellationToken)
+ public virtual Task 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);
}
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
index 5330656f..52cc69b4 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
@@ -123,11 +123,23 @@ namespace OpenIddict.EntityFramework
ClientId = descriptor.ClientId,
ClientSecret = descriptor.ClientSecret,
DisplayName = descriptor.DisplayName,
- LogoutRedirectUri = descriptor.LogoutRedirectUri,
- RedirectUri = descriptor.RedirectUri,
Type = descriptor.Type
};
+ if (descriptor.PostLogoutRedirectUris.Count != 0)
+ {
+ application.PostLogoutRedirectUris = string.Join(
+ OpenIddictConstants.Separators.Space,
+ descriptor.PostLogoutRedirectUris.Select(uri => uri.OriginalString));
+ }
+
+ if (descriptor.RedirectUris.Count != 0)
+ {
+ application.RedirectUris = string.Join(
+ OpenIddictConstants.Separators.Space,
+ descriptor.RedirectUris.Select(uri => uri.OriginalString));
+ }
+
return CreateAsync(application, cancellationToken);
}
@@ -165,8 +177,13 @@ namespace OpenIddict.EntityFramework
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public override Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public override Task 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));
}
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
index deb68fbe..217bfeed 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
@@ -125,11 +125,15 @@ namespace OpenIddict.EntityFramework
var authorization = new TAuthorization
{
- Scope = string.Join(" ", descriptor.Scopes),
Status = descriptor.Status,
Subject = descriptor.Subject
};
+ if (descriptor.Scopes.Count != 0)
+ {
+ authorization.Scopes = string.Join(OpenIddictConstants.Separators.Space, descriptor.Scopes);
+ }
+
// Bind the authorization to the specified application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId))
{
@@ -154,8 +158,13 @@ namespace OpenIddict.EntityFramework
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
///
- public override Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public override Task 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));
}
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
index e3817327..1987e46e 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
@@ -198,8 +198,13 @@ namespace OpenIddict.EntityFramework
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
///
- public override Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public override Task 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));
}
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
index c33728a2..d2ebeaf3 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
@@ -122,11 +122,23 @@ namespace OpenIddict.EntityFrameworkCore
ClientId = descriptor.ClientId,
ClientSecret = descriptor.ClientSecret,
DisplayName = descriptor.DisplayName,
- LogoutRedirectUri = descriptor.LogoutRedirectUri,
- RedirectUri = descriptor.RedirectUri,
Type = descriptor.Type
};
+ if (descriptor.PostLogoutRedirectUris.Count != 0)
+ {
+ application.PostLogoutRedirectUris = string.Join(
+ OpenIddictConstants.Separators.Space,
+ descriptor.PostLogoutRedirectUris.Select(uri => uri.OriginalString));
+ }
+
+ if (descriptor.RedirectUris.Count != 0)
+ {
+ application.RedirectUris = string.Join(
+ OpenIddictConstants.Separators.Space,
+ descriptor.RedirectUris.Select(uri => uri.OriginalString));
+ }
+
return CreateAsync(application, cancellationToken);
}
@@ -164,8 +176,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
///
- public override Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public override Task 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);
}
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
index 6acea907..35351fa2 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
@@ -124,11 +124,15 @@ namespace OpenIddict.EntityFrameworkCore
var authorization = new TAuthorization
{
- Scope = string.Join(" ", descriptor.Scopes),
Status = descriptor.Status,
Subject = descriptor.Subject
};
+ if (descriptor.Scopes.Count != 0)
+ {
+ authorization.Scopes = string.Join(OpenIddictConstants.Separators.Space, descriptor.Scopes);
+ }
+
// Bind the authorization to the specified application, if applicable.
if (!string.IsNullOrEmpty(descriptor.ApplicationId))
{
@@ -153,8 +157,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
///
- public override Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public override Task 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);
}
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
index 34c593f4..3dc76866 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
@@ -197,8 +197,13 @@ namespace OpenIddict.EntityFrameworkCore
/// A that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
///
- public override Task FindByIdAsync(string identifier, CancellationToken cancellationToken)
+ public override Task 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);
}
diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs
index 120ac6ef..4ddea077 100644
--- a/src/OpenIddict.Models/OpenIddictApplication.cs
+++ b/src/OpenIddict.Models/OpenIddictApplication.cs
@@ -64,16 +64,18 @@ namespace OpenIddict.Models
public virtual TKey Id { get; set; }
///
- /// 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.
///
- public virtual string LogoutRedirectUri { get; set; }
+ public virtual string PostLogoutRedirectUris { get; set; }
///
- /// 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.
///
- public virtual string RedirectUri { get; set; }
+ public virtual string RedirectUris { get; set; }
///
/// Gets the list of the tokens associated with this application.
diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.Models/OpenIddictAuthorization.cs
index 2ff98b8e..5ec20cd3 100644
--- a/src/OpenIddict.Models/OpenIddictAuthorization.cs
+++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs
@@ -48,7 +48,7 @@ namespace OpenIddict.Models
/// Gets or sets the space-delimited scopes
/// associated with the current authorization.
///
- public virtual string Scope { get; set; }
+ public virtual string Scopes { get; set; }
///
/// Gets or sets the status of the current authorization.
diff --git a/src/OpenIddict/OpenIddictProvider.Authentication.cs b/src/OpenIddict/OpenIddictProvider.Authentication.cs
index 9a147728..1b4f6da9 100644
--- a/src/OpenIddict/OpenIddictProvider.Authentication.cs
+++ b/src/OpenIddict/OpenIddictProvider.Authentication.cs
@@ -260,33 +260,20 @@ namespace OpenIddict
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
- error: OpenIdConnectConstants.Errors.InvalidClient,
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Application not found in the database: ensure that your client_id is correct.");
return;
}
- // Ensure a redirect_uri was associated with the application.
- if (!await Applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted))
- {
- Logger.LogError("The authorization request was rejected because no redirect_uri " +
- "was registered with the application '{ClientId}'.", context.ClientId);
-
- context.Reject(
- error: OpenIdConnectConstants.Errors.UnauthorizedClient,
- description: "The client application is not allowed to use interactive flows.");
-
- return;
- }
-
- // Ensure the redirect_uri is valid.
+ // Ensure that the specified redirect_uri is valid and is associated with the client application.
if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted))
{
Logger.LogError("The authorization request was rejected because the redirect_uri " +
"was invalid: '{RedirectUri}'.", context.RedirectUri);
context.Reject(
- error: OpenIdConnectConstants.Errors.InvalidClient,
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid redirect_uri.");
return;
diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs
index 60e7ba89..da335e25 100644
--- a/src/OpenIddict/OpenIddictProvider.Serialization.cs
+++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs
@@ -256,14 +256,7 @@ namespace OpenIddict
{
Debug.Assert(!string.IsNullOrEmpty(descriptor.ApplicationId), "The client identifier shouldn't be null.");
- var authorization = await Authorizations.CreateAsync(new OpenIddictAuthorizationDescriptor
- {
- ApplicationId = descriptor.ApplicationId,
- Scopes = request.GetScopes(),
- Status = OpenIddictConstants.Statuses.Valid,
- Subject = descriptor.Subject
- }, context.RequestAborted);
-
+ var authorization = await CreateAuthorizationAsync(descriptor, context, request);
if (authorization != null)
{
descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted);
@@ -399,5 +392,24 @@ namespace OpenIddict
return ticket;
}
+
+ private Task 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);
+ }
}
}
\ No newline at end of file
diff --git a/src/OpenIddict/OpenIddictProvider.Session.cs b/src/OpenIddict/OpenIddictProvider.Session.cs
index 66066bdb..b920828d 100644
--- a/src/OpenIddict/OpenIddictProvider.Session.cs
+++ b/src/OpenIddict/OpenIddictProvider.Session.cs
@@ -82,17 +82,43 @@ namespace OpenIddict
public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context)
{
// If an optional post_logout_redirect_uri was provided, validate it.
- if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri) &&
- !await Applications.ValidateLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted))
+ if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
- Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " +
- "was invalid: '{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri);
+ if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
+ {
+ Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri was not " +
+ "a valid absolute URL: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
- context.Reject(
- error: OpenIdConnectConstants.Errors.InvalidClient,
- description: "Invalid post_logout_redirect_uri.");
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.");
- return;
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(uri.Fragment))
+ {
+ Logger.LogError("The logout request was rejected because the 'post_logout_redirect_uri' contained " +
+ "a URL fragment: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
+
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "The 'post_logout_redirect_uri' parameter must not include a fragment.");
+
+ return;
+ }
+
+ if (!await Applications.ValidatePostLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted))
+ {
+ Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " +
+ "was unknown: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
+
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "Invalid post_logout_redirect_uri.");
+
+ return;
+ }
}
context.Validate();
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
index 5657820c..21d2b4b0 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
@@ -337,50 +337,12 @@ namespace OpenIddict.Tests
});
// Assert
- Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error);
+ Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Application not found in the database: ensure that your client_id is correct.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), 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()))
- .ReturnsAsync(application);
-
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .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()), Times.Once());
- Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny()), Times.Once());
- }
-
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid()
{
@@ -392,9 +354,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(false);
});
@@ -415,11 +374,10 @@ namespace OpenIddict.Tests
});
// Assert
- Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error);
+ Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Invalid redirect_uri.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once());
- Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny()), Times.Once());
Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once());
}
@@ -439,9 +397,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -471,7 +426,6 @@ namespace OpenIddict.Tests
Assert.Equal("Confidential clients are not allowed to retrieve a token from the authorization endpoint.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once());
- Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny()), Times.Once());
Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once());
}
@@ -491,9 +445,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -549,9 +500,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -628,9 +576,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -672,9 +617,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
index 7396e477..0179b014 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
@@ -237,9 +237,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -300,9 +297,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -374,9 +368,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -446,9 +437,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -507,9 +495,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
@@ -573,9 +558,6 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
.ReturnsAsync(application);
- instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
- .ReturnsAsync(true);
-
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(true);
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs
index da087aa0..11995df2 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs
@@ -61,13 +61,36 @@ namespace OpenIddict.Tests
Assert.Equal("Invalid request: timeout expired.", response.ErrorDescription);
}
+ [Theory]
+ [InlineData("/path", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")]
+ [InlineData("/tmp/file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")]
+ [InlineData("C:\\tmp\\file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")]
+ [InlineData("http://www.fabrikam.com/path#param=value", "The 'post_logout_redirect_uri' parameter must not include a fragment.")]
+ public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid(string address, string message)
+ {
+ // Arrange
+ var server = CreateAuthorizationServer();
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act
+ var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest
+ {
+ PostLogoutRedirectUri = address
+ });
+
+ // Assert
+ Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
+ Assert.Equal(message, response.ErrorDescription);
+ }
+
[Fact]
- public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid()
+ public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsUnknown()
{
// Arrange
var manager = CreateApplicationManager(instance =>
{
- instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()))
+ instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(false);
});
@@ -85,10 +108,10 @@ namespace OpenIddict.Tests
});
// Assert
- Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error);
+ Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("Invalid post_logout_redirect_uri.", response.ErrorDescription);
- Mock.Get(manager).Verify(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once());
}
[Fact]
@@ -101,7 +124,7 @@ namespace OpenIddict.Tests
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
- instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()))
+ instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()))
.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()))
+ instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()))
.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()))
+ instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()))
.ReturnsAsync(false);
}));