Browse Source

Backport the MongoDB stores to OpenIddict 1.x

pull/670/head
Kévin Chalet 8 years ago
parent
commit
7f37169eee
  1. 16
      OpenIddict.sln
  2. 1
      build/dependencies.props
  3. 21
      src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj
  4. 91
      src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs
  5. 68
      src/OpenIddict.MongoDb.Models/OpenIddictAuthorization.cs
  6. 64
      src/OpenIddict.MongoDb.Models/OpenIddictScope.cs
  7. 98
      src/OpenIddict.MongoDb.Models/OpenIddictToken.cs
  8. 27
      src/OpenIddict.MongoDb/IOpenIddictMongoDbContext.cs
  9. 27
      src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj
  10. 184
      src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs
  11. 113
      src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs
  12. 81
      src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs
  13. 42
      src/OpenIddict.MongoDb/OpenIddictMongoDbOptions.cs
  14. 57
      src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs
  15. 57
      src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs
  16. 57
      src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs
  17. 57
      src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs
  18. 813
      src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs
  19. 764
      src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs
  20. 610
      src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs
  21. 874
      src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs

16
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}

1
build/dependencies.props

@ -11,6 +11,7 @@
<ImmutableCollectionsVersion>1.2.0</ImmutableCollectionsVersion>
<JetBrainsVersion>10.3.0</JetBrainsVersion>
<JsonNetVersion>9.0.1</JsonNetVersion>
<MongoDbVersion>2.6.1</MongoDbVersion>
<NetStandardImplicitPackageVersion>1.6.0</NetStandardImplicitPackageVersion>
<MoqVersion>4.7.63</MoqVersion>
<QueryableVersion>4.0.1</QueryableVersion>

21
src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.5</TargetFrameworks>
<SignAssembly>false</SignAssembly>
<NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion>
</PropertyGroup>
<PropertyGroup>
<Description>Document-oriented entities for the MongoDB stores.</Description>
<Authors>Kévin Chalet</Authors>
<PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Bson" Version="$(MongoDbVersion)" />
</ItemGroup>
</Project>

