From d8e3986a3dfaf04fc0914b5674e3db4f395950fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 17 Sep 2017 19:21:25 +0200 Subject: [PATCH] Introduce OpenIddictApplicationDescriptor and add a CreateAsync() overload accepting a OpenIddictApplicationDescriptor parameter --- .../OpenIddictApplicationDescriptor.cs | 45 ++++++ .../Managers/OpenIddictApplicationManager.cs | 131 +++++++++++++----- .../OpenIddictAuthorizationManager.cs | 44 ++++-- .../Stores/IOpenIddictApplicationStore.cs | 10 ++ .../Stores/OpenIddictApplicationStore.cs | 28 ++++ 5 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs diff --git a/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs new file mode 100644 index 00000000..5738113a --- /dev/null +++ b/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs @@ -0,0 +1,45 @@ +namespace OpenIddict.Core +{ + /// + /// Represents an OpenIddict application descriptor. + /// + public class OpenIddictApplicationDescriptor + { + /// + /// Gets or sets the client identifier + /// associated with the application. + /// + public virtual 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; } + + /// + /// Gets or sets the display name + /// associated with the application. + /// + public virtual string DisplayName { get; set; } + + /// + /// Gets or sets the logout callback URL + /// associated with the application. + /// + public virtual string LogoutRedirectUri { get; set; } + + /// + /// Gets or sets the callback URL + /// associated with the application. + /// + public virtual string RedirectUri { get; set; } + + /// + /// Gets or sets the application type + /// associated with the application. + /// + public virtual string Type { get; set; } + } +} diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 25b4fd06..6bc8b742 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -94,29 +94,69 @@ namespace OpenIddict.Core OpenIddictConstants.ClientTypes.Confidential, cancellationToken); } - if (string.IsNullOrEmpty(secret)) + // If the client is a confidential application, throw an + // exception as the client secret is required in this case. + if (string.IsNullOrEmpty(secret) && + string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) { - // If the client is a confidential application, throw an - // exception as the client secret is required in this case. - if (string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("A client secret must be provided when creating a confidential application."); - } - - await Store.SetClientSecretAsync(application, null, cancellationToken); + throw new InvalidOperationException("A client secret must be provided when creating a confidential application."); } - else + if (!string.IsNullOrEmpty(secret)) { secret = await ObfuscateClientSecretAsync(secret, cancellationToken); await Store.SetClientSecretAsync(application, secret, cancellationToken); } await ValidateAsync(application, cancellationToken); - return await Store.CreateAsync(application, cancellationToken); } + /// + /// Creates a new application. + /// Note: the default implementation automatically hashes the client + /// secret before storing it in the database, for security reasons. + /// + /// The application descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the application. + /// + public virtual async Task CreateAsync([NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + // If no client type was specified, assume it's a + // public application if no secret was provided. + if (string.IsNullOrEmpty(descriptor.Type)) + { + descriptor.Type = string.IsNullOrEmpty(descriptor.ClientSecret) ? + OpenIddictConstants.ClientTypes.Public : + OpenIddictConstants.ClientTypes.Confidential; + } + + // If the client is a confidential application, throw an + // exception as the client secret is required in this case. + if (string.IsNullOrEmpty(descriptor.ClientSecret) && + string.Equals(descriptor.Type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("A client secret must be provided when creating a confidential application."); + } + + // Obfuscate the provided client secret. + if (!string.IsNullOrEmpty(descriptor.ClientSecret)) + { + descriptor.ClientSecret = await ObfuscateClientSecretAsync(descriptor.ClientSecret, cancellationToken); + } + + await ValidateAsync(descriptor, cancellationToken); + return await Store.CreateAsync(descriptor, cancellationToken); + } + /// /// Removes an existing application. /// @@ -409,6 +449,7 @@ namespace OpenIddict.Core await Store.SetClientSecretAsync(application, secret, cancellationToken); } + await ValidateAsync(application, cancellationToken); await UpdateAsync(application, cancellationToken); } @@ -525,53 +566,72 @@ namespace OpenIddict.Core /// protected virtual async Task ValidateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { - if (application == null) + var descriptor = new OpenIddictApplicationDescriptor { - throw new ArgumentNullException(nameof(application)); + 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) + }; + + await ValidateAsync(descriptor, cancellationToken); + } + + /// + /// Validates the application descriptor to ensure it's in a consistent state. + /// + /// The application descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + protected virtual Task ValidateAsync([NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); } - if (string.IsNullOrEmpty(await Store.GetClientIdAsync(application, cancellationToken))) + if (string.IsNullOrEmpty(descriptor.ClientId)) { - throw new ArgumentException("The client identifier cannot be null or empty.", nameof(application)); + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(descriptor)); } - var type = await Store.GetClientTypeAsync(application, cancellationToken); - if (string.IsNullOrEmpty(type)) + if (string.IsNullOrEmpty(descriptor.Type)) { - throw new ArgumentException("The client type cannot be null or empty.", nameof(application)); + throw new ArgumentException("The client type cannot be null or empty.", nameof(descriptor)); } // Ensure the application type is supported by the manager. - if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && - !string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(descriptor.Type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && + !string.Equals(descriptor.Type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Only 'confidential' or 'public' applications are " + - "supported by the default application manager.", nameof(application)); + "supported by the default application manager.", nameof(descriptor)); } - var secret = await Store.GetClientSecretAsync(application, cancellationToken); - // Ensure a client secret was specified if the client is a confidential application. - if (string.IsNullOrEmpty(secret) && - string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(descriptor.ClientSecret) && + string.Equals(descriptor.Type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException("The client secret cannot be null or empty for a confidential application.", nameof(application)); + throw new ArgumentException("The client secret cannot be null or empty for a confidential application.", nameof(descriptor)); } // Ensure no client secret was specified if the client is a public application. - else if (!string.IsNullOrEmpty(secret) && - string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) + else if (!string.IsNullOrEmpty(descriptor.ClientSecret) && + string.Equals(descriptor.Type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException("A client secret cannot be associated with a public application.", nameof(application)); + 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. // See https://tools.ietf.org/html/rfc6749#section-3.1 for more information. - var address = await Store.GetRedirectUriAsync(application, cancellationToken); - if (!string.IsNullOrEmpty(address)) + if (!string.IsNullOrEmpty(descriptor.RedirectUri)) { // Ensure the redirect_uri is a valid and absolute URL. - if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri)) + if (!Uri.TryCreate(descriptor.RedirectUri, UriKind.Absolute, out Uri uri)) { throw new ArgumentException("The redirect_uri must be an absolute URL."); } @@ -584,11 +644,10 @@ namespace OpenIddict.Core } // When a post_logout_redirect_uri is specified, ensure it is valid. - address = await Store.GetLogoutRedirectUriAsync(application, cancellationToken); - if (!string.IsNullOrEmpty(address)) + if (!string.IsNullOrEmpty(descriptor.LogoutRedirectUri)) { // Ensure the post_logout_redirect_uri is a valid and absolute URL. - if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri)) + if (!Uri.TryCreate(descriptor.LogoutRedirectUri, UriKind.Absolute, out Uri uri)) { throw new ArgumentException("The post_logout_redirect_uri must be an absolute URL."); } @@ -599,6 +658,8 @@ namespace OpenIddict.Core throw new ArgumentException("The post_logout_redirect_uri cannot contain a fragment."); } } + + return Task.CompletedTask; } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index cd249bb0..e0be8bdd 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -5,7 +5,6 @@ */ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -52,14 +51,15 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// - public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - return Store.CreateAsync(authorization, cancellationToken); + await ValidateAsync(authorization, cancellationToken); + return await Store.CreateAsync(authorization, cancellationToken); } /// @@ -70,14 +70,15 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// - public virtual Task CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) + public virtual async Task CreateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) { if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); } - return Store.CreateAsync(descriptor, cancellationToken); + await ValidateAsync(descriptor, cancellationToken); + return await Store.CreateAsync(descriptor, cancellationToken); } /// @@ -145,6 +146,7 @@ namespace OpenIddict.Core if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) { await Store.SetStatusAsync(authorization, OpenIddictConstants.Statuses.Revoked, cancellationToken); + await ValidateAsync(authorization, cancellationToken); await UpdateAsync(authorization, cancellationToken); } } @@ -157,14 +159,15 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - return Store.UpdateAsync(authorization, cancellationToken); + await ValidateAsync(authorization, cancellationToken); + await Store.UpdateAsync(authorization, cancellationToken); } /// @@ -182,10 +185,35 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(authorization)); } - if (string.IsNullOrEmpty(await Store.GetSubjectAsync(authorization, cancellationToken))) + var descriptor = new OpenIddictAuthorizationDescriptor + { + Subject = await Store.GetSubjectAsync(authorization, cancellationToken) + }; + + await ValidateAsync(descriptor, cancellationToken); + } + + /// + /// Validates the authorization descriptor to ensure it's in a consistent state. + /// + /// The authorization descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + protected virtual Task ValidateAsync([NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (string.IsNullOrEmpty(descriptor.Subject)) { throw new ArgumentException("The subject cannot be null or empty."); } + + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs index 88281ae9..70f7b4e9 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -33,6 +33,16 @@ namespace OpenIddict.Core /// Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// + /// Creates a new application. + /// + /// The application descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the application. + /// + Task CreateAsync([NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken); + /// /// Removes an existing application. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 90c4fb05..a344f528 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -105,6 +105,34 @@ namespace OpenIddict.EntityFrameworkCore return application; } + /// + /// Creates a new application. + /// + /// The application descriptor. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the application. + /// + public Task CreateAsync([NotNull] OpenIddictApplicationDescriptor descriptor, CancellationToken cancellationToken) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + var application = new TApplication + { + ClientId = descriptor.ClientId, + ClientSecret = descriptor.ClientSecret, + DisplayName = descriptor.DisplayName, + LogoutRedirectUri = descriptor.LogoutRedirectUri, + RedirectUri = descriptor.RedirectUri, + Type = descriptor.Type + }; + + return CreateAsync(application, cancellationToken); + } + /// /// Removes an existing application. ///