diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 7a61d0e0..824e07e8 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -44,7 +44,7 @@ namespace OpenIddict.Core { /// 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] TApplication application, CancellationToken cancellationToken) { + public virtual async Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } @@ -73,7 +73,7 @@ namespace OpenIddict.Core { /// 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( + public virtual async Task CreateAsync( [NotNull] TApplication application, [NotNull] string secret, CancellationToken cancellationToken) { if (application == null) { @@ -196,6 +196,23 @@ namespace OpenIddict.Core { return Store.GetDisplayNameAsync(application, cancellationToken); } + /// + /// Retrieves the unique 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 unique identifier associated with the application. + /// + public virtual Task GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Store.GetIdAsync(application, cancellationToken); + } + /// /// Retrieves the token identifiers associated with an application. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index d0a70380..c377075c 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -4,6 +4,9 @@ * the license and the contributors participating to this project. */ +using System; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Logging; @@ -29,5 +32,83 @@ namespace OpenIddict.Core { /// Gets the store associated with the current manager. /// protected IOpenIddictAuthorizationStore Store { get; } + + /// + /// Creates a new authorization. + /// + /// The application to create. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { + if (authorization == null) { + throw new ArgumentNullException(nameof(authorization)); + } + + return Store.CreateAsync(authorization, cancellationToken); + } + + /// + /// Retrieves an authorization using its associated subject/client. + /// + /// 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 subject/client. + /// + public virtual Task FindAsync(string subject, string client, CancellationToken cancellationToken) { + return Store.FindAsync(subject, client, cancellationToken); + } + + /// + /// Retrieves an authorization using its unique identifier. + /// + /// 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 identifier. + /// + public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken) { + return Store.FindByIdAsync(identifier, cancellationToken); + } + + /// + /// Retrieves the unique identifier associated with an authorization. + /// + /// 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 unique identifier associated with the authorization. + /// + public virtual Task GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { + if (authorization == null) { + throw new ArgumentNullException(nameof(authorization)); + } + + return Store.GetIdAsync(authorization, cancellationToken); + } + + /// + /// Validates the authorization to ensure it's in a consistent state. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + protected virtual async Task ValidateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { + if (authorization == null) { + throw new ArgumentNullException(nameof(authorization)); + } + + if (string.IsNullOrEmpty(await Store.GetSubjectAsync(authorization, cancellationToken))) { + throw new ArgumentException("The subject cannot be null or empty."); + } + } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index d7736a1e..95ef036a 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -7,6 +7,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Primitives; using JetBrains.Annotations; using Microsoft.Extensions.Logging; @@ -34,20 +35,46 @@ namespace OpenIddict.Core { protected IOpenIddictTokenStore Store { get; } /// - /// Creates a new token, which is not associated with a particular user or client. + /// Creates a new token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the token. + /// + public virtual async Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + await ValidateAsync(token, cancellationToken); + return await Store.CreateAsync(token, cancellationToken); + } + + /// + /// Creates a new token, which is associated with a particular subject. /// /// The token type. + /// The subject associated with the token. /// 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 token. + /// A that can be used to monitor the asynchronous operation, whose result returns the token. /// - public virtual Task CreateAsync(string type, CancellationToken cancellationToken) { + public virtual async Task CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); } - return Store.CreateAsync(type, cancellationToken); + if (!string.Equals(type, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, StringComparison.OrdinalIgnoreCase) && + !string.Equals(type, OpenIdConnectConstants.TokenTypeHints.RefreshToken, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("The specified token type is not supported by the default token manager."); + } + + if (string.IsNullOrEmpty(subject)) { + throw new ArgumentException("The subject cannot be null or empty."); + } + + return await Store.CreateAsync(type, subject, cancellationToken); } /// @@ -63,18 +90,128 @@ namespace OpenIddict.Core { return Store.FindByIdAsync(identifier, cancellationToken); } + /// + /// Retrieves the list of tokens corresponding to the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// + /// 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) { + return Store.FindBySubjectAsync(subject, cancellationToken); + } + + /// + /// Retrieves the unique identifier associated with a token. + /// + /// The token. + /// 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 token. + /// + public virtual Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + return Store.GetIdAsync(token, cancellationToken); + } + /// /// Revokes a token. /// /// The token to revoke. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. - public virtual Task RevokeAsync(TToken token, CancellationToken cancellationToken) { + public virtual Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } return Store.RevokeAsync(token, cancellationToken); } + + /// + /// Sets the authorization associated with a token. + /// + /// The token. + /// 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. + /// + public virtual async Task SetAuthorizationAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + await Store.SetAuthorizationAsync(token, identifier, cancellationToken); + await UpdateAsync(token, cancellationToken); + } + + /// + /// Sets the client application associated with a token. + /// + /// The token. + /// The unique identifier associated with the client application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task SetClientAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + await Store.SetClientAsync(token, identifier, cancellationToken); + await UpdateAsync(token, cancellationToken); + } + + /// + /// Updates an existing token. + /// + /// The token to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + return Store.UpdateAsync(token, cancellationToken); + } + + /// + /// Validates the token to ensure it's in a consistent state. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + protected virtual async Task ValidateAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + var type = await Store.GetTokenTypeAsync(token, cancellationToken); + if (string.IsNullOrEmpty(type)) { + throw new ArgumentException("The token type cannot be null or empty.", nameof(token)); + } + + if (!string.Equals(type, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, StringComparison.OrdinalIgnoreCase) && + !string.Equals(type, OpenIdConnectConstants.TokenTypeHints.RefreshToken, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("The specified token type is not supported by the default token manager."); + } + + if (string.IsNullOrEmpty(await Store.GetSubjectAsync(token, cancellationToken))) { + throw new ArgumentException("The subject cannot be null or empty."); + } + } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index 965402ff..ebb1ec31 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -8,6 +8,7 @@ using System; using System.ComponentModel; using JetBrains.Annotations; using OpenIddict.Core; +using OpenIddict.Models; #if NETSTANDARD1_3 using System.Reflection; @@ -34,25 +35,25 @@ namespace Microsoft.Extensions.DependencyInjection { /// Gets or sets the type corresponding to the Application entity. /// [EditorBrowsable(EditorBrowsableState.Never)] - public Type ApplicationType { get; set; } + public Type ApplicationType { get; set; } = typeof(OpenIddictApplication); /// /// Gets or sets the type corresponding to the Authorization entity. /// [EditorBrowsable(EditorBrowsableState.Never)] - public Type AuthorizationType { get; set; } + public Type AuthorizationType { get; set; } = typeof(OpenIddictAuthorization); /// /// Gets or sets the type corresponding to the Scope entity. /// [EditorBrowsable(EditorBrowsableState.Never)] - public Type ScopeType { get; set; } + public Type ScopeType { get; set; } = typeof(OpenIddictScope); /// /// Gets or sets the type corresponding to the Token entity. /// [EditorBrowsable(EditorBrowsableState.Never)] - public Type TokenType { get; set; } + public Type TokenType { get; set; } = typeof(OpenIddictToken); /// /// Gets the services collection. diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index b6f7d0be..f8ca9d9d 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -24,6 +24,10 @@ namespace OpenIddict.Core { public const string ExternalProvidersSupported = "external_providers_supported"; } + public static class Properties { + public const string AuthorizationId = ".authorization_id"; + } + 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 f40cd297..a6954e8f 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -21,10 +21,9 @@ namespace OpenIddict.Core { /// The application to create. /// 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. + /// A that can be used to monitor the asynchronous operation, whose result returns the application. /// - Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken); + Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken); /// /// Removes an existing application. @@ -113,6 +112,17 @@ namespace OpenIddict.Core { /// Task GetHashedSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// + /// Retrieves the unique 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 unique identifier associated with the application. + /// + Task GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// /// Retrieves the logout callback address associated with an application. /// diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs index 9ba7a7dd..2bb1b3a8 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs @@ -4,10 +4,69 @@ * the license and the contributors participating to this project. */ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + namespace OpenIddict.Core { /// /// Provides methods allowing to manage the authorizations stored in a database. /// /// The type of the Authorization entity. - public interface IOpenIddictAuthorizationStore where TAuthorization : class { } + public interface IOpenIddictAuthorizationStore where TAuthorization : class { + /// + /// Creates a new authorization. + /// + /// The authorization to create. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. + /// + Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + + /// + /// Retrieves an authorization using its unique identifier. + /// + /// 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 identifier. + /// + Task FindByIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves an authorization using its associated subject/client. + /// + /// 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 subject/client. + /// + Task FindAsync(string subject, string client, CancellationToken cancellationToken); + + /// + /// Retrieves the unique identifier associated with an authorization. + /// + /// 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 unique identifier associated with the authorization. + /// + Task GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + + /// + /// Retrieves the subject associated with an authorization. + /// + /// 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 subject associated with the specified authorization. + /// + Task GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs index 057eb55b..c2f92973 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs @@ -15,15 +15,25 @@ namespace OpenIddict.Core { /// The type of the Token entity. public interface IOpenIddictTokenStore where TToken : class { /// - /// Creates a new token, which is not associated with a particular user or client. + /// Creates a new token. + /// + /// The token to create. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the token. + /// + Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken); + + /// + /// Creates a new token, which is associated with a particular subject. /// /// The token type. + /// The subject associated with the token. /// 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 token. + /// A that can be used to monitor the asynchronous operation, whose result returns the token. /// - Task CreateAsync([NotNull] string type, CancellationToken cancellationToken); + Task CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken); /// /// Retrieves an token using its unique identifier. @@ -36,6 +46,50 @@ namespace OpenIddict.Core { /// Task FindByIdAsync(string identifier, CancellationToken cancellationToken); + /// + /// Retrieves the list of tokens corresponding to the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// + /// 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); + + /// + /// Retrieves the unique identifier associated with a token. + /// + /// The token. + /// 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 token. + /// + Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken); + + /// + /// Retrieves the token type associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the token type associated with the specified token. + /// + Task GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken); + + /// + /// Retrieves the subject associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified token. + /// + Task GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken); + /// /// Revokes a token. /// @@ -43,5 +97,37 @@ namespace OpenIddict.Core { /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken); + + /// + /// Sets the authorization associated with a token. + /// + /// The token. + /// 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. + /// + Task SetAuthorizationAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken); + + /// + /// Sets the client application associated with a token. + /// + /// The token. + /// The unique identifier associated with the client application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetClientAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken); + + /// + /// Updates an existing token. + /// + /// The token to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/OpenIddict.Core/project.json b/src/OpenIddict.Core/project.json index 0a482671..2e2ade8a 100644 --- a/src/OpenIddict.Core/project.json +++ b/src/OpenIddict.Core/project.json @@ -33,6 +33,7 @@ }, "dependencies": { + "AspNet.Security.OpenIdConnect.Primitives": "1.0.0-rc1-*", "CryptoHelper": "2.0.0", "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0", @@ -46,7 +47,8 @@ "netstandard1.3": { "dependencies": { - "System.Reflection.TypeExtensions": "4.1.0" + "System.Reflection.TypeExtensions": "4.1.0", + "System.Security.Claims": "4.0.1" }, "imports": [ diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs index 04dde40c..46f86ec5 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs @@ -17,10 +17,10 @@ namespace OpenIddict.EntityFrameworkCore { /// required by the OpenIddict stack in an Entity Framework context. /// public class OpenIddictCustomizer : ModelCustomizer - where TApplication : OpenIddictApplication - where TAuthorization : OpenIddictAuthorization - where TScope : OpenIddictScope - where TToken : OpenIddictToken + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TScope : OpenIddictScope, new() + where TToken : OpenIddictToken, new() where TKey : IEquatable { public override void Customize([NotNull] ModelBuilder builder, [NotNull] DbContext context) { if (builder == null) { diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs index 669250b5..a698efd0 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs @@ -5,18 +5,19 @@ */ using System; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Models; namespace OpenIddict.EntityFrameworkCore { public class OpenIddictExtension : IDbContextOptionsExtension - where TApplication : OpenIddictApplication - where TAuthorization : OpenIddictAuthorization - where TScope : OpenIddictScope - where TToken : OpenIddictToken + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TScope : OpenIddictScope, new() + where TToken : OpenIddictToken, new() where TKey : IEquatable { - public void ApplyServices(IServiceCollection services) { + public void ApplyServices([NotNull] IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs index d25d8e6e..593f7835 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs @@ -5,6 +5,7 @@ */ using System; +using System.ComponentModel; using System.Diagnostics; using System.Reflection; using JetBrains.Annotations; @@ -18,7 +19,7 @@ using OpenIddict.Models; namespace Microsoft.Extensions.DependencyInjection { public static class OpenIddictExtensions { /// - /// Registers the Entity Framework stores. Note: when using the built-in Entity Framework stores, + /// Registers the Entity Framework Core stores. Note: when using the Entity Framework Core stores, /// the entities MUST be derived from the models contained in the OpenIddict.Models package. /// /// The services builder used by OpenIddict to register new services. @@ -34,35 +35,42 @@ namespace Microsoft.Extensions.DependencyInjection { builder.ScopeType != null && builder.TokenType != null, "The entity types exposed by OpenIddictBuilder shouldn't be null."); - var application = FindGenericBaseType(builder.ApplicationType, typeof(OpenIddictApplication<,>)); + var application = FindGenericBaseType(builder.ApplicationType, typeof(OpenIddictApplication<,,>)); if (application == null) { - throw new InvalidOperationException("The Entity Framework stores can only be used " + + throw new InvalidOperationException("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictApplication entity."); } - var authorization = FindGenericBaseType(builder.AuthorizationType, typeof(OpenIddictAuthorization<,>)); + var authorization = FindGenericBaseType(builder.AuthorizationType, typeof(OpenIddictAuthorization<,,>)); if (authorization == null) { - throw new InvalidOperationException("The Entity Framework stores can only be used " + + throw new InvalidOperationException("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictAuthorization entity."); } var scope = FindGenericBaseType(builder.ScopeType, typeof(OpenIddictScope<>)); if (scope == null) { - throw new InvalidOperationException("The Entity Framework stores can only be used " + + throw new InvalidOperationException("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictScope entity."); } - var token = FindGenericBaseType(builder.TokenType, typeof(OpenIddictToken<>)); + var token = FindGenericBaseType(builder.TokenType, typeof(OpenIddictToken<,,>)); if (token == null) { - throw new InvalidOperationException("The Entity Framework stores can only be used " + + throw new InvalidOperationException("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictToken entity."); } + var converter = TypeDescriptor.GetConverter(application.GenericTypeArguments[0]); + if (converter == null || !converter.CanConvertFrom(typeof(string)) || + !converter.CanConvertTo(typeof(string))) { + throw new InvalidOperationException("The specified entity key type is not supported."); + } + // Register the application store in the DI container. builder.Services.TryAddScoped( typeof(IOpenIddictApplicationStore<>).MakeGenericType(builder.ApplicationType), - typeof(OpenIddictApplicationStore<,,,>).MakeGenericType( + typeof(OpenIddictApplicationStore<,,,,>).MakeGenericType( /* TApplication: */ builder.ApplicationType, + /* TAuthorization: */ builder.AuthorizationType, /* TToken: */ builder.TokenType, /* TContext: */ typeof(TContext), /* TKey: */ application.GenericTypeArguments[0])); @@ -70,8 +78,9 @@ namespace Microsoft.Extensions.DependencyInjection { // Register the authorization store in the DI container. builder.Services.TryAddScoped( typeof(IOpenIddictAuthorizationStore<>).MakeGenericType(builder.AuthorizationType), - typeof(OpenIddictAuthorizationStore<,,,>).MakeGenericType( + typeof(OpenIddictAuthorizationStore<,,,,>).MakeGenericType( /* TAuthorization: */ builder.AuthorizationType, + /* TApplication: */ builder.ApplicationType, /* TToken: */ builder.TokenType, /* TContext: */ typeof(TContext), /* TKey: */ authorization.GenericTypeArguments[0])); @@ -87,8 +96,9 @@ namespace Microsoft.Extensions.DependencyInjection { // Register the token store in the DI container. builder.Services.TryAddScoped( typeof(IOpenIddictTokenStore<>).MakeGenericType(builder.TokenType), - typeof(OpenIddictTokenStore<,,,>).MakeGenericType( + typeof(OpenIddictTokenStore<,,,,>).MakeGenericType( /* TToken: */ builder.TokenType, + /* TApplication: */ builder.ApplicationType, /* TAuthorization: */ builder.AuthorizationType, /* TContext: */ typeof(TContext), /* TKey: */ token.GenericTypeArguments[0])); @@ -103,7 +113,10 @@ namespace Microsoft.Extensions.DependencyInjection { /// The builder used to configure the Entity Framework context. /// The Entity Framework context builder. public static DbContextOptionsBuilder UseOpenIddict([NotNull] this DbContextOptionsBuilder builder) { - return builder.UseOpenIddict(); + return builder.UseOpenIddict(); } /// @@ -126,10 +139,10 @@ namespace Microsoft.Extensions.DependencyInjection { /// The builder used to configure the Entity Framework context. /// The Entity Framework context builder. public static DbContextOptionsBuilder UseOpenIddict([NotNull] this DbContextOptionsBuilder builder) - where TApplication : OpenIddictApplication - where TAuthorization : OpenIddictAuthorization - where TScope : OpenIddictScope - where TToken : OpenIddictToken + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TScope : OpenIddictScope, new() + where TToken : OpenIddictToken, new() where TKey : IEquatable { if (builder == null) { throw new ArgumentNullException(nameof(builder)); @@ -148,7 +161,10 @@ namespace Microsoft.Extensions.DependencyInjection { /// The builder used to configure the Entity Framework context. /// The Entity Framework context builder. public static ModelBuilder UseOpenIddict([NotNull] this ModelBuilder builder) { - return builder.UseOpenIddict(); + return builder.UseOpenIddict(); } /// @@ -171,10 +187,10 @@ namespace Microsoft.Extensions.DependencyInjection { /// The builder used to configure the Entity Framework context. /// The Entity Framework context builder. public static ModelBuilder UseOpenIddict([NotNull] this ModelBuilder builder) - where TApplication : OpenIddictApplication - where TAuthorization : OpenIddictAuthorization - where TScope : OpenIddictScope - where TToken : OpenIddictToken + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TScope : OpenIddictScope, new() + where TToken : OpenIddictToken, new() where TKey : IEquatable { if (builder == null) { throw new ArgumentNullException(nameof(builder)); @@ -188,11 +204,16 @@ namespace Microsoft.Extensions.DependencyInjection { builder.Entity(entity => { entity.HasKey(application => application.Id); - entity.HasIndex("ClientId") + entity.HasIndex(application => application.ClientId) .IsUnique(unique: true); + entity.HasMany(application => application.Authorizations) + .WithOne(authorization => authorization.Application) + .HasForeignKey("ApplicationId") + .IsRequired(required: false); + entity.HasMany(application => application.Tokens) - .WithOne() + .WithOne(token => token.Application) .HasForeignKey("ApplicationId") .IsRequired(required: false); @@ -204,7 +225,7 @@ namespace Microsoft.Extensions.DependencyInjection { entity.HasKey(authorization => authorization.Id); entity.HasMany(application => application.Tokens) - .WithOne() + .WithOne(token => token.Authorization) .HasForeignKey("AuthorizationId") .IsRequired(required: false); @@ -229,6 +250,14 @@ namespace Microsoft.Extensions.DependencyInjection { } private static TypeInfo FindGenericBaseType(Type type, Type definition) { + if (type == null) { + throw new ArgumentNullException(nameof(type)); + } + + if (definition == null) { + throw new ArgumentNullException(nameof(definition)); + } + for (var candidate = type.GetTypeInfo(); candidate != null; candidate = candidate.BaseType?.GetTypeInfo()) { if (candidate.IsGenericType && candidate.GetGenericTypeDefinition() == definition) { return candidate; diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index a15c57dd..35f7fb56 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -16,16 +16,42 @@ using OpenIddict.Core; using OpenIddict.Models; namespace OpenIddict.EntityFrameworkCore { + /// + /// Provides methods allowing to manage the applications stored in a database. + /// + /// The type of the Entity Framework database context. + public class OpenIddictApplicationStore : OpenIddictApplicationStore + where TContext : DbContext { + public OpenIddictApplicationStore([NotNull] TContext context) : base(context) { } + } + + /// + /// Provides methods allowing to manage the applications stored in a database. + /// + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictApplicationStore : OpenIddictApplicationStore, + OpenIddictAuthorization, + OpenIddictToken, TContext, TKey> + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictApplicationStore([NotNull] TContext context) : base(context) { } + } + /// /// Provides methods allowing to manage the applications stored in a database. /// /// The type of the Application entity. + /// The type of the Authorization entity. /// The type of the Token entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictApplicationStore : IOpenIddictApplicationStore - where TApplication : OpenIddictApplication - where TToken : OpenIddictToken, new() + public class OpenIddictApplicationStore : IOpenIddictApplicationStore + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TToken : OpenIddictToken, new() where TContext : DbContext where TKey : IEquatable { public OpenIddictApplicationStore([NotNull] TContext context) { @@ -51,23 +77,19 @@ namespace OpenIddict.EntityFrameworkCore { /// /// The application to create. /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public virtual async Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the application. + /// + public virtual async Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } - // Ensure that the key type can be serialized. - var converter = TypeDescriptor.GetConverter(typeof(TKey)); - if (!converter.CanConvertTo(typeof(string))) { - throw new InvalidOperationException($"The '{typeof(TKey).Name}' key type is not supported."); - } - Context.Add(application); await Context.SaveChangesAsync(cancellationToken); - return converter.ConvertToInvariantString(application.Id); + return application; } /// @@ -102,15 +124,7 @@ namespace OpenIddict.EntityFrameworkCore { /// whose result returns the client application corresponding to the identifier. /// public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken) { - var converter = TypeDescriptor.GetConverter(typeof(TKey)); - - // If the string key cannot be converted to TKey, return null - // to indicate that the requested application doesn't exist. - if (!converter.CanConvertFrom(typeof(string))) { - return Task.FromResult(null); - } - - var key = (TKey) converter.ConvertFromInvariantString(identifier); + var key = ConvertIdentifierFromString(identifier); return Applications.SingleOrDefaultAsync(application => application.Id.Equals(key), cancellationToken); } @@ -209,6 +223,23 @@ namespace OpenIddict.EntityFrameworkCore { return Task.FromResult(application.ClientSecret); } + /// + /// Retrieves the unique 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 unique identifier associated with the application. + /// + public virtual Task GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(ConvertIdentifierToString(application.Id)); + } + /// /// Retrieves the logout callback address associated with an application. /// @@ -257,12 +288,6 @@ namespace OpenIddict.EntityFrameworkCore { throw new ArgumentNullException(nameof(application)); } - // Ensure that the key type can be serialized. - var converter = TypeDescriptor.GetConverter(typeof(TKey)); - if (!converter.CanConvertTo(typeof(string))) { - throw new InvalidOperationException($"The '{typeof(TKey).Name}' key type is not supported."); - } - var query = from entity in Applications where entity.Id.Equals(application.Id) from token in entity.Tokens @@ -271,7 +296,7 @@ namespace OpenIddict.EntityFrameworkCore { var tokens = new List(); foreach (var identifier in await query.ToArrayAsync()) { - tokens.Add(converter.ConvertToInvariantString(identifier)); + tokens.Add(ConvertIdentifierToString(identifier)); } return tokens; @@ -345,5 +370,33 @@ namespace OpenIddict.EntityFrameworkCore { catch (DbUpdateConcurrencyException) { } } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) { + if (string.IsNullOrEmpty(identifier)) { + return default(TKey); + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) { + if (Equals(identifier, default(TKey))) { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertToInvariantString(identifier); + } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index 03767c29..26a36030 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -5,22 +5,52 @@ */ using System; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using OpenIddict.Core; using OpenIddict.Models; namespace OpenIddict.EntityFrameworkCore { + /// + /// Provides methods allowing to manage the authorizations stored in a database. + /// + /// The type of the Entity Framework database context. + public class OpenIddictAuthorizationStore : OpenIddictAuthorizationStore + where TContext : DbContext { + public OpenIddictAuthorizationStore([NotNull] TContext context) : base(context) { } + } + + /// + /// Provides methods allowing to manage the authorizations stored in a database. + /// + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictAuthorizationStore : OpenIddictAuthorizationStore, + OpenIddictApplication, + OpenIddictToken, TContext, TKey> + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictAuthorizationStore([NotNull] TContext context) : base(context) { } + } + /// /// Provides methods allowing to manage the authorizations stored in a database. /// /// The type of the Authorization entity. + /// The type of the Application entity. /// The type of the Token entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore - where TAuthorization : OpenIddictAuthorization - where TToken : OpenIddictToken + public class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore + where TAuthorization : OpenIddictAuthorization, new() + where TApplication : OpenIddictApplication, new() + where TToken : OpenIddictToken, new() where TContext : DbContext where TKey : IEquatable { public OpenIddictAuthorizationStore([NotNull] TContext context) { @@ -36,9 +66,131 @@ namespace OpenIddict.EntityFrameworkCore { /// protected virtual TContext Context { get; } + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Applications => Context.Set(); + /// /// Gets the database set corresponding to the entity. /// protected DbSet Authorizations => Context.Set(); + + /// + /// Creates a new authorization. + /// + /// The authorization to create. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. + /// + public virtual async Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { + if (authorization == null) { + throw new ArgumentNullException(nameof(authorization)); + } + + Context.Add(authorization); + + await Context.SaveChangesAsync(cancellationToken); + + return authorization; + } + + /// + /// Retrieves an authorization using its associated subject/client. + /// + /// 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 subject/client. + /// + public virtual Task FindAsync(string subject, string client, CancellationToken cancellationToken) { + var key = ConvertIdentifierFromString(client); + + return (from application in Applications + where application.Id.Equals(key) + from authorization in application.Authorizations + where authorization.Subject == subject + select authorization).FirstOrDefaultAsync(); + } + + /// + /// Retrieves an authorization using its unique identifier. + /// + /// 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 identifier. + /// + public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken) { + var key = ConvertIdentifierFromString(identifier); + + return Authorizations.SingleOrDefaultAsync(authorization => authorization.Id.Equals(key), cancellationToken); + } + + /// + /// Retrieves the unique identifier associated with an authorization. + /// + /// 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 unique identifier associated with the authorization. + /// + public virtual Task GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { + if (authorization == null) { + throw new ArgumentNullException(nameof(authorization)); + } + + return Task.FromResult(ConvertIdentifierToString(authorization.Id)); + } + + /// + /// Retrieves the subject associated with an authorization. + /// + /// 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 subject associated with the specified authorization. + /// + public virtual Task GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { + if (authorization == null) { + throw new ArgumentNullException(nameof(authorization)); + } + + return Task.FromResult(authorization.Subject); + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) { + if (string.IsNullOrEmpty(identifier)) { + return default(TKey); + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) { + if (Equals(identifier, default(TKey))) { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertToInvariantString(identifier); + } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index b28e49ee..d2ed90b6 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -5,12 +5,33 @@ */ using System; +using System.ComponentModel; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using OpenIddict.Core; using OpenIddict.Models; namespace OpenIddict.EntityFrameworkCore { + /// + /// Provides methods allowing to manage the scopes stored in a database. + /// + /// The type of the Entity Framework database context. + public class OpenIddictScopeStore : OpenIddictScopeStore + where TContext : DbContext { + public OpenIddictScopeStore([NotNull] TContext context) : base(context) { } + } + + /// + /// Provides methods allowing to manage the scopes stored in a database. + /// + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictScopeStore : OpenIddictScopeStore, TContext, TKey> + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictScopeStore([NotNull] TContext context) : base(context) { } + } + /// /// Provides methods allowing to manage the scopes stored in a database. /// @@ -18,7 +39,7 @@ namespace OpenIddict.EntityFrameworkCore { /// The type of the Entity Framework database context. /// The type of the entity primary keys. public class OpenIddictScopeStore : IOpenIddictScopeStore - where TScope : OpenIddictScope + where TScope : OpenIddictScope, new() where TContext : DbContext where TKey : IEquatable { public OpenIddictScopeStore([NotNull] TContext context) { @@ -37,6 +58,34 @@ namespace OpenIddict.EntityFrameworkCore { /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + protected DbSet Scopes => Context.Set(); + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) { + if (string.IsNullOrEmpty(identifier)) { + return default(TKey); + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) { + if (Equals(identifier, default(TKey))) { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertToInvariantString(identifier); + } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index 4145db57..5621c005 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -14,16 +15,42 @@ using OpenIddict.Core; using OpenIddict.Models; namespace OpenIddict.EntityFrameworkCore { + /// + /// Provides methods allowing to manage the tokens stored in a database. + /// + /// The type of the Entity Framework database context. + public class OpenIddictTokenStore : OpenIddictTokenStore + where TContext : DbContext { + public OpenIddictTokenStore([NotNull] TContext context) : base(context) { } + } + + /// + /// Provides methods allowing to manage the tokens stored in a database. + /// + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictTokenStore : OpenIddictTokenStore, + OpenIddictApplication, + OpenIddictAuthorization, TContext, TKey> + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictTokenStore([NotNull] TContext context) : base(context) { } + } + /// /// Provides methods allowing to manage the tokens stored in a database. /// /// The type of the Token entity. + /// The type of the Application entity. /// The type of the Authorization entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictTokenStore : IOpenIddictTokenStore - where TToken : OpenIddictToken, new() - where TAuthorization : OpenIddictAuthorization + public class OpenIddictTokenStore : IOpenIddictTokenStore + where TToken : OpenIddictToken, new() + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() where TContext : DbContext where TKey : IEquatable { public OpenIddictTokenStore([NotNull] TContext context) { @@ -39,37 +66,56 @@ namespace OpenIddict.EntityFrameworkCore { /// protected virtual TContext Context { get; } + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Applications => Context.Set(); + + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Authorizations => Context.Set(); + /// /// Gets the database set corresponding to the entity. /// protected DbSet Tokens => Context.Set(); /// - /// Creates a new token, which is not associated with a particular user or client. + /// Creates a new token. /// - /// The token type. + /// The token to create. /// 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 token. + /// A that can be used to monitor the asynchronous operation, whose result returns the token. /// - public virtual async Task CreateAsync([NotNull] string type, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(type)) { - throw new ArgumentException("The token type cannot be null or empty."); - } - - // Ensure that the key type can be serialized. - var converter = TypeDescriptor.GetConverter(typeof(TKey)); - if (!converter.CanConvertTo(typeof(string))) { - throw new InvalidOperationException($"The '{typeof(TKey).Name}' key type is not supported."); + public virtual async Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); } - var token = new TToken { Type = type }; - Tokens.Add(token); + Context.Add(token); await Context.SaveChangesAsync(cancellationToken); - return converter.ConvertToInvariantString(token.Id); + return token; + } + + /// + /// Creates a new token, which is associated with a particular subject. + /// + /// The token type. + /// The subject associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result returns the token. + /// + public virtual Task CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken) { + if (string.IsNullOrEmpty(type)) { + throw new ArgumentException("The token type cannot be null or empty."); + } + + return CreateAsync(new TToken { Subject = subject, Type = type }, cancellationToken); } /// @@ -82,16 +128,73 @@ namespace OpenIddict.EntityFrameworkCore { /// whose result returns the token corresponding to the unique identifier. /// public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken) { - // If the string key cannot be converted to TKey, return null - // to indicate that the requested token doesn't exist. - var converter = TypeDescriptor.GetConverter(typeof(TKey)); - if (!converter.CanConvertFrom(typeof(string))) { - return Task.FromResult(null); + var key = ConvertIdentifierFromString(identifier); + + return Tokens.SingleOrDefaultAsync(token => token.Id.Equals(key), cancellationToken); + } + + /// + /// Retrieves the list of tokens corresponding to the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// + /// 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) { + return Tokens.Where(token => token.Subject == subject).ToArrayAsync(); + } + + /// + /// Retrieves the unique identifier associated with a token. + /// + /// The token. + /// 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 token. + /// + public virtual Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); } - var key = (TKey) converter.ConvertFromInvariantString(identifier); + return Task.FromResult(ConvertIdentifierToString(token.Id)); + } - return Tokens.SingleOrDefaultAsync(token => token.Id.Equals(key), cancellationToken); + /// + /// Retrieves the token type associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the token type associated with the specified token. + /// + public virtual Task GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + return Task.FromResult(token.Type); + } + + /// + /// Retrieves the subject associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified token. + /// + public virtual Task GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + return Task.FromResult(token.Subject); } /// @@ -113,5 +216,130 @@ namespace OpenIddict.EntityFrameworkCore { catch (DbUpdateConcurrencyException) { } } + + /// + /// Sets the authorization associated with a token. + /// + /// The token. + /// 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. + /// + public virtual async Task SetAuthorizationAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + if (!string.IsNullOrEmpty(identifier)) { + var key = ConvertIdentifierFromString(identifier); + + var authorization = await Authorizations.SingleOrDefaultAsync(element => element.Id.Equals(key)); + if (authorization == null) { + throw new InvalidOperationException("The authorization associated with the token cannot be found."); + } + + authorization.Tokens.Add(token); + } + + else { + var key = await GetIdAsync(token, cancellationToken); + + // Try to retrieve the authorization associated with the token. + // If none can be found, assume that no authorization is attached. + var authorization = await Authorizations.SingleOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key))); + if (authorization != null) { + authorization.Tokens.Remove(token); + } + } + } + + /// + /// Sets the client application associated with a token. + /// + /// The token. + /// The unique identifier associated with the client application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task SetClientAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + if (!string.IsNullOrEmpty(identifier)) { + var key = ConvertIdentifierFromString(identifier); + + var application = await Applications.SingleOrDefaultAsync(element => element.Id.Equals(key)); + if (application == null) { + throw new InvalidOperationException("The application associated with the token cannot be found."); + } + + application.Tokens.Add(token); + } + + else { + var key = await GetIdAsync(token, cancellationToken); + + // Try to retrieve the application associated with the token. + // If none can be found, assume that no application is attached. + var application = await Applications.SingleOrDefaultAsync(element => element.Tokens.Any(t => t.Id.Equals(key))); + if (application != null) { + application.Tokens.Remove(token); + } + } + } + + /// + /// Updates an existing token. + /// + /// The token to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + Context.Attach(token); + Context.Update(token); + + try { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException) { } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) { + if (string.IsNullOrEmpty(identifier)) { + return default(TKey); + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) { + if (Equals(identifier, default(TKey))) { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)) + .ConvertToInvariantString(identifier); + } } } \ No newline at end of file diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs index ee2b18a2..dc9b98df 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -11,7 +11,7 @@ namespace OpenIddict.Models { /// /// Represents an OpenIddict application. /// - public class OpenIddictApplication : OpenIddictApplication { + public class OpenIddictApplication : OpenIddictApplication { public OpenIddictApplication() { // Generate a new string identifier. Id = Guid.NewGuid().ToString(); @@ -21,13 +21,18 @@ namespace OpenIddict.Models { /// /// Represents an OpenIddict application. /// - public class OpenIddictApplication : OpenIddictApplication> + public class OpenIddictApplication : OpenIddictApplication, OpenIddictToken> where TKey : IEquatable { } /// /// Represents an OpenIddict application. /// - public class OpenIddictApplication where TKey : IEquatable { + public class OpenIddictApplication where TKey : IEquatable { + /// + /// Gets the list of the authorizations associated with this application. + /// + public virtual IList Authorizations { get; } = new List(); + /// /// Gets or sets the client identifier /// associated with the current application. diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.Models/OpenIddictAuthorization.cs index 9837b05a..13ad15b5 100644 --- a/src/OpenIddict.Models/OpenIddictAuthorization.cs +++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs @@ -11,7 +11,7 @@ namespace OpenIddict.Models { /// /// Represents an OpenIddict authorization. /// - public class OpenIddictAuthorization : OpenIddictAuthorization { + public class OpenIddictAuthorization : OpenIddictAuthorization { public OpenIddictAuthorization() { // Generate a new string identifier. Id = Guid.NewGuid().ToString(); @@ -21,13 +21,18 @@ namespace OpenIddict.Models { /// /// Represents an OpenIddict authorization. /// - public class OpenIddictAuthorization : OpenIddictAuthorization> + public class OpenIddictAuthorization : OpenIddictAuthorization, OpenIddictToken> where TKey : IEquatable { } /// /// Represents an OpenIddict authorization. /// - public class OpenIddictAuthorization where TKey : IEquatable { + public class OpenIddictAuthorization where TKey : IEquatable { + /// + /// Gets or sets the application associated with the current authorization. + /// + public virtual TApplication Application { get; set; } + /// /// Gets or sets the unique identifier /// associated with the current authorization. @@ -40,6 +45,11 @@ namespace OpenIddict.Models { /// public virtual string Scope { get; set; } + /// + /// Gets or sets the subject associated with the current authorization. + /// + public virtual string Subject { get; set; } + /// /// Gets or sets the list of tokens /// associated with the current authorization. diff --git a/src/OpenIddict.Models/OpenIddictToken.cs b/src/OpenIddict.Models/OpenIddictToken.cs index e8a1976c..d9574e3d 100644 --- a/src/OpenIddict.Models/OpenIddictToken.cs +++ b/src/OpenIddict.Models/OpenIddictToken.cs @@ -10,7 +10,7 @@ namespace OpenIddict.Models { /// /// Represents an OpenIddict token. /// - public class OpenIddictToken : OpenIddictToken { + public class OpenIddictToken : OpenIddictToken { public OpenIddictToken() { // Generate a new string identifier. Id = Guid.NewGuid().ToString(); @@ -20,13 +20,35 @@ namespace OpenIddict.Models { /// /// Represents an OpenIddict token. /// - public class OpenIddictToken where TKey : IEquatable { + public class OpenIddictToken : OpenIddictToken, OpenIddictAuthorization> + where TKey : IEquatable { + } + + /// + /// Represents an OpenIddict token. + /// + public class OpenIddictToken where TKey : IEquatable { + /// + /// Gets or sets the application associated with the current token. + /// + public virtual TApplication Application { get; set; } + + /// + /// Gets or sets the authorization associated with the current token. + /// + public virtual TAuthorization Authorization { get; set; } + /// /// Gets or sets the unique identifier /// associated with the current token. /// public virtual TKey Id { get; set; } + /// + /// Gets or sets the subject associated with the current token. + /// + public virtual string Subject { get; set; } + /// /// Gets or sets the type of the current token. /// diff --git a/src/OpenIddict.Mvc/OpenIddictModelBinder.cs b/src/OpenIddict.Mvc/OpenIddictModelBinder.cs index ed27023a..df34f900 100644 --- a/src/OpenIddict.Mvc/OpenIddictModelBinder.cs +++ b/src/OpenIddict.Mvc/OpenIddictModelBinder.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; +using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -17,7 +18,7 @@ namespace OpenIddict.Mvc { /// /// The model binding context. /// A representing the asynchronous operation. - public Task BindModelAsync(ModelBindingContext context) { + public Task BindModelAsync([NotNull] ModelBindingContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } @@ -65,7 +66,7 @@ namespace OpenIddict.Mvc { /// /// The model binding context. /// The current instance or null if the model is not supported. - public IModelBinder GetBinder(ModelBinderProviderContext context) { + public IModelBinder GetBinder([NotNull] ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs index f1964163..022a4b07 100644 --- a/src/OpenIddict/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs @@ -5,6 +5,8 @@ */ using System; +using System.Diagnostics; +using System.Security.Claims; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; @@ -18,11 +20,30 @@ namespace OpenIddict { public partial class OpenIddictProvider : OpenIdConnectServerProvider where TApplication : class where TAuthorization : class where TScope : class where TToken : class { public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { + var applications = context.HttpContext.RequestServices.GetRequiredService>(); var options = context.HttpContext.RequestServices.GetRequiredService>(); var tokens = context.HttpContext.RequestServices.GetRequiredService>(); + Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client identifier shouldn't be null or empty."); + if (!options.Value.DisableTokenRevocation) { - var identifier = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, context.HttpContext.RequestAborted); + // Resolve the subject from the authentication ticket. If it cannot be found, throw an exception. + var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject) ?? + context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier) ?? + context.Ticket.Principal.GetClaim(ClaimTypes.Upn); + + if (string.IsNullOrEmpty(subject)) { + throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved."); + } + + // If a null value was returned by CreateAsync, return immediately. + var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, subject, context.HttpContext.RequestAborted); + if (token == null) { + return; + } + + // Throw an exception if the token identifier can't be resolved. + var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The unique key associated with an authorization code cannot be null or empty."); } @@ -31,15 +52,45 @@ namespace OpenIddict { // to the authorization code to override the default GUID // generated by the OpenID Connect server middleware. context.Ticket.SetTicketId(identifier); + + var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted); + if (application == null) { + throw new InvalidOperationException("The client application cannot be retrieved from the database."); + } + + await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted); + + // If an authorization identifier was specified, bind it to the token. + var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); + if (!string.IsNullOrEmpty(authorization)) { + await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted); + } } } public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { + var applications = context.HttpContext.RequestServices.GetRequiredService>(); var options = context.HttpContext.RequestServices.GetRequiredService>(); var tokens = context.HttpContext.RequestServices.GetRequiredService>(); if (!options.Value.DisableTokenRevocation) { - var identifier = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, context.HttpContext.RequestAborted); + // Resolve the subject from the authentication ticket. If it cannot be found, throw an exception. + var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject) ?? + context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier) ?? + context.Ticket.Principal.GetClaim(ClaimTypes.Upn); + + if (string.IsNullOrEmpty(subject)) { + throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved."); + } + + // If a null value was returned by CreateAsync, return immediately. + var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, subject, context.HttpContext.RequestAborted); + if (token == null) { + return; + } + + // Throw an exception if the token identifier can't be resolved. + var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); } @@ -48,6 +99,22 @@ namespace OpenIddict { // to the refresh token to override the default GUID // generated by the OpenID Connect server middleware. context.Ticket.SetTicketId(identifier); + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) { + var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted); + if (application == null) { + throw new InvalidOperationException("The client application cannot be retrieved from the database."); + } + + await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted); + } + + // If an authorization identifier was specified, bind it to the token. + var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); + if (!string.IsNullOrEmpty(authorization)) { + await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted); + } } } } diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs index 4995f34f..2d5f0356 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs @@ -9,117 +9,129 @@ namespace OpenIddict.EntityFrameworkCore.Tests { [Fact] public void AddEntityFrameworkCoreStores_ThrowsAnExceptionForInvalidApplicationEntity() { // Arrange - var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(new ServiceCollection()); + builder.ApplicationType = typeof(object); // Act and assert var exception = Assert.Throws(delegate { - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); }); - Assert.Equal("The Entity Framework stores can only be used " + + Assert.Equal("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictApplication entity.", exception.Message); } [Fact] public void AddEntityFrameworkCoreStores_ThrowsAnExceptionForInvalidAuthorizationEntity() { // Arrange - var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(new ServiceCollection()); + builder.AuthorizationType = typeof(object); // Act and assert var exception = Assert.Throws(delegate { - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); }); - Assert.Equal("The Entity Framework stores can only be used " + + Assert.Equal("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictAuthorization entity.", exception.Message); } [Fact] public void AddEntityFrameworkCoreStores_ThrowsAnExceptionForInvalidScopeEntity() { // Arrange - var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(new ServiceCollection()); + builder.ScopeType = typeof(object); // Act and assert var exception = Assert.Throws(delegate { - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); }); - Assert.Equal("The Entity Framework stores can only be used " + + Assert.Equal("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictScope entity.", exception.Message); } [Fact] public void AddEntityFrameworkCoreStores_ThrowsAnExceptionForInvalidTokenEntity() { // Arrange - var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(new ServiceCollection()); + builder.TokenType = typeof(object); // Act and assert var exception = Assert.Throws(delegate { - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); }); - Assert.Equal("The Entity Framework stores can only be used " + + Assert.Equal("The Entity Framework Core stores can only be used " + "with the built-in OpenIddictToken entity.", exception.Message); } [Theory] - [InlineData(typeof(OpenIddictApplicationStore))] - [InlineData(typeof(OpenIddictAuthorizationStore))] + [InlineData(typeof(OpenIddictApplicationStore))] + [InlineData(typeof(OpenIddictAuthorizationStore))] [InlineData(typeof(OpenIddictScopeStore))] - [InlineData(typeof(OpenIddictTokenStore))] + [InlineData(typeof(OpenIddictTokenStore))] public void AddEntityFrameworkCoreStores_RegistersEntityFrameworkStores(Type type) { // Arrange var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(services); // Act - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); // Assert Assert.Contains(services, service => service.ImplementationType == type); } [Theory] - [InlineData(typeof(OpenIddictApplicationStore, OpenIddictToken, DbContext, Guid>))] - [InlineData(typeof(OpenIddictAuthorizationStore, OpenIddictToken, DbContext, Guid>))] + [InlineData(typeof(OpenIddictApplicationStore, OpenIddictAuthorization, OpenIddictToken, DbContext, Guid>))] + [InlineData(typeof(OpenIddictAuthorizationStore, OpenIddictApplication, OpenIddictToken, DbContext, Guid>))] [InlineData(typeof(OpenIddictScopeStore, DbContext, Guid>))] - [InlineData(typeof(OpenIddictTokenStore, OpenIddictAuthorization, DbContext, Guid>))] + [InlineData(typeof(OpenIddictTokenStore, OpenIddictApplication, OpenIddictAuthorization, DbContext, Guid>))] public void AddEntityFrameworkCoreStores_KeyTypeIsInferredFromEntities(Type type) { // Arrange var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(services) { + ApplicationType = typeof(OpenIddictApplication), + AuthorizationType = typeof(OpenIddictAuthorization), + ScopeType = typeof(OpenIddictScope), + TokenType = typeof(OpenIddictToken) + }; + // Act - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); // Assert Assert.Contains(services, service => service.ImplementationType == type); } [Theory] - [InlineData(typeof(OpenIddictApplicationStore))] - [InlineData(typeof(OpenIddictAuthorizationStore))] + [InlineData(typeof(OpenIddictApplicationStore))] + [InlineData(typeof(OpenIddictAuthorizationStore))] [InlineData(typeof(OpenIddictScopeStore))] - [InlineData(typeof(OpenIddictTokenStore))] + [InlineData(typeof(OpenIddictTokenStore))] public void AddEntityFrameworkCoreStores_DefaultEntitiesCanBeReplaced(Type type) { // Arrange var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(services) { + ApplicationType = typeof(CustomApplication), + AuthorizationType = typeof(CustomAuthorization), + ScopeType = typeof(CustomScope), + TokenType = typeof(CustomToken) + }; + // Act - services.AddOpenIddict() - .AddEntityFrameworkCoreStores(); + builder.AddEntityFrameworkCoreStores(); // Assert Assert.Contains(services, service => service.ImplementationType == type); } - public class CustomApplication : OpenIddictApplication { } - public class CustomAuthorization : OpenIddictAuthorization { } + public class CustomApplication : OpenIddictApplication { } + public class CustomAuthorization : OpenIddictAuthorization { } public class CustomScope : OpenIddictScope { } - public class CustomToken : OpenIddictToken { } + public class CustomToken : OpenIddictToken { } } } diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index 48da126b..6b20410b 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -18,7 +18,6 @@ namespace OpenIddict.Tests { public void UseOpenIddict_ThrowsAnExceptionWhenServicesAreNotRegistered() { // Arrange var services = new ServiceCollection(); - var builder = new ApplicationBuilder(services.BuildServiceProvider()); // Act and assert diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs index b162a218..3f6df57a 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -503,7 +503,12 @@ namespace OpenIddict.Tests { })); builder.Services.AddSingleton(CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, It.IsAny())) + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); })); }); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs index aca01474..a410f8ee 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Client; using AspNet.Security.OpenIdConnect.Primitives; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Core; @@ -11,11 +13,112 @@ using Xunit; namespace OpenIddict.Tests { public partial class OpenIddictProviderTests { [Fact] - public async Task SerializeAuthorizationCode_AuthorizationCodeIsAutomaticallyPersisted() { + public async Task SerializeAuthorizationCode_AuthorizationCodeIsNotPersistedWhenRevocationIsDisabled() { // Arrange + var manager = CreateTokenManager(); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + 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); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); + + builder.DisableTokenRevocation(); + }); + + 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.NotNull(response.Code); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task SerializeAuthorizationCode_AuthorizationCodeIsCorrectlyPersisted() { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + 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); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + 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.NotNull(response.Code); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task SerializeAuthorizationCode_ClientApplicationIsAutomaticallyAttached() { + // Arrange + var token = new OpenIddictToken(); + var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => { @@ -33,6 +136,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); })); builder.Services.AddSingleton(manager); @@ -50,18 +156,206 @@ namespace OpenIddict.Tests { // Assert Assert.NotNull(response.Code); - Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); } [Fact] - public async Task SerializeRefreshToken_RefreshTokenIsAutomaticallyPersisted() { + public async Task SerializeAuthorizationCode_AuthorizationIsAutomaticallyAttached() { // Arrange + var token = new OpenIddictToken(); + var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, It.IsAny())) + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .Returns(Task.FromResult(0)); + + instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .Returns(Task.FromResult(0)); }); var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + 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); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + builder.Services.AddSingleton(CreateAuthorizationManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + })); + + 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.NotNull(response.Code); + + Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task SerializeRefreshToken_RefreshTokenIsNotPersistedWhenRevocationIsDisabled() { + // Arrange + var manager = CreateTokenManager(); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); + + builder.DisableTokenRevocation(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = OpenIdConnectConstants.Scopes.OfflineAccess + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task SerializeRefreshToken_RefreshTokenIsCorrectlyPersisted() { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = OpenIdConnectConstants.Scopes.OfflineAccess + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task SerializeRefreshToken_ClientApplicationIsAutomaticallyAttached() { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .Returns(Task.FromResult(0)); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = OpenIdConnectConstants.Scopes.OfflineAccess + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task SerializeRefreshToken_AuthorizationIsAutomaticallyAttached() { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .Returns(Task.FromResult(0)); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateAuthorizationManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + })); + builder.Services.AddSingleton(manager); }); @@ -78,7 +372,7 @@ namespace OpenIddict.Tests { // Assert Assert.NotNull(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()), Times.Once()); } } } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index 5dfb2e7f..b0caca19 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -133,6 +133,8 @@ namespace OpenIddict.Tests { ticket.SetScopes(request.GetScopes()); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70"); + return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); } @@ -166,6 +168,16 @@ namespace OpenIddict.Tests { return manager.Object; } + private static OpenIddictAuthorizationManager CreateAuthorizationManager(Action>> setup = null) { + var manager = new Mock>( + Mock.Of>(), + Mock.Of>>()); + + setup?.Invoke(manager); + + return manager.Object; + } + private static OpenIddictTokenManager CreateTokenManager(Action>> setup = null) { var manager = new Mock>( Mock.Of>(),