91
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
{
/// <summary>
/// Represents an OpenIddict application.
/// </summary>
public class OpenIddictApplication
{
/// <summary>
/// Gets or sets the client identifier
/// associated with the current application.
/// </summary>
[BsonElement("client_id"), BsonRequired]
public virtual string ClientId { get; set; }
/// <summary>
/// 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.
/// </summary>
[BsonElement("client_secret"), BsonIgnoreIfNull]
public virtual string ClientSecret { get; set; }
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
[BsonElement("concurrency_token"), BsonRequired]
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the consent type
/// associated with the current application.
/// </summary>
[BsonElement("consent_type"), BsonIgnoreIfNull]
public virtual string ConsentType { get; set; }
/// <summary>
/// Gets or sets the display name
/// associated with the current application.
/// </summary>
[BsonElement("display_name"), BsonIgnoreIfNull]
public virtual string DisplayName { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current application.
/// </summary>
[BsonId, BsonRequired]
public virtual ObjectId Id { get; set; }
/// <summary>
/// Gets or sets the permissions associated with the current application.
/// </summary>
[BsonElement("permissions"), BsonIgnoreIfDefault]
public virtual string[] Permissions { get; set; } = new string[0];
/// <summary>
/// Gets or sets the logout callback URLs associated with the current application.
/// </summary>
[BsonElement("post_logout_redirect_uris"), BsonIgnoreIfDefault]
public virtual string[] PostLogoutRedirectUris { get; set; } = new string[0];
/// <summary>
/// Gets or sets the additional properties associated with the current application.
/// </summary>
[BsonExtraElements]
public virtual BsonDocument Properties { get; set; } = new BsonDocument();
/// <summary>
/// Gets or sets the callback URLs associated with the current application.
/// </summary>
[BsonElement("redirect_uris"), BsonIgnoreIfDefault]
public virtual string[] RedirectUris { get; set; } = new string[0];
/// <summary>
/// Gets or sets the application type
/// associated with the current application.
/// </summary>
[BsonElement("type"), BsonRequired]
public virtual string Type { get; set; }
}
}

68
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
{
/// <summary>
/// Represents an OpenIddict authorization.
/// </summary>
public class OpenIddictAuthorization
{
/// <summary>
/// Gets or sets the identifier of the application
/// associated with the current authorization.
/// </summary>
[BsonElement("application_id"), BsonIgnoreIfDefault]
public virtual ObjectId ApplicationId { get; set; }
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
[BsonElement("concurrency_token"), BsonRequired]
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current authorization.
/// </summary>
[BsonId, BsonRequired]
public virtual ObjectId Id { get; set; }
/// <summary>
/// Gets or sets the additional properties associated with the current authorization.
/// </summary>
[BsonExtraElements]
public virtual BsonDocument Properties { get; set; } = new BsonDocument();
/// <summary>
/// Gets or sets the scopes associated with the current authorization.
/// </summary>
[BsonElement("scopes"), BsonIgnoreIfDefault]
public virtual string[] Scopes { get; set; } = new string[0];
/// <summary>
/// Gets or sets the status of the current authorization.
/// </summary>
[BsonElement("status"), BsonRequired]
public virtual string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the current authorization.
/// </summary>
[BsonElement("subject"), BsonRequired]
public virtual string Subject { get; set; }
/// <summary>
/// Gets or sets the type of the current authorization.
/// </summary>
[BsonElement("type"), BsonRequired]
public virtual string Type { get; set; }
}
}

64
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
{
/// <summary>
/// Represents an OpenIddict scope.
/// </summary>
public class OpenIddictScope
{
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
[BsonElement("concurrency_token"), BsonRequired]
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the public description
/// associated with the current scope.
/// </summary>
[BsonElement("description"), BsonIgnoreIfNull]
public virtual string Description { get; set; }
/// <summary>
/// Gets or sets the display name
/// associated with the current scope.
/// </summary>
[BsonElement("display_name"), BsonIgnoreIfNull]
public virtual string DisplayName { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current scope.
/// </summary>
[BsonId, BsonRequired]
public virtual ObjectId Id { get; set; }
/// <summary>
/// Gets or sets the unique name
/// associated with the current scope.
/// </summary>
[BsonElement("name"), BsonRequired]
public virtual string Name { get; set; }
/// <summary>
/// Gets or sets the additional properties associated with the current scope.
/// </summary>
[BsonExtraElements]
public virtual BsonDocument Properties { get; set; } = new BsonDocument();
/// <summary>
/// Gets or sets the resources associated with the current scope.
/// </summary>
[BsonElement("resources"), BsonIgnoreIfDefault]
public virtual string[] Resources { get; set; } = new string[0];
}
}

98
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
{
/// <summary>
/// Represents an OpenIddict token.
/// </summary>
public class OpenIddictToken
{
/// <summary>
/// Gets or sets the identifier of the application associated with the current token.
/// </summary>
[BsonElement("application_id"), BsonIgnoreIfDefault]
public virtual ObjectId ApplicationId { get; set; }
/// <summary>
/// Gets or sets the identifier of the authorization associated with the current token.
/// </summary>
[BsonElement("authorization_id"), BsonIgnoreIfDefault]
public virtual ObjectId AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
[BsonElement("concurrency_token"), BsonRequired]
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the date on which the token
/// will start to be considered valid.
/// </summary>
[BsonElement("creation_date"), BsonIgnoreIfNull]
public virtual DateTime? CreationDate { get; set; }
/// <summary>
/// Gets or sets the date on which the token
/// will no longer be considered valid.
/// </summary>
[BsonElement("expiration_date"), BsonIgnoreIfNull]
public virtual DateTime? ExpirationDate { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current token.
/// </summary>
[BsonId, BsonRequired]
public virtual ObjectId Id { get; set; }
/// <summary>
/// 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.
/// </summary>
[BsonElement("payload"), BsonIgnoreIfNull]
public virtual string Payload { get; set; }
/// <summary>
/// Gets or sets the additional properties associated with the current token.
/// </summary>
[BsonExtraElements]
public virtual BsonDocument Properties { get; set; } = new BsonDocument();
/// <summary>
/// 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.
/// </summary>
[BsonElement("reference_id"), BsonIgnoreIfNull]
public virtual string ReferenceId { get; set; }
/// <summary>
/// Gets or sets the status of the current token.
/// </summary>
[BsonElement("status"), BsonRequired]
public virtual string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the current token.
/// </summary>
[BsonElement("subject"), BsonRequired]
public virtual string Subject { get; set; }
/// <summary>
/// Gets or sets the type of the current token.
/// </summary>
[BsonElement("type"), BsonRequired]
public virtual string Type { get; set; }
}
}

27
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
{
/// <summary>
/// Exposes the MongoDB database used by the OpenIddict stores.
/// </summary>
public interface IOpenIddictMongoDbContext
{
/// <summary>
/// Gets the <see cref="IMongoDatabase"/>.
/// </summary>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the
/// asynchronous operation, whose result returns the MongoDB database.
/// </returns>
ValueTask<IMongoDatabase> GetDatabaseAsync(CancellationToken cancellationToken);
}
}

27
src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.5</TargetFrameworks>
<SignAssembly>false</SignAssembly>
<NetStandardImplicitPackageVersion>1.6.1</NetStandardImplicitPackageVersion>
</PropertyGroup>
<PropertyGroup>
<Description>MongoDB stores for OpenIddict.</Description>
<Authors>Kévin Chalet</Authors>
<PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\OpenIddict.MongoDb.Models\OpenIddict.MongoDb.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="MongoDB.Driver" Version="$(MongoDbVersion)" />
</ItemGroup>
</Project>

184
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
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict MongoDB services.
/// </summary>
public class OpenIddictMongoDbBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictMongoDbBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictMongoDbBuilder([NotNull] IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
Services = services;
}
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict MongoDB configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder Configure([NotNull] Action<OpenIddictMongoDbOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Configures the MongoDB stores to use the specified database
/// instead of retrieving it from the dependency injection container.
/// </summary>
/// <param name="database">The <see cref="IMongoDatabase"/>.</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder UseDatabase([NotNull] IMongoDatabase database)
{
if (database == null)
{
throw new ArgumentNullException(nameof(database));
}
return Configure(options => options.Database = database);
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default application entity.
/// </summary>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder ReplaceDefaultApplicationEntity<TApplication>()
where TApplication : OpenIddictApplication, new()
{
Services.Configure<OpenIddictCoreOptions>(options => options.DefaultApplicationType = typeof(TApplication));
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default authorization entity.
/// </summary>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder ReplaceDefaultAuthorizationEntity<TAuthorization>()
where TAuthorization : OpenIddictAuthorization, new()
{
Services.Configure<OpenIddictCoreOptions>(options => options.DefaultAuthorizationType = typeof(TAuthorization));
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default scope entity.
/// </summary>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder ReplaceDefaultScopeEntity<TScope>()
where TScope : OpenIddictScope, new()
{
Services.Configure<OpenIddictCoreOptions>(options => options.DefaultScopeType = typeof(TScope));
return this;
}
/// <summary>
/// Configures OpenIddict to use the specified entity as the default token entity.
/// </summary>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public OpenIddictMongoDbBuilder ReplaceDefaultTokenEntity<TToken>()
where TToken : OpenIddictToken, new()
{
Services.Configure<OpenIddictCoreOptions>(options => options.DefaultTokenType = typeof(TToken));
return this;
}
/// <summary>
/// Replaces the default applications collection name (by default, openiddict.applications).
/// </summary>
/// <param name="name">The collection name</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
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);
}
/// <summary>
/// Replaces the default authorizations collection name (by default, openiddict.authorizations).
/// </summary>
/// <param name="name">The collection name</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
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);
}
/// <summary>
/// Replaces the default scopes collection name (by default, openiddict.scopes).
/// </summary>
/// <param name="name">The collection name</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
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);
}
/// <summary>
/// Replaces the default tokens collection name (by default, openiddict.tokens).
/// </summary>
/// <param name="name">The collection name</param>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
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);
}
}
}

