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 8d155c26..850642f8 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -11,6 +11,7 @@
1.2.0
10.3.0
9.0.1
+ 2.6.1
1.6.0
4.7.63
4.0.1
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..f055b316
--- /dev/null
+++ b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+ net451;netstandard1.5
+ false
+ 1.6.1
+
+
+
+ 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..8e64c371
--- /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; } = new string[0];
+
+ ///
+ /// Gets or sets the logout callback URLs associated with the current application.
+ ///
+ [BsonElement("post_logout_redirect_uris"), BsonIgnoreIfDefault]
+ public virtual string[] PostLogoutRedirectUris { get; set; } = new string[0];
+
+ ///
+ /// 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; } = new string[0];
+
+ ///
+ /// 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..91fef20d
--- /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; } = new string[0];
+
+ ///
+ /// 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..61b331a5
--- /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; } = new string[0];
+ }
+}
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..0298d451
--- /dev/null
+++ b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj
@@ -0,0 +1,27 @@
+
+
+
+
+
+ net451;netstandard1.5
+ false
+ 1.6.1
+
+
+
+ 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..f7c9db60
--- /dev/null
+++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
@@ -0,0 +1,113 @@
+/*
+ * 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 ValueTask GetDatabaseAsync(CancellationToken cancellationToken)
+ {
+ if (_database != null)
+ {
+ return new ValueTask(_database);
+ }
+
+ var options = _options.CurrentValue;
+ if (options == null)
+ {
+ throw new InvalidOperationException("The OpenIddict MongoDB options cannot be retrieved.");
+ }
+
+ async Task ExecuteAsync()
+ {
+ 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;
+ }
+
+ return new ValueTask(ExecuteAsync());
+ }
+ }
+}
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..2d794080
--- /dev/null
+++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs
@@ -0,0 +1,57 @@
+/*
+ * 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.Reflection;
+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..53eef9c6
--- /dev/null
+++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs
@@ -0,0 +1,57 @@
+/*
+ * 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.Reflection;
+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..5ec295a0
--- /dev/null
+++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs
@@ -0,0 +1,57 @@
+/*
+ * 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.Reflection;
+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..fa6229c7
--- /dev/null
+++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs
@@ -0,0 +1,57 @@
+/*
+ * 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.Reflection;
+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..ea5738bb
--- /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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ application.Permissions = permissions.ToArray();
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ application.PostLogoutRedirectUris = addresses.ToArray();
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ application.Properties = new BsonDocument(properties.ToObject>());
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ application.RedirectUris = addresses.ToArray();
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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..353d11ae
--- /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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ authorization.Properties = new BsonDocument(properties.ToObject>());
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ authorization.Scopes = scopes.ToArray();
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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..70ad1fef
--- /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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ scope.Properties = new BsonDocument(properties.ToObject>());
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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.FromResult(0);
+ }
+
+ scope.Resources = resources.ToArray();
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// 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..99d7b473
--- /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;
+ }
+
+ ///