From f41c3db4c949f2ea2b7c8d05d208a97af025b3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 16 May 2018 01:40:39 +0200 Subject: [PATCH] Introduce MongoDB stores --- OpenIddict.sln | 16 +- build/dependencies.props | 1 + .../OpenIddict.MongoDb.Models.csproj | 20 + .../OpenIddictApplication.cs | 91 ++ .../OpenIddictAuthorization.cs | 68 ++ .../OpenIddictScope.cs | 64 ++ .../OpenIddictToken.cs | 98 ++ .../IOpenIddictMongoDbContext.cs | 27 + .../OpenIddict.MongoDb.csproj | 26 + .../OpenIddictMongoDbBuilder.cs | 184 ++++ .../OpenIddictMongoDbContext.cs | 108 +++ .../OpenIddictMongoDbExtensions.cs | 81 ++ .../OpenIddictMongoDbOptions.cs | 42 + .../OpenIddictApplicationStoreResolver.cs | 56 ++ .../OpenIddictAuthorizationStoreResolver.cs | 56 ++ .../Resolvers/OpenIddictScopeStoreResolver.cs | 56 ++ .../Resolvers/OpenIddictTokenStoreResolver.cs | 56 ++ .../Stores/OpenIddictApplicationStore.cs | 813 ++++++++++++++++ .../Stores/OpenIddictAuthorizationStore.cs | 764 +++++++++++++++ .../Stores/OpenIddictScopeStore.cs | 610 ++++++++++++ .../Stores/OpenIddictTokenStore.cs | 874 ++++++++++++++++++ 21 files changed, 4110 insertions(+), 1 deletion(-) create mode 100644 src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj create mode 100644 src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs create mode 100644 src/OpenIddict.MongoDb.Models/OpenIddictAuthorization.cs create mode 100644 src/OpenIddict.MongoDb.Models/OpenIddictScope.cs create mode 100644 src/OpenIddict.MongoDb.Models/OpenIddictToken.cs create mode 100644 src/OpenIddict.MongoDb/IOpenIddictMongoDbContext.cs create mode 100644 src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj create mode 100644 src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs create mode 100644 src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs create mode 100644 src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs create mode 100644 src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs create mode 100644 src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs create mode 100644 src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs create mode 100644 src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs create mode 100644 src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs create mode 100644 src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs create mode 100644 src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs create mode 100644 src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs create mode 100644 src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs diff --git a/OpenIddict.sln b/OpenIddict.sln index 98a99180..d58b859f 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -50,12 +50,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Abstractions", "src\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj", "{886A16DA-C9CF-4979-9B38-D06DF8A714B6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation", "src\OpenIddict.Validation\OpenIddict.Validation.csproj", "{6AB8F9E7-47F8-4A40-837F-C8753362AF54}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkCore.Models", "src\OpenIddict.EntityFrameworkCore.Models\OpenIddict.EntityFrameworkCore.Models.csproj", "{B5371534-4C33-41FA-B3D3-7D70D632DB15}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.MongoDb", "src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj", "{BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.MongoDb.Models", "src\OpenIddict.MongoDb.Models\OpenIddict.MongoDb.Models.csproj", "{14C55FB6-9626-4BDE-8961-3BE91DDD6418}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +138,14 @@ Global {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Release|Any CPU.Build.0 = Release|Any CPU + {BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}.Release|Any CPU.Build.0 = Release|Any CPU + {14C55FB6-9626-4BDE-8961-3BE91DDD6418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14C55FB6-9626-4BDE-8961-3BE91DDD6418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14C55FB6-9626-4BDE-8961-3BE91DDD6418}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14C55FB6-9626-4BDE-8961-3BE91DDD6418}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -157,6 +169,8 @@ Global {F470E734-F4B6-4355-AF32-53412B619E41} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {6AB8F9E7-47F8-4A40-837F-C8753362AF54} = {D544447C-D701-46BB-9A5B-C76C612A596B} {B5371534-4C33-41FA-B3D3-7D70D632DB15} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {14C55FB6-9626-4BDE-8961-3BE91DDD6418} = {D544447C-D701-46BB-9A5B-C76C612A596B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} diff --git a/build/dependencies.props b/build/dependencies.props index cedd715a..98ef196b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -12,6 +12,7 @@ 10.0.2 1.0.1 1.4.0 + 2.6.1 4.7.63 2.0.0 2.0.0 diff --git a/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj new file mode 100644 index 00000000..bc3249e2 --- /dev/null +++ b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj @@ -0,0 +1,20 @@ + + + + + + netstandard2.0 + false + + + + Document-oriented entities for the MongoDB stores. + Kévin Chalet + aspnetcore;authentication;jwt;openidconnect;openiddict;security + + + + + + + diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs b/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs new file mode 100644 index 00000000..b7a16baf --- /dev/null +++ b/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace OpenIddict.MongoDb.Models +{ + /// + /// Represents an OpenIddict application. + /// + public class OpenIddictApplication + { + /// + /// Gets or sets the client identifier + /// associated with the current application. + /// + [BsonElement("client_id"), BsonRequired] + public virtual string ClientId { get; set; } + + /// + /// Gets or sets the client secret associated with the current application. + /// Note: depending on the application manager used to create this instance, + /// this property may be hashed or encrypted for security reasons. + /// + [BsonElement("client_secret"), BsonIgnoreIfNull] + public virtual string ClientSecret { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + [BsonElement("concurrency_token"), BsonRequired] + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the consent type + /// associated with the current application. + /// + [BsonElement("consent_type"), BsonIgnoreIfNull] + public virtual string ConsentType { get; set; } + + /// + /// Gets or sets the display name + /// associated with the current application. + /// + [BsonElement("display_name"), BsonIgnoreIfNull] + public virtual string DisplayName { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current application. + /// + [BsonId, BsonRequired] + public virtual ObjectId Id { get; set; } + + /// + /// Gets or sets the permissions associated with the current application. + /// + [BsonElement("permissions"), BsonIgnoreIfDefault] + public virtual string[] Permissions { get; set; } = Array.Empty(); + + /// + /// Gets or sets the logout callback URLs associated with the current application. + /// + [BsonElement("post_logout_redirect_uris"), BsonIgnoreIfDefault] + public virtual string[] PostLogoutRedirectUris { get; set; } = Array.Empty(); + + /// + /// Gets or sets the additional properties associated with the current application. + /// + [BsonExtraElements] + public virtual BsonDocument Properties { get; set; } = new BsonDocument(); + + /// + /// Gets or sets the callback URLs associated with the current application. + /// + [BsonElement("redirect_uris"), BsonIgnoreIfDefault] + public virtual string[] RedirectUris { get; set; } = Array.Empty(); + + /// + /// Gets or sets the application type + /// associated with the current application. + /// + [BsonElement("type"), BsonRequired] + public virtual string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictAuthorization.cs b/src/OpenIddict.MongoDb.Models/OpenIddictAuthorization.cs new file mode 100644 index 00000000..5dec7b5f --- /dev/null +++ b/src/OpenIddict.MongoDb.Models/OpenIddictAuthorization.cs @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace OpenIddict.MongoDb.Models +{ + /// + /// Represents an OpenIddict authorization. + /// + public class OpenIddictAuthorization + { + /// + /// Gets or sets the identifier of the application + /// associated with the current authorization. + /// + [BsonElement("application_id"), BsonIgnoreIfDefault] + public virtual ObjectId ApplicationId { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + [BsonElement("concurrency_token"), BsonRequired] + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the unique identifier + /// associated with the current authorization. + /// + [BsonId, BsonRequired] + public virtual ObjectId Id { get; set; } + + /// + /// Gets or sets the additional properties associated with the current authorization. + /// + [BsonExtraElements] + public virtual BsonDocument Properties { get; set; } = new BsonDocument(); + + /// + /// Gets or sets the scopes associated with the current authorization. + /// + [BsonElement("scopes"), BsonIgnoreIfDefault] + public virtual string[] Scopes { get; set; } = Array.Empty(); + + /// + /// Gets or sets the status of the current authorization. + /// + [BsonElement("status"), BsonRequired] + public virtual string Status { get; set; } + + /// + /// Gets or sets the subject associated with the current authorization. + /// + [BsonElement("subject"), BsonRequired] + public virtual string Subject { get; set; } + + /// + /// Gets or sets the type of the current authorization. + /// + [BsonElement("type"), BsonRequired] + public virtual string Type { get; set; } + } +} diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictScope.cs b/src/OpenIddict.MongoDb.Models/OpenIddictScope.cs new file mode 100644 index 00000000..2f6590d1 --- /dev/null +++ b/src/OpenIddict.MongoDb.Models/OpenIddictScope.cs @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace OpenIddict.MongoDb.Models +{ + /// + /// Represents an OpenIddict scope. + /// + public class OpenIddictScope + { + /// + /// Gets or sets the concurrency token. + /// + [BsonElement("concurrency_token"), BsonRequired] + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the public description + /// associated with the current scope. + /// + [BsonElement("description"), BsonIgnoreIfNull] + public virtual string Description { get; set; } + + /// + /// Gets or sets the display name + /// associated with the current scope. + /// + [BsonElement("display_name"), BsonIgnoreIfNull] + public virtual string DisplayName { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current scope. + /// + [BsonId, BsonRequired] + public virtual ObjectId Id { get; set; } + + /// + /// Gets or sets the unique name + /// associated with the current scope. + /// + [BsonElement("name"), BsonRequired] + public virtual string Name { get; set; } + + /// + /// Gets or sets the additional properties associated with the current scope. + /// + [BsonExtraElements] + public virtual BsonDocument Properties { get; set; } = new BsonDocument(); + + /// + /// Gets or sets the resources associated with the current scope. + /// + [BsonElement("resources"), BsonIgnoreIfDefault] + public virtual string[] Resources { get; set; } = Array.Empty(); + } +} diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictToken.cs b/src/OpenIddict.MongoDb.Models/OpenIddictToken.cs new file mode 100644 index 00000000..de8f6d37 --- /dev/null +++ b/src/OpenIddict.MongoDb.Models/OpenIddictToken.cs @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace OpenIddict.MongoDb.Models +{ + /// + /// Represents an OpenIddict token. + /// + public class OpenIddictToken + { + /// + /// Gets or sets the identifier of the application associated with the current token. + /// + [BsonElement("application_id"), BsonIgnoreIfDefault] + public virtual ObjectId ApplicationId { get; set; } + + /// + /// Gets or sets the identifier of the authorization associated with the current token. + /// + [BsonElement("authorization_id"), BsonIgnoreIfDefault] + public virtual ObjectId AuthorizationId { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + [BsonElement("concurrency_token"), BsonRequired] + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the date on which the token + /// will start to be considered valid. + /// + [BsonElement("creation_date"), BsonIgnoreIfNull] + public virtual DateTime? CreationDate { get; set; } + + /// + /// Gets or sets the date on which the token + /// will no longer be considered valid. + /// + [BsonElement("expiration_date"), BsonIgnoreIfNull] + public virtual DateTime? ExpirationDate { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current token. + /// + [BsonId, BsonRequired] + public virtual ObjectId Id { get; set; } + + /// + /// Gets or sets the payload of the current token, if applicable. + /// Note: this property is only used for reference tokens + /// and may be encrypted for security reasons. + /// + [BsonElement("payload"), BsonIgnoreIfNull] + public virtual string Payload { get; set; } + + /// + /// Gets or sets the additional properties associated with the current token. + /// + [BsonExtraElements] + public virtual BsonDocument Properties { get; set; } = new BsonDocument(); + + /// + /// Gets or sets the reference identifier associated + /// with the current token, if applicable. + /// Note: this property is only used for reference tokens + /// and may be hashed or encrypted for security reasons. + /// + [BsonElement("reference_id"), BsonIgnoreIfNull] + public virtual string ReferenceId { get; set; } + + /// + /// Gets or sets the status of the current token. + /// + [BsonElement("status"), BsonRequired] + public virtual string Status { get; set; } + + /// + /// Gets or sets the subject associated with the current token. + /// + [BsonElement("subject"), BsonRequired] + public virtual string Subject { get; set; } + + /// + /// Gets or sets the type of the current token. + /// + [BsonElement("type"), BsonRequired] + public virtual string Type { get; set; } + } +} diff --git a/src/OpenIddict.MongoDb/IOpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/IOpenIddictMongoDbContext.cs new file mode 100644 index 00000000..f2bd207f --- /dev/null +++ b/src/OpenIddict.MongoDb/IOpenIddictMongoDbContext.cs @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace OpenIddict.MongoDb +{ + /// + /// Exposes the MongoDB database used by the OpenIddict stores. + /// + public interface IOpenIddictMongoDbContext + { + /// + /// Gets the . + /// + /// + /// A that can be used to monitor the + /// asynchronous operation, whose result returns the MongoDB database. + /// + ValueTask GetDatabaseAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj new file mode 100644 index 00000000..fa1cb183 --- /dev/null +++ b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj @@ -0,0 +1,26 @@ + + + + + + netstandard2.0 + false + + + + MongoDB stores for OpenIddict. + Kévin Chalet + aspnetcore;authentication;jwt;openidconnect;openiddict;security + + + + + + + + + + + + + diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs new file mode 100644 index 00000000..7a6a41a6 --- /dev/null +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs @@ -0,0 +1,184 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using JetBrains.Annotations; +using MongoDB.Driver; +using OpenIddict.Core; +using OpenIddict.MongoDb; +using OpenIddict.MongoDb.Models; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes the necessary methods required to configure the OpenIddict MongoDB services. + /// + public class OpenIddictMongoDbBuilder + { + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictMongoDbBuilder([NotNull] IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + Services = services; + } + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict MongoDB configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictMongoDbBuilder Configure([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Configures the MongoDB stores to use the specified database + /// instead of retrieving it from the dependency injection container. + /// + /// The . + /// The . + public OpenIddictMongoDbBuilder UseDatabase([NotNull] IMongoDatabase database) + { + if (database == null) + { + throw new ArgumentNullException(nameof(database)); + } + + return Configure(options => options.Database = database); + } + + /// + /// Configures OpenIddict to use the specified entity as the default application entity. + /// + /// The . + public OpenIddictMongoDbBuilder ReplaceDefaultApplicationEntity() + where TApplication : OpenIddictApplication, new() + { + Services.Configure(options => options.DefaultApplicationType = typeof(TApplication)); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default authorization entity. + /// + /// The . + public OpenIddictMongoDbBuilder ReplaceDefaultAuthorizationEntity() + where TAuthorization : OpenIddictAuthorization, new() + { + Services.Configure(options => options.DefaultAuthorizationType = typeof(TAuthorization)); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default scope entity. + /// + /// The . + public OpenIddictMongoDbBuilder ReplaceDefaultScopeEntity() + where TScope : OpenIddictScope, new() + { + Services.Configure(options => options.DefaultScopeType = typeof(TScope)); + + return this; + } + + /// + /// Configures OpenIddict to use the specified entity as the default token entity. + /// + /// The . + public OpenIddictMongoDbBuilder ReplaceDefaultTokenEntity() + where TToken : OpenIddictToken, new() + { + Services.Configure(options => options.DefaultTokenType = typeof(TToken)); + + return this; + } + + /// + /// Replaces the default applications collection name (by default, openiddict.applications). + /// + /// The collection name + /// The . + public OpenIddictMongoDbBuilder SetApplicationsCollectionName([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.ApplicationsCollectionName = name); + } + + /// + /// Replaces the default authorizations collection name (by default, openiddict.authorizations). + /// + /// The collection name + /// The . + public OpenIddictMongoDbBuilder SetAuthorizationsCollectionName([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.AuthorizationsCollectionName = name); + } + + /// + /// Replaces the default scopes collection name (by default, openiddict.scopes). + /// + /// The collection name + /// The . + public OpenIddictMongoDbBuilder SetScopesCollectionName([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.ScopesCollectionName = name); + } + + /// + /// Replaces the default tokens collection name (by default, openiddict.tokens). + /// + /// The collection name + /// The . + public OpenIddictMongoDbBuilder SetTokensCollectionName([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The collection name cannot be null or empty.", nameof(name)); + } + + return Configure(options => options.TokensCollectionName = name); + } + } +} diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs new file mode 100644 index 00000000..de27f63d --- /dev/null +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Exposes the MongoDB database used by the OpenIddict stores. + /// + public class OpenIddictMongoDbContext : IOpenIddictMongoDbContext + { + private readonly IOptionsMonitor _options; + private readonly IServiceProvider _provider; + private IMongoDatabase _database; + + public OpenIddictMongoDbContext( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) + { + _options = options; + _provider = provider; + } + + /// + /// Gets the . + /// + /// + /// A that can be used to monitor the + /// asynchronous operation, whose result returns the MongoDB database. + /// + public async ValueTask GetDatabaseAsync(CancellationToken cancellationToken) + { + if (_database != null) + { + return _database; + } + + var options = _options.CurrentValue; + if (options == null) + { + throw new InvalidOperationException("The OpenIddict MongoDB options cannot be retrieved."); + } + + var database = options.Database; + if (database == null) + { + database = _provider.GetService(); + } + + if (database == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No suitable MongoDB database service can be found.") + .Append("To configure the OpenIddict MongoDB stores to use a specific database, use ") + .Append("'services.AddOpenIddict().AddCore().UseMongoDb().UseDatabase()' or register an ") + .Append("'IMongoDatabase' in the dependency injection container in 'ConfigureServices()'.") + .ToString()); + } + + // Note: the cancellation token passed as a parameter is deliberately not used here to ensure + // the cancellation of a single store operation doesn't prevent the indexes from being created. + var applications = database.GetCollection(options.ApplicationsCollectionName); + await applications.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(application => application.ClientId), + new CreateIndexOptions + { + Unique = true + }); + + await applications.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris)); + + await applications.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(application => application.RedirectUris)); + + var scopes = database.GetCollection(options.ScopesCollectionName); + await scopes.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(scope => scope.Name), + new CreateIndexOptions + { + Unique = true + }); + + var tokens = database.GetCollection(options.TokensCollectionName); + await tokens.Indexes.CreateOneAsync( + Builders.IndexKeys.Ascending(token => token.ReferenceId), + new CreateIndexOptions + { + PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId), + Unique = true + }); + + return _database = database; + } + } +} diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs new file mode 100644 index 00000000..e54c4c1a --- /dev/null +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenIddict.MongoDb; +using OpenIddict.MongoDb.Models; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OpenIddictMongoDbExtensions + { + /// + /// Registers the MongoDB stores services in the DI container and + /// configures OpenIddict to use the MongoDB entities by default. + /// + /// The services builder used by OpenIddict to register new services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictMongoDbBuilder UseMongoDb([NotNull] this OpenIddictCoreBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + + // Note: the Mongo stores/resolvers don't depend on scoped/transient services and thus + // can be safely registered as singleton services and shared/reused across requests. + builder.ReplaceApplicationStoreResolver(ServiceLifetime.Singleton) + .ReplaceAuthorizationStoreResolver(ServiceLifetime.Singleton) + .ReplaceScopeStoreResolver(ServiceLifetime.Singleton) + .ReplaceTokenStoreResolver(ServiceLifetime.Singleton); + + builder.Services.TryAddSingleton(typeof(OpenIddictApplicationStore<>)); + builder.Services.TryAddSingleton(typeof(OpenIddictAuthorizationStore<>)); + builder.Services.TryAddSingleton(typeof(OpenIddictScopeStore<>)); + builder.Services.TryAddSingleton(typeof(OpenIddictTokenStore<>)); + + builder.Services.TryAddSingleton(); + + return new OpenIddictMongoDbBuilder(builder.Services); + } + + /// + /// Registers the MongoDB stores services in the DI container and + /// configures OpenIddict to use the MongoDB entities by default. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the MongoDB services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictCoreBuilder UseMongoDb( + [NotNull] this OpenIddictCoreBuilder builder, + [NotNull] Action configuration) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseMongoDb()); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs new file mode 100644 index 00000000..d8abbec3 --- /dev/null +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using MongoDB.Driver; + +namespace OpenIddict.MongoDb +{ + /// + /// Provides various settings needed to configure the OpenIddict MongoDB integration. + /// + public class OpenIddictMongoDbOptions + { + /// + /// Gets or sets the name of the applications collection (by default, openiddict.applications). + /// + public string ApplicationsCollectionName { get; set; } = "openiddict.applications"; + + /// + /// Gets or sets the name of the authorizations collection (by default, openiddict.authorizations). + /// + public string AuthorizationsCollectionName { get; set; } = "openiddict.authorizations"; + + /// + /// Gets or sets the used by the OpenIddict stores. + /// If no value is explicitly set, the database is resolved from the DI container. + /// + public IMongoDatabase Database { get; set; } + + /// + /// Gets or sets the name of the scopes collection (by default, openiddict.scopes). + /// + public string ScopesCollectionName { get; set; } = "openiddict.scopes"; + + /// + /// Gets or sets the name of the tokens collection (by default, openiddict.tokens). + /// + public string TokensCollectionName { get; set; } = "openiddict.tokens"; + } +} diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs new file mode 100644 index 00000000..af6e765e --- /dev/null +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Exposes a method allowing to resolve an application store. + /// + public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver + { + private readonly IServiceProvider _provider; + + public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) + { + _provider = provider; + } + + /// + /// Returns an application store compatible with the specified application type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Application entity. + /// An . + public IOpenIddictApplicationStore Get() where TApplication : class + { + var store = _provider.GetService>(); + if (store != null) + { + return store; + } + + if (!typeof(OpenIddictApplication).IsAssignableFrom(typeof(TApplication))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified application type is not compatible with the MongoDB stores.") + .Append("When enabling the MongoDB stores, make sure you use the built-in 'OpenIddictApplication' ") + .Append("entity (from the 'OpenIddict.MongoDb.Models' package) or a custom entity ") + .Append("that inherits from the 'OpenIddictApplication' entity.") + .ToString()); + } + + return (IOpenIddictApplicationStore) _provider.GetRequiredService( + typeof(OpenIddictApplicationStore<>).MakeGenericType(typeof(TApplication))); + } + } +} diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs new file mode 100644 index 00000000..c449eb12 --- /dev/null +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Exposes a method allowing to resolve an authorization store. + /// + public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver + { + private readonly IServiceProvider _provider; + + public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) + { + _provider = provider; + } + + /// + /// Returns an authorization store compatible with the specified authorization type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Authorization entity. + /// An . + public IOpenIddictAuthorizationStore Get() where TAuthorization : class + { + var store = _provider.GetService>(); + if (store != null) + { + return store; + } + + if (!typeof(OpenIddictAuthorization).IsAssignableFrom(typeof(TAuthorization))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified authorization type is not compatible with the MongoDB stores.") + .Append("When enabling the MongoDB stores, make sure you use the built-in 'OpenIddictAuthorization' ") + .Append("entity (from the 'OpenIddict.MongoDb.Models' package) or a custom entity ") + .Append("that inherits from the 'OpenIddictAuthorization' entity.") + .ToString()); + } + + return (IOpenIddictAuthorizationStore) _provider.GetRequiredService( + typeof(OpenIddictAuthorizationStore<>).MakeGenericType(typeof(TAuthorization))); + } + } +} diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs new file mode 100644 index 00000000..f2674248 --- /dev/null +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Exposes a method allowing to resolve a scope store. + /// + public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver + { + private readonly IServiceProvider _provider; + + public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) + { + _provider = provider; + } + + /// + /// Returns a scope store compatible with the specified scope type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Scope entity. + /// An . + public IOpenIddictScopeStore Get() where TScope : class + { + var store = _provider.GetService>(); + if (store != null) + { + return store; + } + + if (!typeof(OpenIddictScope).IsAssignableFrom(typeof(TScope))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified scope type is not compatible with the MongoDB stores.") + .Append("When enabling the MongoDB stores, make sure you use the built-in 'OpenIddictScope' ") + .Append("entity (from the 'OpenIddict.MongoDb.Models' package) or a custom entity ") + .Append("that inherits from the 'OpenIddictScope' entity.") + .ToString()); + } + + return (IOpenIddictScopeStore) _provider.GetRequiredService( + typeof(OpenIddictScopeStore<>).MakeGenericType(typeof(TScope))); + } + } +} diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs new file mode 100644 index 00000000..fe99d150 --- /dev/null +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Exposes a method allowing to resolve a token store. + /// + public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver + { + private readonly IServiceProvider _provider; + + public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) + { + _provider = provider; + } + + /// + /// Returns a token store compatible with the specified token type or throws an + /// if no store can be built using the specified type. + /// + /// The type of the Token entity. + /// An . + public IOpenIddictTokenStore Get() where TToken : class + { + var store = _provider.GetService>(); + if (store != null) + { + return store; + } + + if (!typeof(OpenIddictToken).IsAssignableFrom(typeof(TToken))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified token type is not compatible with the MongoDB stores.") + .Append("When enabling the MongoDB stores, make sure you use the built-in 'OpenIddictToken' ") + .Append("entity (from the 'OpenIddict.MongoDb.Models' package) or a custom entity ") + .Append("that inherits from the 'OpenIddictToken' entity.") + .ToString()); + } + + return (IOpenIddictTokenStore) _provider.GetRequiredService( + typeof(OpenIddictTokenStore<>).MakeGenericType(typeof(TToken))); + } + } +} diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs new file mode 100644 index 00000000..db2f49c3 --- /dev/null +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs @@ -0,0 +1,813 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Provides methods allowing to manage the applications stored in a database. + /// + /// The type of the Application entity. + public class OpenIddictApplicationStore : IOpenIddictApplicationStore + where TApplication : OpenIddictApplication, new() + { + public OpenIddictApplicationStore( + [NotNull] IMemoryCache cache, + [NotNull] IOpenIddictMongoDbContext context, + [NotNull] IOptionsMonitor options) + { + Cache = cache; + Context = context; + Options = options; + } + + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictMongoDbContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + /// Determines the number of applications that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications in the database. + /// + public virtual async Task CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return await collection.CountAsync(FilterDefinition.Empty); + } + + /// + /// Determines the number of applications that match the specified query. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications that match the specified query. + /// + public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable())).LongCountAsync(); + } + + /// + /// Creates a new application. + /// + /// 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) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + await collection.InsertOneAsync(application, null, cancellationToken); + } + + /// + /// Removes an existing application. + /// + /// The application to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + if ((await collection.DeleteOneAsync(entity => + entity.Id == application.Id && + entity.ConcurrencyToken == application.ConcurrencyToken)).DeletedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The application was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the application from the database and retry the operation.") + .ToString()); + } + + // Delete the authorizations associated with the application. + await database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName) + .DeleteManyAsync(authorization => authorization.ApplicationId == application.Id, cancellationToken); + + // Delete the tokens associated with the application. + await database.GetCollection(Options.CurrentValue.TokensCollectionName) + .DeleteManyAsync(token => token.ApplicationId == application.Id, cancellationToken); + } + + /// + /// Retrieves an application using its client identifier. + /// + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + public virtual async Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return await collection.Find(application => application.ClientId == identifier).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return await collection.Find(application => application.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves all the applications associated with the specified post_logout_redirect_uri. + /// + /// The post_logout_redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified post_logout_redirect_uri. + /// + public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(application => + application.PostLogoutRedirectUris.Contains(address)).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified redirect_uri. + /// + public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(application => + application.RedirectUris.Contains(address)).ToListAsync(cancellationToken)); + } + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + public virtual async Task GetAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the client identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client identifier associated with the application. + /// + public virtual ValueTask GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ClientId); + } + + /// + /// Retrieves the client secret associated with an application. + /// Note: depending on the manager used to create the application, + /// the client secret may be hashed for security reasons. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client secret associated with the application. + /// + public virtual ValueTask GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ClientSecret); + } + + /// + /// Retrieves the client type associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client type of the application (by default, "public"). + /// + public virtual ValueTask GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.Type); + } + + /// + /// Retrieves the consent type 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 consent type of the application (by default, "explicit"). + /// + public virtual ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ConsentType); + } + + /// + /// Retrieves the display name 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 display name associated with the application. + /// + public virtual ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.DisplayName); + } + + /// + /// 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 ValueTask GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.Id.ToString()); + } + + /// + /// Retrieves the permissions 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 all the permissions associated with the application. + /// + public virtual ValueTask> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.Permissions == null || application.Permissions.Length == 0) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(application.Permissions.ToImmutableArray()); + } + + /// + /// Retrieves the logout callback addresses associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the post_logout_redirect_uri associated with the application. + /// + public virtual ValueTask> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.PostLogoutRedirectUris == null || application.PostLogoutRedirectUris.Length == 0) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(application.PostLogoutRedirectUris.ToImmutableArray()); + } + + /// + /// Retrieves the additional properties 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 all the additional properties associated with the application. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.Properties == null) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.FromObject(application.Properties.ToDictionary())); + } + + /// + /// Retrieves the callback addresses associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the redirect_uri associated with the application. + /// + public virtual ValueTask> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.RedirectUris == null || application.RedirectUris.Length == 0) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(application.RedirectUris.ToImmutableArray()); + } + + /// + /// Instantiates a new application. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated application, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TApplication()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + var query = (IMongoQueryable) collection.AsQueryable().OrderBy(application => application.Id); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + return ImmutableArray.CreateRange( + await ((IMongoQueryable) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken)); + } + + /// + /// Sets the client identifier associated with an application. + /// + /// The application. + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientIdAsync([NotNull] TApplication application, + [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientId = identifier; + + return Task.CompletedTask; + } + + /// + /// Sets the client secret associated with an application. + /// Note: depending on the manager used to create the application, + /// the client secret may be hashed for security reasons. + /// + /// The application. + /// The client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientSecretAsync([NotNull] TApplication application, + [CanBeNull] string secret, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientSecret = secret; + + return Task.CompletedTask; + } + + /// + /// Sets the client type associated with an application. + /// + /// The application. + /// The client type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientTypeAsync([NotNull] TApplication application, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.Type = type; + + return Task.CompletedTask; + } + + /// + /// Sets the consent type associated with an application. + /// + /// The application. + /// The consent type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetConsentTypeAsync([NotNull] TApplication application, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ConsentType = type; + + return Task.CompletedTask; + } + + /// + /// Sets the display name associated with an application. + /// + /// The application. + /// The display name associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDisplayNameAsync([NotNull] TApplication application, + [CanBeNull] string name, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.DisplayName = name; + + return Task.CompletedTask; + } + + /// + /// Sets the permissions associated with an application. + /// + /// The application. + /// The permissions associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray permissions, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (permissions.IsDefaultOrEmpty) + { + application.Permissions = null; + + return Task.CompletedTask; + } + + application.Permissions = permissions.ToArray(); + + return Task.CompletedTask; + } + + /// + /// Sets the logout callback addresses associated with an application. + /// + /// The application. + /// The logout callback addresses associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.PostLogoutRedirectUris = null; + + return Task.CompletedTask; + } + + application.PostLogoutRedirectUris = addresses.ToArray(); + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with an application. + /// + /// The application. + /// The additional properties associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TApplication application, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (properties == null) + { + application.Properties = null; + + return Task.CompletedTask; + } + + application.Properties = new BsonDocument(properties.ToObject>()); + + return Task.CompletedTask; + } + + /// + /// Sets the callback addresses associated with an application. + /// + /// The application. + /// The callback addresses associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetRedirectUrisAsync([NotNull] TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.RedirectUris = null; + + return Task.CompletedTask; + } + + application.RedirectUris = addresses.ToArray(); + + return Task.CompletedTask; + } + + /// + /// Updates an existing application. + /// + /// The application 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] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + // Generate a new concurrency token and attach it + // to the application before persisting the changes. + application.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); + + if ((await collection.ReplaceOneAsync(entity => + entity.Id == application.Id && + entity.ConcurrencyToken == application.ConcurrencyToken, application, null, cancellationToken)).MatchedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The application was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the application from the database and retry the operation.") + .ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs new file mode 100644 index 00000000..34321262 --- /dev/null +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs @@ -0,0 +1,764 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Provides methods allowing to manage the authorizations stored in a database. + /// + /// The type of the Authorization entity. + public class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore + where TAuthorization : OpenIddictAuthorization, new() + { + public OpenIddictAuthorizationStore( + [NotNull] IMemoryCache cache, + [NotNull] IOpenIddictMongoDbContext context, + [NotNull] IOptionsMonitor options) + { + Cache = cache; + Context = context; + Options = options; + } + + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictMongoDbContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + /// Determines the number of authorizations that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of authorizations in the database. + /// + public virtual async Task CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return await collection.CountAsync(FilterDefinition.Empty); + } + + /// + /// Determines the number of authorizations that match the specified query. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of authorizations that match the specified query. + /// + public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable())).LongCountAsync(); + } + + /// + /// 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. + /// + public virtual async Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + await collection.InsertOneAsync(authorization, null, cancellationToken); + } + + /// + /// Removes an existing authorization. + /// + /// The authorization to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + if ((await collection.DeleteOneAsync(entity => + entity.Id == authorization.Id && + entity.ConcurrencyToken == authorization.ConcurrencyToken)).DeletedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The authorization was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the authorization from the database and retry the operation.") + .ToString()); + } + + // Delete the tokens associated with the authorization. + await database.GetCollection(Options.CurrentValue.TokensCollectionName) + .DeleteManyAsync(token => token.AuthorizationId == authorization.Id, cancellationToken); + } + + /// + /// Retrieves the authorizations corresponding to the specified + /// subject and associated with the application identifier. + /// + /// 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 authorizations corresponding to the subject/client. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client cannot be null or empty.", nameof(client)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(authorization => + authorization.Subject == subject && + authorization.ApplicationId == ObjectId.Parse(client)).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the authorizations matching the specified parameters. + /// + /// The subject associated with the authorization. + /// The client associated with the authorization. + /// The authorization status. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorizations corresponding to the criteria. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(authorization => + authorization.Subject == subject && + authorization.ApplicationId == ObjectId.Parse(client) && + authorization.Status == status).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the authorizations matching the specified parameters. + /// + /// The subject associated with the authorization. + /// The client associated with the authorization. + /// The authorization status. + /// The authorization type. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorizations corresponding to the criteria. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The type cannot be null or empty.", nameof(type)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(authorization => + authorization.Subject == subject && + authorization.ApplicationId == ObjectId.Parse(client) && + authorization.Status == status && + authorization.Type == type).ToListAsync(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 async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return await collection.Find(authorization => authorization.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves all the authorizations corresponding to the specified subject. + /// + /// The subject 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 authorizations corresponding to the specified subject. + /// + public virtual async Task> FindBySubjectAsync( + [NotNull] string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(authorization => authorization.Subject == subject).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the optional application 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 application identifier associated with the authorization. + /// + public virtual ValueTask GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.ApplicationId.ToString()); + } + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + public virtual async Task GetAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(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 ValueTask GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Id.ToString()); + } + + /// + /// Retrieves the additional properties 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 all the additional properties associated with the authorization. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (authorization.Properties == null) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.FromObject(authorization.Properties.ToDictionary())); + } + + /// + /// Retrieves the scopes 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 scopes associated with the specified authorization. + /// + public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (authorization.Scopes == null || authorization.Scopes.Length == 0) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(authorization.Scopes.ToImmutableArray()); + } + + /// + /// Retrieves the status 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 status associated with the specified authorization. + /// + public virtual ValueTask GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Status); + } + + /// + /// 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 ValueTask GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Subject); + } + + /// + /// Retrieves the type 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 type associated with the specified authorization. + /// + public virtual ValueTask GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Type); + } + + /// + /// Instantiates a new authorization. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated authorization, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TAuthorization()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var query = (IMongoQueryable) collection.AsQueryable().OrderBy(authorization => authorization.Id); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return ImmutableArray.CreateRange( + await ((IMongoQueryable) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken)); + } + + /// + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task PruneAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + var identifiers = + await (from authorization in collection.AsQueryable() + join token in database.GetCollection(Options.CurrentValue.TokensCollectionName).AsQueryable() + on authorization.Id equals token.AuthorizationId into tokens + where authorization.Status != OpenIddictConstants.Statuses.Valid || + (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && + !tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid)) + orderby authorization.Id + select authorization.Id).ToListAsync(cancellationToken); + + await collection.DeleteManyAsync(authorization => identifiers.Contains(authorization.Id)); + } + + /// + /// Sets the application identifier associated with an authorization. + /// + /// The authorization. + /// 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 Task SetApplicationIdAsync([NotNull] TAuthorization authorization, + [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (!string.IsNullOrEmpty(identifier)) + { + authorization.ApplicationId = ObjectId.Parse(identifier); + } + + else + { + authorization.ApplicationId = ObjectId.Empty; + } + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with an authorization. + /// + /// The authorization. + /// The additional properties 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 Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (properties == null) + { + authorization.Properties = null; + + return Task.CompletedTask; + } + + authorization.Properties = new BsonDocument(properties.ToObject>()); + + return Task.CompletedTask; + } + + /// + /// Sets the scopes associated with an authorization. + /// + /// The authorization. + /// The scopes 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 Task SetScopesAsync([NotNull] TAuthorization authorization, + ImmutableArray scopes, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (scopes.IsDefaultOrEmpty) + { + authorization.Scopes = null; + + return Task.CompletedTask; + } + + authorization.Scopes = scopes.ToArray(); + + return Task.CompletedTask; + } + + /// + /// Sets the status associated with an authorization. + /// + /// The authorization. + /// The status 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 Task SetStatusAsync([NotNull] TAuthorization authorization, + [CanBeNull] string status, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Status = status; + + return Task.CompletedTask; + } + + /// + /// Sets the subject associated with an authorization. + /// + /// The authorization. + /// The subject 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 Task SetSubjectAsync([NotNull] TAuthorization authorization, + [CanBeNull] string subject, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Subject = subject; + + return Task.CompletedTask; + } + + /// + /// Sets the type associated with an authorization. + /// + /// The authorization. + /// The type 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 Task SetTypeAsync([NotNull] TAuthorization authorization, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Type = type; + + return Task.CompletedTask; + } + + /// + /// Updates an existing authorization. + /// + /// The authorization 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] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + // Generate a new concurrency token and attach it + // to the authorization before persisting the changes. + authorization.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + if ((await collection.ReplaceOneAsync(entity => + entity.Id == authorization.Id && + entity.ConcurrencyToken == authorization.ConcurrencyToken, authorization, null, cancellationToken)).MatchedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The authorization was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the authorization from the database and retry the operation.") + .ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs new file mode 100644 index 00000000..456e179a --- /dev/null +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs @@ -0,0 +1,610 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Provides methods allowing to manage the scopes stored in a database. + /// + /// The type of the Scope entity. + public class OpenIddictScopeStore : IOpenIddictScopeStore + where TScope : OpenIddictScope, new() + { + public OpenIddictScopeStore( + [NotNull] IMemoryCache cache, + [NotNull] IOpenIddictMongoDbContext context, + [NotNull] IOptionsMonitor options) + { + Cache = cache; + Context = context; + Options = options; + } + + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictMongoDbContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + /// Determines the number of scopes that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of scopes in the database. + /// + public virtual async Task CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return await collection.CountAsync(FilterDefinition.Empty); + } + + /// + /// Determines the number of scopes that match the specified query. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of scopes that match the specified query. + /// + public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable())).LongCountAsync(); + } + + /// + /// Creates a new scope. + /// + /// The scope 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] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + await collection.InsertOneAsync(scope, null, cancellationToken); + } + + /// + /// Removes an existing scope. + /// + /// The scope to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + if ((await collection.DeleteOneAsync(entity => + entity.Id == scope.Id && + entity.ConcurrencyToken == scope.ConcurrencyToken)).DeletedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The scope was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the scope from the database and retry the operation.") + .ToString()); + } + } + + /// + /// Retrieves a scope using its unique identifier. + /// + /// The unique identifier associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the identifier. + /// + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return await collection.Find(scope => scope.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves a scope using its name. + /// + /// The name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the specified name. + /// + public virtual async Task FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return await collection.Find(scope => scope.Name == name).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves a list of scopes using their name. + /// + /// The names associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes corresponding to the specified names. + /// + public virtual async Task> FindByNamesAsync( + ImmutableArray names, CancellationToken cancellationToken) + { + if (names.Any(name => string.IsNullOrEmpty(name))) + { + throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(scope => names.Contains(scope.Name)).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + public virtual async Task> FindByResourceAsync( + [NotNull] string resource, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(scope => scope.Resources.Contains(resource)).ToListAsync(cancellationToken)); + } + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + public virtual async Task GetAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the description associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the description associated with the specified scope. + /// + public virtual ValueTask GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Description); + } + + /// + /// Retrieves the display name associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the scope. + /// + public virtual ValueTask GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.DisplayName); + } + + /// + /// Retrieves the unique identifier associated with a scope. + /// + /// The scope. + /// 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 scope. + /// + public virtual ValueTask GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Id.ToString()); + } + + /// + /// Retrieves the name associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the name associated with the specified scope. + /// + public virtual ValueTask GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Name); + } + + /// + /// Retrieves the additional properties associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the scope. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.Properties == null) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.FromObject(scope.Properties.ToDictionary())); + } + + /// + /// Retrieves the resources associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the resources associated with the scope. + /// + public virtual ValueTask> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (scope.Resources == null || scope.Resources.Length == 0) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(scope.Resources.ToImmutableArray()); + } + + /// + /// Instantiates a new scope. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated scope, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TScope()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + var query = (IMongoQueryable) collection.AsQueryable().OrderBy(scope => scope.Id); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + return ImmutableArray.CreateRange( + await ((IMongoQueryable) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken)); + } + + /// + /// Sets the description associated with a scope. + /// + /// The scope. + /// The description 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 Task SetDescriptionAsync([NotNull] TScope scope, [CanBeNull] string description, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Description = description; + + return Task.CompletedTask; + } + + /// + /// Sets the display name associated with a scope. + /// + /// The scope. + /// The display name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDisplayNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.DisplayName = name; + + return Task.CompletedTask; + } + + /// + /// Sets the name associated with a scope. + /// + /// The scope. + /// The name 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 Task SetNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Name = name; + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with a scope. + /// + /// The scope. + /// The additional properties associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TScope scope, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (properties == null) + { + scope.Properties = null; + + return Task.CompletedTask; + } + + scope.Properties = new BsonDocument(properties.ToObject>()); + + return Task.CompletedTask; + } + + /// + /// Sets the resources associated with a scope. + /// + /// The scope. + /// The resources associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray resources, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (resources.IsDefaultOrEmpty) + { + scope.Resources = null; + + return Task.CompletedTask; + } + + scope.Resources = resources.ToArray(); + + return Task.CompletedTask; + } + + /// + /// Updates an existing scope. + /// + /// The scope 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] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + // Generate a new concurrency token and attach it + // to the scope before persisting the changes. + scope.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); + + if ((await collection.ReplaceOneAsync(entity => + entity.Id == scope.Id && + entity.ConcurrencyToken == scope.ConcurrencyToken, scope, null, cancellationToken)).MatchedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The scope was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the scope from the database and retry the operation.") + .ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs new file mode 100644 index 00000000..49208e12 --- /dev/null +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs @@ -0,0 +1,874 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb.Models; + +namespace OpenIddict.MongoDb +{ + /// + /// Provides methods allowing to manage the tokens stored in a database. + /// + /// The type of the Token entity. + public class OpenIddictTokenStore : IOpenIddictTokenStore + where TToken : OpenIddictToken, new() + { + public OpenIddictTokenStore( + [NotNull] IMemoryCache cache, + [NotNull] IOpenIddictMongoDbContext context, + [NotNull] IOptionsMonitor options) + { + Cache = cache; + Context = context; + Options = options; + } + + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + + /// + /// Gets the database context associated with the current store. + /// + protected IOpenIddictMongoDbContext Context { get; } + + /// + /// Gets the options associated with the current store. + /// + protected IOptionsMonitor Options { get; } + + /// + /// Determines the number of tokens that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications in the database. + /// + public virtual async Task CountAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return await collection.CountAsync(FilterDefinition.Empty); + } + + /// + /// Determines the number of tokens that match the specified query. + /// + /// The result type. + /// The query to execute. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of tokens that match the specified query. + /// + public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable())).LongCountAsync(); + } + + /// + /// 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. + /// + public virtual async Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + await collection.InsertOneAsync(token, null, cancellationToken); + } + + /// + /// Removes a token. + /// + /// The token to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + if ((await collection.DeleteOneAsync(entity => + entity.Id == token.Id && + entity.ConcurrencyToken == token.ConcurrencyToken)).DeletedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The token was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the token from the database and retry the operation.") + .ToString()); + } + } + + /// + /// Retrieves the list of tokens corresponding to the specified application identifier. + /// + /// The application identifier 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 application. + /// + public virtual async Task> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(token => token.ApplicationId == ObjectId.Parse(identifier)).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the list of tokens corresponding to the specified authorization identifier. + /// + /// The authorization identifier 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 authorization. + /// + public virtual async Task> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(token => token.AuthorizationId == ObjectId.Parse(identifier)).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves a token using its unique identifier. + /// + /// The unique identifier 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 corresponding to the unique identifier. + /// + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return await collection.Find(token => token.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the list of tokens corresponding to the specified reference identifier. + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// + /// The reference identifier 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 reference identifier. + /// + public virtual async Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return await collection.Find(token => token.ReferenceId == identifier).FirstOrDefaultAsync(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 async Task> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(token => token.Subject == subject).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the optional application 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 application identifier associated with the token. + /// + public virtual ValueTask GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ApplicationId.ToString()); + } + + /// + /// Executes the specified query and returns the first element. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the first element returned when executing the query. + /// + public virtual async Task GetAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return await ((IMongoQueryable) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the optional authorization 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 authorization identifier associated with the token. + /// + public virtual ValueTask GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.AuthorizationId.ToString()); + } + + /// + /// Retrieves the creation date 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 creation date associated with the specified token. + /// + public virtual ValueTask GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.CreationDate); + } + + /// + /// Retrieves the expiration date 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 expiration date associated with the specified token. + /// + public virtual ValueTask GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ExpirationDate); + } + + /// + /// 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 ValueTask GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Id.ToString()); + } + + /// + /// Retrieves the payload 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 payload associated with the specified token. + /// + public virtual ValueTask GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Payload); + } + + /// + /// Retrieves the additional properties 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 all the additional properties associated with the token. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (token.Properties == null) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.FromObject(token.Properties.ToDictionary())); + } + + /// + /// Retrieves the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. + /// + /// 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 reference identifier associated with the specified token. + /// + public virtual ValueTask GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ReferenceId); + } + + /// + /// Retrieves the status 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 status associated with the specified token. + /// + public virtual ValueTask GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Status); + } + + /// + /// 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 ValueTask GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Subject); + } + + /// + /// 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 ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Type); + } + + /// + /// Instantiates a new token. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated token, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TToken()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + var query = (IMongoQueryable) collection.AsQueryable().OrderBy(token => token.Id); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The state type. + /// The result type. + /// The query to execute. + /// The optional state. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [NotNull] Func, TState, IQueryable> query, + [CanBeNull] TState state, CancellationToken cancellationToken) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange( + await ((IMongoQueryable) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken)); + } + + /// + /// Removes the tokens that are marked as expired or invalid. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task PruneAsync(CancellationToken cancellationToken) + { + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + await collection.DeleteManyAsync(token => token.ExpirationDate < DateTimeOffset.UtcNow || + token.Status != OpenIddictConstants.Statuses.Valid, cancellationToken); + } + + /// + /// Sets the application identifier 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 Task SetApplicationIdAsync([NotNull] TToken token, + [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (!string.IsNullOrEmpty(identifier)) + { + token.ApplicationId = ObjectId.Parse(identifier); + } + + else + { + token.ApplicationId = ObjectId.Empty; + } + + return Task.CompletedTask; + } + + /// + /// Sets the authorization identifier 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 Task SetAuthorizationIdAsync([NotNull] TToken token, + [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (!string.IsNullOrEmpty(identifier)) + { + token.AuthorizationId = ObjectId.Parse(identifier); + } + + else + { + token.AuthorizationId = ObjectId.Empty; + } + + return Task.CompletedTask; + } + + /// + /// Sets the creation date associated with a token. + /// + /// The token. + /// The creation date. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetCreationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.CreationDate = date?.UtcDateTime; + + return Task.CompletedTask; + } + + /// + /// Sets the expiration date associated with a token. + /// + /// The token. + /// The expiration date. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetExpirationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ExpirationDate = date?.UtcDateTime; + + return Task.CompletedTask; + } + + /// + /// Sets the payload associated with a token. + /// + /// The token. + /// The payload associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPayloadAsync([NotNull] TToken token, [CanBeNull] string payload, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Payload = payload; + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with a token. + /// + /// The token. + /// The additional properties associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (properties == null) + { + token.Properties = null; + + return Task.CompletedTask; + } + + token.Properties = new BsonDocument(properties.ToObject>()); + + return Task.CompletedTask; + } + + /// + /// Sets the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. + /// + /// The token. + /// The reference identifier associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetReferenceIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ReferenceId = identifier; + + return Task.CompletedTask; + } + + /// + /// Sets the status associated with a token. + /// + /// The token. + /// The status 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 Task SetStatusAsync([NotNull] TToken token, [CanBeNull] string status, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + token.Status = status; + + return Task.CompletedTask; + } + + /// + /// Sets the subject associated with a token. + /// + /// The token. + /// 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. + /// + public virtual Task SetSubjectAsync([NotNull] TToken token, [CanBeNull] string subject, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + token.Subject = subject; + + return Task.CompletedTask; + } + + /// + /// Sets the token type associated with a token. + /// + /// The token. + /// The token type associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetTokenTypeAsync([NotNull] TToken token, [CanBeNull] string type, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); + } + + token.Type = type; + + return Task.CompletedTask; + } + + /// + /// 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)); + } + + // Generate a new concurrency token and attach it + // to the token before persisting the changes. + token.ConcurrencyToken = Guid.NewGuid().ToString(); + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + if ((await collection.ReplaceOneAsync(entity => + entity.Id == token.Id && + entity.ConcurrencyToken == token.ConcurrencyToken, token, null, cancellationToken)).MatchedCount == 0) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The token was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the token from the database and retry the operation.") + .ToString()); + } + } + } +} \ No newline at end of file