113
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
{
/// <summary>
/// Exposes the MongoDB database used by the OpenIddict stores.
/// </summary>
public class OpenIddictMongoDbContext : IOpenIddictMongoDbContext
{
private readonly IOptionsMonitor<OpenIddictMongoDbOptions> _options;
private readonly IServiceProvider _provider;
private IMongoDatabase _database;
public OpenIddictMongoDbContext(
[NotNull] IOptionsMonitor<OpenIddictMongoDbOptions> options,
[NotNull] IServiceProvider provider)
{
_options = options;
_provider = provider;
}
/// <summary>
/// Gets the <see cref="IMongoDatabase"/>.
/// </summary>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the
/// asynchronous operation, whose result returns the MongoDB database.
/// </returns>
public ValueTask<IMongoDatabase> GetDatabaseAsync(CancellationToken cancellationToken)
{
if (_database != null)
{
return new ValueTask<IMongoDatabase>(_database);
}
var options = _options.CurrentValue;
if (options == null)
{
throw new InvalidOperationException("The OpenIddict MongoDB options cannot be retrieved.");
}
async Task<IMongoDatabase> ExecuteAsync()
{
var database = options.Database;
if (database == null)
{
database = _provider.GetService<IMongoDatabase>();
}
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<OpenIddictApplication>(options.ApplicationsCollectionName);
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.ClientId),
new CreateIndexOptions
{
Unique = true
});
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.PostLogoutRedirectUris));
await applications.Indexes.CreateOneAsync(
Builders<OpenIddictApplication>.IndexKeys.Ascending(application => application.RedirectUris));
var scopes = database.GetCollection<OpenIddictScope>(options.ScopesCollectionName);
await scopes.Indexes.CreateOneAsync(
Builders<OpenIddictScope>.IndexKeys.Ascending(scope => scope.Name),
new CreateIndexOptions
{
Unique = true
});
var tokens = database.GetCollection<OpenIddictToken>(options.TokensCollectionName);
await tokens.Indexes.CreateOneAsync(
Builders<OpenIddictToken>.IndexKeys.Ascending(token => token.ReferenceId),
new CreateIndexOptions<OpenIddictToken>
{
PartialFilterExpression = Builders<OpenIddictToken>.Filter.Exists(token => token.ReferenceId),
Unique = true
});
return _database = database;
}
return new ValueTask<IMongoDatabase>(ExecuteAsync());
}
}
}

