Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

208 lines
9.6 KiB

/*
* 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.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using Quartz;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.Quartz
{
/// <summary>
/// Represents a Quartz.NET job performing scheduled tasks for the OpenIddict server feature.
/// </summary>
[DisallowConcurrentExecution]
public class OpenIddictServerQuartzJob : IJob
{
private readonly IOptionsMonitor<OpenIddictServerQuartzOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerQuartzJob"/> class.
/// </summary>
public OpenIddictServerQuartzJob() => throw new InvalidOperationException(SR.GetResourceString(SR.ID1081));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerQuartzJob"/> class.
/// </summary>
/// <param name="options">The OpenIddict server Quartz.NET options.</param>
/// <param name="provider">The service provider.</param>
public OpenIddictServerQuartzJob(IOptionsMonitor<OpenIddictServerQuartzOptions> options, IServiceProvider provider)
{
_options = options;
_provider = provider;
}
/// <summary>
/// Gets the default identity assigned to this job.
/// </summary>
public static JobKey Identity { get; } = new JobKey(
name: typeof(OpenIddictServerQuartzJob).Name,
group: typeof(OpenIddictServerQuartzJob).Assembly.GetName().Name!);
/// <inheritdoc/>
public async Task Execute(IJobExecutionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
List<Exception>? exceptions = null;
// Note: this job is registered as a transient service. As such, it cannot directly depend on scoped services
// like the core managers. To work around this limitation, a scope is manually created for each invocation.
var scope = _provider.CreateScope();
try
{
// Note: this background task is responsible of automatically removing orphaned tokens/authorizations
// (i.e tokens that are no longer valid and ad-hoc authorizations that have no valid tokens associated).
// Since ad-hoc authorizations and their associated tokens are removed as part of the same operation
// when they no longer have any token attached, it's more efficient to remove the authorizations first.
// Note: the authorization/token managers MUST be resolved from the scoped provider
// as they depend on scoped stores that should be disposed as soon as possible.
if (!_options.CurrentValue.DisableAuthorizationsPruning)
{
var manager = scope.ServiceProvider.GetService<IOpenIddictAuthorizationManager>();
if (manager == null)
{
// Inform Quartz.NET that the triggers associated with this job should be removed,
// as the future invocations will always fail until the application is correctly
// re-configured to register the OpenIddict core services in the DI container.
throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID1277)))
{
RefireImmediately = false,
UnscheduleAllTriggers = true,
UnscheduleFiringTrigger = true
};
}
try
{
await manager.PruneAsync(context.CancellationToken);
}
// OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is.
catch (OutOfMemoryException)
{
throw;
}
// OperationCanceledExceptions are typically thrown the host is about to shut down.
// To allow the host to shut down as fast as possible, this exception type is special-cased
// to prevent further processing in this job and inform Quartz.NET it shouldn't be refired.
catch (OperationCanceledException exception) when (exception.CancellationToken == context.CancellationToken)
{
throw new JobExecutionException(exception)
{
RefireImmediately = false
};
}
// AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s)
// occurred while trying to prune the entities. In this case, add the inner exceptions to the collection.
catch (AggregateException exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.AddRange(exception.InnerExceptions);
}
// Other exceptions are assumed to be transient and are added to the exceptions collection
// to be re-thrown later (typically, at the very end of this job, as an AggregateException).
catch (Exception exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.Add(exception);
}
}
if (!_options.CurrentValue.DisableTokensPruning)
{
var manager = scope.ServiceProvider.GetService<IOpenIddictTokenManager>();
if (manager == null)
{
// Inform Quartz.NET that the triggers associated with this job should be removed,
// as the future invocations will always fail until the application is correctly
// re-configured to register the OpenIddict core services in the DI container.
throw new JobExecutionException(new InvalidOperationException(SR.GetResourceString(SR.ID1277)))
{
RefireImmediately = false,
UnscheduleAllTriggers = true,
UnscheduleFiringTrigger = true
};
}
try
{
await manager.PruneAsync(context.CancellationToken);
}
// OutOfMemoryExceptions are treated as fatal errors and are always re-thrown as-is.
catch (OutOfMemoryException)
{
throw;
}
// OperationCanceledExceptions are typically thrown the host is about to shut down.
// To allow the host to shut down as fast as possible, this exception type is special-cased
// to prevent further processing in this job and inform Quartz.NET it shouldn't be refired.
catch (OperationCanceledException exception) when (exception.CancellationToken == context.CancellationToken)
{
throw new JobExecutionException(exception)
{
RefireImmediately = false
};
}
// AggregateExceptions are generally thrown by the manager itself when one or multiple exception(s)
// occurred while trying to prune the entities. In this case, add the inner exceptions to the collection.
catch (AggregateException exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.AddRange(exception.InnerExceptions);
}
// Other exceptions are assumed to be transient and are added to the exceptions collection
// to be re-thrown later (typically, at the very end of this job, as an AggregateException).
catch (Exception exception)
{
exceptions ??= new List<Exception>(capacity: 1);
exceptions.Add(exception);
}
}
if (exceptions != null)
{
throw new JobExecutionException(new AggregateException(exceptions))
{
// Only refire the job if the maximum refire count set in the options wasn't reached.
RefireImmediately = context.RefireCount < _options.CurrentValue.MaximumRefireCount
};
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
}
}