diff --git a/Streetwriters.Identity/Jobs/TokenCleanupJob.cs b/Streetwriters.Identity/Jobs/TokenCleanupJob.cs new file mode 100644 index 0000000..f2d61a6 --- /dev/null +++ b/Streetwriters.Identity/Jobs/TokenCleanupJob.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Quartz; +using Streetwriters.Identity.Services; + +namespace Streetwriters.Identity.Jobs +{ + public class TokenCleanupJob : IJob + { + private TokenCleanup TokenCleanup { get; set; } + public TokenCleanupJob(TokenCleanup tokenCleanup) + { + TokenCleanup = tokenCleanup; + } + + public Task Execute(IJobExecutionContext context) + { + return TokenCleanup.RemoveExpiredGrantsAsync(); + } + } +} \ No newline at end of file diff --git a/Streetwriters.Identity/Services/CustomPersistedGrantDbContext.cs b/Streetwriters.Identity/Services/CustomPersistedGrantDbContext.cs new file mode 100644 index 0000000..e943c5e --- /dev/null +++ b/Streetwriters.Identity/Services/CustomPersistedGrantDbContext.cs @@ -0,0 +1,78 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using IdentityServer4.MongoDB; +using IdentityServer4.MongoDB.Configuration; +using IdentityServer4.MongoDB.DbContexts; +using IdentityServer4.MongoDB.Entities; +using IdentityServer4.MongoDB.Interfaces; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Streetwriters.Identity.Services +{ + public class CustomPersistedGrantDbContext : MongoDBContextBase, IPersistedGrantDbContext + { + private readonly IMongoCollection _persistedGrants; + + public CustomPersistedGrantDbContext(IOptions settings) + : base(settings) + { + _persistedGrants = Database.GetCollection(Constants.TableNames.PersistedGrant); + CreateIndexes(); + } + + private void CreateIndexes() + { + var indexOptions = new CreateIndexOptions() { Background = true }; + var builder = Builders.IndexKeys; + + var expirationIndexModel = new CreateIndexModel(builder.Descending(_ => _.Expiration), indexOptions); + var keyIndexModel = new CreateIndexModel(builder.Ascending(_ => _.Key), indexOptions); + var subIndexModel = new CreateIndexModel(builder.Ascending(_ => _.SubjectId), indexOptions); + var clientIdSubIndexModel = new CreateIndexModel( + builder.Combine( + builder.Ascending(_ => _.ClientId), + builder.Ascending(_ => _.SubjectId)), + indexOptions); + + var clientIdSubTypeIndexModel = new CreateIndexModel( + builder.Combine( + builder.Ascending(_ => _.ClientId), + builder.Ascending(_ => _.SubjectId), + builder.Ascending(_ => _.Type)), + indexOptions); + + _persistedGrants.Indexes.CreateOne(expirationIndexModel); + _persistedGrants.Indexes.CreateOne(keyIndexModel); + _persistedGrants.Indexes.CreateOne(subIndexModel); + _persistedGrants.Indexes.CreateOne(clientIdSubIndexModel); + _persistedGrants.Indexes.CreateOne(clientIdSubTypeIndexModel); + } + + public IQueryable PersistedGrants + { + get { return _persistedGrants.AsQueryable(); } + } + + public Task Remove(Expression> filter) + { + return _persistedGrants.DeleteManyAsync(filter); + } + + public Task RemoveExpired() + { + return Remove(x => x.Expiration < DateTime.UtcNow.AddHours(12)); + } + + public Task InsertOrUpdate(Expression> filter, PersistedGrant entity) + { + return _persistedGrants.ReplaceOneAsync(filter, entity, new ReplaceOptions() { IsUpsert = true }); + } + } +} \ No newline at end of file diff --git a/Streetwriters.Identity/Services/TokenCleanup.cs b/Streetwriters.Identity/Services/TokenCleanup.cs new file mode 100644 index 0000000..9613b65 --- /dev/null +++ b/Streetwriters.Identity/Services/TokenCleanup.cs @@ -0,0 +1,51 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using IdentityServer4.MongoDB.Interfaces; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Streetwriters.Identity.Services +{ + public class TokenCleanup + { + private readonly IPersistedGrantDbContext _persistedGrantDbContext; + private readonly ILogger _logger; + + public TokenCleanup(IPersistedGrantDbContext persistedGrantDbContext, ILogger logger) + { + _persistedGrantDbContext = persistedGrantDbContext; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Method to clear expired persisted grants. + /// + /// + public async Task RemoveExpiredGrantsAsync() + { + try + { + _logger.LogTrace("Querying for expired grants to remove"); + + await RemoveGrantsAsync(); + //TODO: await RemoveDeviceCodesAsync(); + } + catch (Exception ex) + { + _logger.LogError("Exception removing expired grants: {exception}", ex.Message); + } + } + + /// + /// Removes the stale persisted grants. + /// + /// + protected virtual async Task RemoveGrantsAsync() + { + await _persistedGrantDbContext.RemoveExpired(); + } + } +} \ No newline at end of file diff --git a/Streetwriters.Identity/Startup.cs b/Streetwriters.Identity/Startup.cs index 011eaa3..b51dab5 100644 --- a/Streetwriters.Identity/Startup.cs +++ b/Streetwriters.Identity/Startup.cs @@ -20,8 +20,13 @@ along with this program. If not, see . using System; using System.IO; using AspNetCore.Identity.Mongo; +using IdentityServer4.MongoDB.Entities; +using IdentityServer4.MongoDB.Interfaces; +using IdentityServer4.MongoDB.Options; +using IdentityServer4.MongoDB.Stores; using IdentityServer4.ResponseHandling; using IdentityServer4.Services; +using IdentityServer4.Stores; using IdentityServer4.Validation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -32,12 +37,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using MongoDB.Bson.Serialization; +using Quartz; using Streetwriters.Common; using Streetwriters.Common.Extensions; using Streetwriters.Common.Messages; using Streetwriters.Common.Models; using Streetwriters.Identity.Helpers; using Streetwriters.Identity.Interfaces; +using Streetwriters.Identity.Jobs; using Streetwriters.Identity.Models; using Streetwriters.Identity.Services; using Streetwriters.Identity.Validation; @@ -96,7 +104,7 @@ namespace Streetwriters.Identity options.ConnectionString = connectionString; }).AddDefaultTokenProviders(); - var builder = services.AddIdentityServer( + services.AddIdentityServer( options => { options.Events.RaiseSuccessEvents = true; @@ -107,16 +115,6 @@ namespace Streetwriters.Identity .AddExtensionGrantValidator() .AddExtensionGrantValidator() .AddExtensionGrantValidator() - .AddOperationalStore(options => - { - options.ConnectionString = connectionString; - }, (options) => - { -#if !DEBUG - options.Enable = true; - options.Interval = 3600; -#endif - }) .AddConfigurationStore(options => { options.ConnectionString = connectionString; @@ -160,6 +158,15 @@ namespace Streetwriters.Identity }; }); + services.AddQuartzHostedService(q => + { + q.WaitForJobsToComplete = true; + q.AwaitApplicationStarted = true; + q.StartDelay = TimeSpan.FromMinutes(1); + }); + + AddOperationalStore(services, new TokenCleanupOptions { Enable = true, Interval = 3600 * 12 }); + services.AddTransient(); services.AddControllers(); services.AddTransient(); @@ -222,5 +229,33 @@ namespace Streetwriters.Identity endpoints.MapHealthChecks("/health"); }); } + + private void AddOperationalStore(IServiceCollection services, TokenCleanupOptions tokenCleanUpOptions = null) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.SetIgnoreExtraElements(true); + }); + + services.AddScoped(); + services.AddTransient(); + services.AddTransient(); + + services.AddQuartz(q => + { + q.UseMicrosoftDependencyInjectionJobFactory(); + + if (tokenCleanUpOptions.Enable) + { + var jobKey = new JobKey("TokenCleanupJob"); + q.AddJob(opts => opts.WithIdentity(jobKey)); + q.AddTrigger(opts => opts + .ForJob(jobKey) + .WithIdentity("TokenCleanup-trigger") + .WithSimpleSchedule((s) => s.RepeatForever().WithIntervalInSeconds(tokenCleanUpOptions.Interval))); + } + }); + } } } diff --git a/Streetwriters.Identity/Streetwriters.Identity.csproj b/Streetwriters.Identity/Streetwriters.Identity.csproj index 32ce847..56b8540 100644 --- a/Streetwriters.Identity/Streetwriters.Identity.csproj +++ b/Streetwriters.Identity/Streetwriters.Identity.csproj @@ -18,6 +18,8 @@ + +