81
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
{
/// <summary>
/// Registers the MongoDB stores services in the DI container and
/// configures OpenIddict to use the MongoDB entities by default.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictMongoDbBuilder"/>.</returns>
public static OpenIddictMongoDbBuilder UseMongoDb([NotNull] this OpenIddictCoreBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
// 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<OpenIddictApplicationStoreResolver>(ServiceLifetime.Singleton)
.ReplaceAuthorizationStoreResolver<OpenIddictAuthorizationStoreResolver>(ServiceLifetime.Singleton)
.ReplaceScopeStoreResolver<OpenIddictScopeStoreResolver>(ServiceLifetime.Singleton)
.ReplaceTokenStoreResolver<OpenIddictTokenStoreResolver>(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<IOpenIddictMongoDbContext, OpenIddictMongoDbContext>();
return new OpenIddictMongoDbBuilder(builder.Services);
}
/// <summary>
/// Registers the MongoDB stores services in the DI container and
/// configures OpenIddict to use the MongoDB entities by default.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the MongoDB services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public static OpenIddictCoreBuilder UseMongoDb(
[NotNull] this OpenIddictCoreBuilder builder,
[NotNull] Action<OpenIddictMongoDbBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseMongoDb());
return builder;
}
}
}

42
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
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict MongoDB integration.
/// </summary>
public class OpenIddictMongoDbOptions
{
/// <summary>
/// Gets or sets the name of the applications collection (by default, openiddict.applications).
/// </summary>
public string ApplicationsCollectionName { get; set; } = "openiddict.applications";
/// <summary>
/// Gets or sets the name of the authorizations collection (by default, openiddict.authorizations).
/// </summary>
public string AuthorizationsCollectionName { get; set; } = "openiddict.authorizations";
/// <summary>
/// Gets or sets the <see cref="IMongoDatabase"/> used by the OpenIddict stores.
/// If no value is explicitly set, the database is resolved from the DI container.
/// </summary>
public IMongoDatabase Database { get; set; }
/// <summary>
/// Gets or sets the name of the scopes collection (by default, openiddict.scopes).
/// </summary>
public string ScopesCollectionName { get; set; } = "openiddict.scopes";
/// <summary>
/// Gets or sets the name of the tokens collection (by default, openiddict.tokens).
/// </summary>
public string TokensCollectionName { get; set; } = "openiddict.tokens";
}
}

57
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
{
/// <summary>
/// Exposes a method allowing to resolve an application store.
/// </summary>
public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver
{
private readonly IServiceProvider _provider;
public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider)
{
_provider = provider;
}
/// <summary>
/// Returns an application store compatible with the specified application type or throws an
/// <see cref="InvalidOperationException"/> if no store can be built using the specified type.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <returns>An <see cref="IOpenIddictApplicationStore{TApplication}"/>.</returns>
public IOpenIddictApplicationStore<TApplication> Get<TApplication>() where TApplication : class
{
var store = _provider.GetService<IOpenIddictApplicationStore<TApplication>>();
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<TApplication>) _provider.GetRequiredService(
typeof(OpenIddictApplicationStore<>).MakeGenericType(typeof(TApplication)));
}
}
}

57
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
{
/// <summary>
/// Exposes a method allowing to resolve an authorization store.
/// </summary>
public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver
{
private readonly IServiceProvider _provider;
public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider)
{
_provider = provider;
}
/// <summary>
/// Returns an authorization store compatible with the specified authorization type or throws an
/// <see cref="InvalidOperationException"/> if no store can be built using the specified type.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <returns>An <see cref="IOpenIddictAuthorizationStore{TAuthorization}"/>.</returns>
public IOpenIddictAuthorizationStore<TAuthorization> Get<TAuthorization>() where TAuthorization : class
{
var store = _provider.GetService<IOpenIddictAuthorizationStore<TAuthorization>>();
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<TAuthorization>) _provider.GetRequiredService(
typeof(OpenIddictAuthorizationStore<>).MakeGenericType(typeof(TAuthorization)));
}
}
}

