From 8488dd4f81c5b37702a263648e56100e90c3d45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 1 Oct 2017 10:28:58 +0200 Subject: [PATCH] Introduce the ability to specify multiple callback URLs (redirect_uri and post_logout_redirect_uri) --- samples/Mvc.Server/Startup.cs | 15 +- .../OpenIddictApplicationDescriptor.cs | 21 +- .../OpenIddictAuthorizationDescriptor.cs | 8 +- .../Managers/OpenIddictApplicationManager.cs | 169 ++++++++----- .../OpenIddictAuthorizationManager.cs | 14 ++ src/OpenIddict.Core/OpenIddictConstants.cs | 5 + .../Stores/IOpenIddictApplicationStore.cs | 46 +++- .../Stores/IOpenIddictAuthorizationStore.cs | 18 +- .../Stores/IOpenIddictTokenStore.cs | 8 +- .../Stores/OpenIddictApplicationStore.cs | 235 ++++++++++++++++-- .../Stores/OpenIddictAuthorizationStore.cs | 19 +- .../Stores/OpenIddictTokenStore.cs | 28 ++- .../Stores/OpenIddictApplicationStore.cs | 23 +- .../Stores/OpenIddictAuthorizationStore.cs | 13 +- .../Stores/OpenIddictTokenStore.cs | 7 +- .../Stores/OpenIddictApplicationStore.cs | 23 +- .../Stores/OpenIddictAuthorizationStore.cs | 13 +- .../Stores/OpenIddictTokenStore.cs | 7 +- .../OpenIddictApplication.cs | 14 +- .../OpenIddictAuthorization.cs | 2 +- .../OpenIddictProvider.Authentication.cs | 19 +- .../OpenIddictProvider.Serialization.cs | 28 ++- src/OpenIddict/OpenIddictProvider.Session.cs | 42 +++- .../OpenIddictProviderTests.Authentication.cs | 62 +---- .../OpenIddictProviderTests.Serialization.cs | 18 -- .../OpenIddictProviderTests.Session.cs | 37 ++- 26 files changed, 633 insertions(+), 261 deletions(-) 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); }));