57
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
{
/// <summary>
/// Exposes a method allowing to resolve a scope store.
/// </summary>
public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver
{
private readonly IServiceProvider _provider;
public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider)
{
_provider = provider;
}
/// <summary>
/// Returns a scope store compatible with the specified scope type or throws an
/// <see cref="InvalidOperationException"/> if no store can be built using the specified type.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <returns>An <see cref="IOpenIddictScopeStore{TScope}"/>.</returns>
public IOpenIddictScopeStore<TScope> Get<TScope>() where TScope : class
{
var store = _provider.GetService<IOpenIddictScopeStore<TScope>>();
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<TScope>) _provider.GetRequiredService(
typeof(OpenIddictScopeStore<>).MakeGenericType(typeof(TScope)));
}
}
}

57
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
{
/// <summary>
/// Exposes a method allowing to resolve a token store.
/// </summary>
public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver
{
private readonly IServiceProvider _provider;
public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider)
{
_provider = provider;
}
/// <summary>
/// Returns a token store compatible with the specified token type or throws an
/// <see cref="InvalidOperationException"/> if no store can be built using the specified type.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <returns>An <see cref="IOpenIddictTokenStore{TToken}"/>.</returns>
public IOpenIddictTokenStore<TToken> Get<TToken>() where TToken : class
{
var store = _provider.GetService<IOpenIddictTokenStore<TToken>>();
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<TToken>) _provider.GetRequiredService(
typeof(OpenIddictTokenStore<>).MakeGenericType(typeof(TToken)));
}
}
}

813
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
{
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
public class OpenIddictApplicationStore<TApplication> : IOpenIddictApplicationStore<TApplication>
where TApplication : OpenIddictApplication, new()
{
public OpenIddictApplicationStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptionsMonitor<OpenIddictMongoDbOptions> options)
{
Cache = cache;
Context = context;
Options = options;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictMongoDbContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictMongoDbOptions> Options { get; }
/// <summary>
/// Determines the number of applications that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual async Task<long> CountAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return await collection.CountAsync(FilterDefinition<TApplication>.Empty);
}
/// <summary>
/// Determines the number of applications that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return await ((IMongoQueryable<TApplication>) query(collection.AsQueryable())).LongCountAsync();
}
/// <summary>
/// Creates a new application.
/// </summary>
/// <param name="application">The application to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
await collection.InsertOneAsync(application, null, cancellationToken);
}
/// <summary>
/// Removes an existing application.
/// </summary>
/// <param name="application">The application to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TApplication>(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<OpenIddictAuthorization>(Options.CurrentValue.AuthorizationsCollectionName)
.DeleteManyAsync(authorization => authorization.ApplicationId == application.Id, cancellationToken);
// Delete the tokens associated with the application.
await database.GetCollection<OpenIddictToken>(Options.CurrentValue.TokensCollectionName)
.DeleteManyAsync(token => token.ApplicationId == application.Id, cancellationToken);
}
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual async Task<TApplication> 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<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return await collection.Find(application => application.ClientId == identifier).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual async Task<TApplication> 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<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return await collection.Find(application => application.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return ImmutableArray.CreateRange(await collection.Find(application =>
application.PostLogoutRedirectUris.Contains(address)).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return ImmutableArray.CreateRange(await collection.Find(application =>
application.RedirectUris.Contains(address)).ToListAsync(cancellationToken));
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual async Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientId);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client secret associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientSecret);
}
/// <summary>
/// Retrieves the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client type of the application (by default, "public").
/// </returns>
public virtual ValueTask<string> GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.Type);
}
/// <summary>
/// Retrieves the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
/// </returns>
public virtual ValueTask<string> GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ConsentType);
}
/// <summary>
/// Retrieves the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.Id.ToString());
}
/// <summary>
/// Retrieves the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> 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<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(application.Permissions.ToImmutableArray());
}
/// <summary>
/// Retrieves the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> 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<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(application.PostLogoutRedirectUris.ToImmutableArray());
}
/// <summary>
/// Retrieves the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the application.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (application.Properties == null)
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.FromObject(application.Properties.ToDictionary()));
}
/// <summary>
/// Retrieves the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> 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<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(application.RedirectUris.ToImmutableArray());
}
/// <summary>
/// Instantiates a new application.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated application, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TApplication> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TApplication>(new TApplication());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> ListAsync(
[CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
var query = (IMongoQueryable<TApplication>) 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));
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TApplication>(Options.CurrentValue.ApplicationsCollectionName);
return ImmutableArray.CreateRange(
await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken));
}
/// <summary>
/// Sets the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The client secret associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The client type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The consent type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="name">The display name associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="permissions">The permissions associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray<string> 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);
}
/// <summary>
/// Sets the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The logout callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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);
}
/// <summary>
/// Sets the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="properties">The additional properties associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<IDictionary<string, object>>());
return Task.FromResult(0);
}
/// <summary>
/// Sets the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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);
}
/// <summary>
/// Updates an existing application.
/// </summary>
/// <param name="application">The application to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TApplication>(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());
}
}
}
}

764
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
{
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
public class OpenIddictAuthorizationStore<TAuthorization> : IOpenIddictAuthorizationStore<TAuthorization>
where TAuthorization : OpenIddictAuthorization, new()
{
public OpenIddictAuthorizationStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptionsMonitor<OpenIddictMongoDbOptions> options)
{
Cache = cache;
Context = context;
Options = options;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictMongoDbContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictMongoDbOptions> Options { get; }
/// <summary>
/// Determines the number of authorizations that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
public virtual async Task<long> CountAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return await collection.CountAsync(FilterDefinition<TAuthorization>.Empty);
}
/// <summary>
/// Determines the number of authorizations that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return await ((IMongoQueryable<TAuthorization>) query(collection.AsQueryable())).LongCountAsync();
}
/// <summary>
/// Creates a new authorization.
/// </summary>
/// <param name="authorization">The authorization to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
await collection.InsertOneAsync(authorization, null, cancellationToken);
}
/// <summary>
/// Removes an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TAuthorization>(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<OpenIddictToken>(Options.CurrentValue.TokensCollectionName)
.DeleteManyAsync(token => token.AuthorizationId == authorization.Id, cancellationToken);
}
/// <summary>
/// Retrieves the authorizations corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return ImmutableArray.CreateRange(await collection.Find(authorization =>
authorization.Subject == subject &&
authorization.ApplicationId == ObjectId.Parse(client)).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return ImmutableArray.CreateRange(await collection.Find(authorization =>
authorization.Subject == subject &&
authorization.ApplicationId == ObjectId.Parse(client) &&
authorization.Status == status).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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<TAuthorization>(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));
}
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public virtual async Task<TAuthorization> 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<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return await collection.Find(authorization => authorization.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves all the authorizations corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return ImmutableArray.CreateRange(await collection.Find(authorization => authorization.Subject == subject).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves the optional application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the authorization.
/// </returns>
public virtual ValueTask<string> GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.ApplicationId.ToString());
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual async Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves the unique identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the authorization.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Id.ToString());
}
/// <summary>
/// Retrieves the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the authorization.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (authorization.Properties == null)
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.FromObject(authorization.Properties.ToDictionary()));
}
/// <summary>
/// Retrieves the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> 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<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(authorization.Scopes.ToImmutableArray());
}
/// <summary>
/// Retrieves the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Status);
}
/// <summary>
/// Retrieves the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Subject);
}
/// <summary>
/// Retrieves the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the type associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Type);
}
/// <summary>
/// Instantiates a new authorization.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated authorization, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TAuthorization> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TAuthorization>(new TAuthorization());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> ListAsync(
[CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
var query = (IMongoQueryable<TAuthorization>) 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));
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
return ImmutableArray.CreateRange(
await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken));
}
/// <summary>
/// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task PruneAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.CurrentValue.AuthorizationsCollectionName);
var identifiers =
await (from authorization in collection.AsQueryable()
join token in database.GetCollection<OpenIddictToken>(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));
}
/// <summary>
/// Sets the application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="properties">The additional properties associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<IDictionary<string, object>>());
return Task.FromResult(0);
}
/// <summary>
/// Sets the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="scopes">The scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetScopesAsync([NotNull] TAuthorization authorization,
ImmutableArray<string> 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);
}
/// <summary>
/// Sets the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="type">The type associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Updates an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TAuthorization>(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());
}
}
}
}

610
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
{
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
public class OpenIddictScopeStore<TScope> : IOpenIddictScopeStore<TScope>
where TScope : OpenIddictScope, new()
{
public OpenIddictScopeStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptionsMonitor<OpenIddictMongoDbOptions> options)
{
Cache = cache;
Context = context;
Options = options;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictMongoDbContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictMongoDbOptions> Options { get; }
/// <summary>
/// Determines the number of scopes that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
public virtual async Task<long> CountAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TScope>(Options.CurrentValue.ScopesCollectionName);
return await collection.CountAsync(FilterDefinition<TScope>.Empty);
}
/// <summary>
/// Determines the number of scopes that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TScope>(Options.CurrentValue.ScopesCollectionName);
return await ((IMongoQueryable<TScope>) query(collection.AsQueryable())).LongCountAsync();
}
/// <summary>
/// Creates a new scope.
/// </summary>
/// <param name="scope">The scope to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TScope>(Options.CurrentValue.ScopesCollectionName);
await collection.InsertOneAsync(scope, null, cancellationToken);
}
/// <summary>
/// Removes an existing scope.
/// </summary>
/// <param name="scope">The scope to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TScope>(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());
}
}
/// <summary>
/// Retrieves a scope using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
public virtual async Task<TScope> 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<TScope>(Options.CurrentValue.ScopesCollectionName);
return await collection.Find(scope => scope.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual async Task<TScope> 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<TScope>(Options.CurrentValue.ScopesCollectionName);
return await collection.Find(scope => scope.Name == name).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> 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<TScope>(Options.CurrentValue.ScopesCollectionName);
return ImmutableArray.CreateRange(await collection.Find(scope => names.Contains(scope.Name)).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves all the scopes that contain the specified resource.
/// </summary>
/// <param name="resource">The resource associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified resource.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> 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<TScope>(Options.CurrentValue.ScopesCollectionName);
return ImmutableArray.CreateRange(await collection.Find(scope => scope.Resources.Contains(resource)).ToListAsync(cancellationToken));
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual async Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TScope>(Options.CurrentValue.ScopesCollectionName);
return await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the description associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Description);
}
/// <summary>
/// Retrieves the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the scope.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the scope.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Id.ToString());
}
/// <summary>
/// Retrieves the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the name associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Name);
}
/// <summary>
/// Retrieves the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the scope.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (scope.Properties == null)
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.FromObject(scope.Properties.ToDictionary()));
}
/// <summary>
/// Retrieves the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the resources associated with the scope.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> 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<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(scope.Resources.ToImmutableArray());
}
/// <summary>
/// Instantiates a new scope.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated scope, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TScope> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TScope>(new TScope());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> ListAsync(
[CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TScope>(Options.CurrentValue.ScopesCollectionName);
var query = (IMongoQueryable<TScope>) 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));
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TScope>(Options.CurrentValue.ScopesCollectionName);
return ImmutableArray.CreateRange(
await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken));
}
/// <summary>
/// Sets the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="description">The description associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The display name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The name associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="properties">The additional properties associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<IDictionary<string, object>>());
return Task.FromResult(0);
}
/// <summary>
/// Sets the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="resources">The resources associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray<string> 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);
}
/// <summary>
/// Updates an existing scope.
/// </summary>
/// <param name="scope">The scope to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<TScope>(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());
}
}
}
}

874
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
{
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
public class OpenIddictTokenStore<TToken> : IOpenIddictTokenStore<TToken>
where TToken : OpenIddictToken, new()
{
public OpenIddictTokenStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptionsMonitor<OpenIddictMongoDbOptions> options)
{
Cache = cache;
Context = context;
Options = options;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Gets the database context associated with the current store.
/// </summary>
protected IOpenIddictMongoDbContext Context { get; }
/// <summary>
/// Gets the options associated with the current store.
/// </summary>
protected IOptionsMonitor<OpenIddictMongoDbOptions> Options { get; }
/// <summary>
/// Determines the number of tokens that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual async Task<long> CountAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return await collection.CountAsync(FilterDefinition<TToken>.Empty);
}
/// <summary>
/// Determines the number of tokens that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return await ((IMongoQueryable<TToken>) query(collection.AsQueryable())).LongCountAsync();
}
/// <summary>
/// Creates a new token.
/// </summary>
/// <param name="token">The token to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
await collection.InsertOneAsync(token, null, cancellationToken);
}
/// <summary>
/// Removes a token.
/// </summary>
/// <param name="token">The token to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
if ((await collection.DeleteOneAsync(entity =>
entity.Id == token.Id &&
entity.ConcurrencyToken == token.ConcurrencyToken)).DeletedCount == 0)
{
throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder()
.AppendLine("The token was concurrently updated and cannot be persisted in its current state.")
.Append("Reload the token from the database and retry the operation.")
.ToString());
}
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return ImmutableArray.CreateRange(await collection.Find(token => token.ApplicationId == ObjectId.Parse(identifier)).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return ImmutableArray.CreateRange(await collection.Find(token => token.AuthorizationId == ObjectId.Parse(identifier)).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves a token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public virtual async Task<TToken> 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<TToken>(Options.CurrentValue.TokensCollectionName);
return await collection.Find(token => token.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
public virtual async Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return await collection.Find(token => token.ReferenceId == identifier).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> 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<TToken>(Options.CurrentValue.TokensCollectionName);
return ImmutableArray.CreateRange(await collection.Find(token => token.Subject == subject).ToListAsync(cancellationToken));
}
/// <summary>
/// Retrieves the optional application identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.ApplicationId.ToString());
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public virtual async Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).FirstOrDefaultAsync(cancellationToken);
}
/// <summary>
/// Retrieves the optional authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.AuthorizationId.ToString());
}
/// <summary>
/// Retrieves the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.CreationDate);
}
/// <summary>
/// Retrieves the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the expiration date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.ExpirationDate);
}
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Id.ToString());
}
/// <summary>
/// Retrieves the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the payload associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Payload);
}
/// <summary>
/// Retrieves the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the token.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (token.Properties == null)
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.FromObject(token.Properties.ToDictionary()));
}
/// <summary>
/// Retrieves the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,
/// the reference identifier may be hashed for security reasons.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the reference identifier associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.ReferenceId);
}
/// <summary>
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Status);
}
/// <summary>
/// Retrieves the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Subject);
}
/// <summary>
/// Retrieves the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Type);
}
/// <summary>
/// Instantiates a new token.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated token, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TToken> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TToken>(new TToken());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> ListAsync(
[CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
var query = (IMongoQueryable<TToken>) collection.AsQueryable().OrderBy(token => token.Id);
if (offset.HasValue)
{
query = query.Skip(offset.Value);
}
if (count.HasValue)
{
query = query.Take(count.Value);
}
return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken));
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
return ImmutableArray.CreateRange(
await ((IMongoQueryable<TResult>) query(collection.AsQueryable(), state)).ToListAsync(cancellationToken));
}
/// <summary>
/// Removes the tokens that are marked as expired or invalid.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task PruneAsync(CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
await collection.DeleteManyAsync(token => token.ExpirationDate < DateTimeOffset.UtcNow ||
token.Status != OpenIddictConstants.Statuses.Valid, cancellationToken);
}
/// <summary>
/// Sets the application identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetApplicationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (!string.IsNullOrEmpty(identifier))
{
token.ApplicationId = ObjectId.Parse(identifier);
}
else
{
token.ApplicationId = ObjectId.Empty;
}
return Task.FromResult(0);
}
/// <summary>
/// Sets the authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetAuthorizationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (!string.IsNullOrEmpty(identifier))
{
token.AuthorizationId = ObjectId.Parse(identifier);
}
else
{
token.AuthorizationId = ObjectId.Empty;
}
return Task.FromResult(0);
}
/// <summary>
/// Sets the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The creation date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetCreationDateAsync([NotNull] TToken token,
[CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
token.CreationDate = date?.UtcDateTime;
return Task.FromResult(0);
}
/// <summary>
/// Sets the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The expiration date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetExpirationDateAsync([NotNull] TToken token,
[CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
token.ExpirationDate = date?.UtcDateTime;
return Task.FromResult(0);
}
/// <summary>
/// Sets the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="payload">The payload associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPayloadAsync([NotNull] TToken token, [CanBeNull] string payload, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
token.Payload = payload;
return Task.FromResult(0);
}
/// <summary>
/// Sets the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="properties">The additional properties associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (properties == null)
{
token.Properties = null;
return Task.FromResult(0);
}
token.Properties = new BsonDocument(properties.ToObject<IDictionary<string, object>>());
return Task.FromResult(0);
}
/// <summary>
/// Sets the reference identifier associated with a token.
/// Note: depending on the manager used to create the token,
/// the reference identifier may be hashed for security reasons.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The reference identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetReferenceIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
token.ReferenceId = identifier;
return Task.FromResult(0);
}
/// <summary>
/// Sets the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetStatusAsync([NotNull] TToken token, [CanBeNull] string status, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(status))
{
throw new ArgumentException("The status cannot be null or empty.", nameof(status));
}
token.Status = status;
return Task.FromResult(0);
}
/// <summary>
/// Sets the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetSubjectAsync([NotNull] TToken token, [CanBeNull] string subject, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
token.Subject = subject;
return Task.FromResult(0);
}
/// <summary>
/// Sets the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="type">The token type associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetTokenTypeAsync([NotNull] TToken token, [CanBeNull] string type, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException("The token type cannot be null or empty.", nameof(type));
}
token.Type = type;
return Task.FromResult(0);
}
/// <summary>
/// Updates an existing token.
/// </summary>
/// <param name="token">The token to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
// Generate a new concurrency token and attach it
// to the token before persisting the changes.
token.ConcurrencyToken = Guid.NewGuid().ToString();
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.CurrentValue.TokensCollectionName);
if ((await collection.ReplaceOneAsync(entity =>
entity.Id == token.Id &&
entity.ConcurrencyToken == token.ConcurrencyToken, token, null, cancellationToken)).MatchedCount == 0)
{
throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder()
.AppendLine("The token was concurrently updated and cannot be persisted in its current state.")
.Append("Reload the token from the database and retry the operation.")
.ToString());
}
}
}
}
Loading…
Cancel
Save