diff --git a/clean_cache.ps1 b/clean_cache.ps1 index d8ca57d7d..4019860d7 100644 --- a/clean_cache.ps1 +++ b/clean_cache.ps1 @@ -1,8 +1,8 @@ -Remove-Item $env:USERPROFILE\.nuget\packages\identityserver4\ -Recurse -ErrorAction SilentlyContinue -Remove-Item $env:USERPROFILE\.nuget\packages\identityserver4.storage\ -Recurse -ErrorAction SilentlyContinue -Remove-Item $env:USERPROFILE\.nuget\packages\identityserver4.entityframework\ -Recurse -ErrorAction SilentlyContinue -Remove-Item $env:USERPROFILE\.nuget\packages\identityserver4.entityframework.storage\ -Recurse -ErrorAction SilentlyContinue -Remove-Item $env:USERPROFILE\.nuget\packages\identityserver4.aspnetidentity\ -Recurse -ErrorAction SilentlyContinue +Remove-Item $env:USERPROFILE\.nuget\packages\duende.identityserver\ -Recurse -ErrorAction SilentlyContinue +Remove-Item $env:USERPROFILE\.nuget\packages\duende.identityserver.storage\ -Recurse -ErrorAction SilentlyContinue +Remove-Item $env:USERPROFILE\.nuget\packages\duende.identityserver.entityframework\ -Recurse -ErrorAction SilentlyContinue +Remove-Item $env:USERPROFILE\.nuget\packages\duende.identityserver.entityframework.storage\ -Recurse -ErrorAction SilentlyContinue +Remove-Item $env:USERPROFILE\.nuget\packages\duende.identityserver.aspnetidentity\ -Recurse -ErrorAction SilentlyContinue Remove-Item $env:USERPROFILE\.nuget\packages\identitymodel\ -Recurse -ErrorAction SilentlyContinue Remove-Item $env:USERPROFILE\.nuget\packages\IdentityModel.AspNetCore.OAuth2Introspection\ -Recurse -ErrorAction SilentlyContinue diff --git a/src/EntityFramework.Storage/host/ConsoleHost/Program.cs b/src/EntityFramework.Storage/host/ConsoleHost/Program.cs index 7a58c49fe..633f0eb45 100644 --- a/src/EntityFramework.Storage/host/ConsoleHost/Program.cs +++ b/src/EntityFramework.Storage/host/ConsoleHost/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. @@ -14,7 +14,7 @@ class Program { static void Main(string[] args) { - var connectionString = "server=(localdb)\\mssqllocaldb;database=IdentityServer4.EntityFramework-4.0.0;trusted_connection=yes;"; + var connectionString = "server=(localdb)\\mssqllocaldb;database=Duende.EntityFramework-5.0.0;trusted_connection=yes;"; var services = new ServiceCollection(); services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace)); diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb.sql b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb.sql index b92083537..4bc058784 100644 --- a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb.sql +++ b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb.sql @@ -375,7 +375,7 @@ CREATE UNIQUE INDEX [IX_IdentityResources_Name] ON [IdentityResources] ([Name]); GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) -VALUES (N'20201012150100_Configuration', N'3.1.0'); +VALUES (N'20201123190954_Configuration', N'3.1.0'); GO diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201012150100_Configuration.Designer.cs b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201123190954_Configuration.Designer.cs similarity index 99% rename from src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201012150100_Configuration.Designer.cs rename to src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201123190954_Configuration.Designer.cs index 886a28fb7..17cbe16f7 100644 --- a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201012150100_Configuration.Designer.cs +++ b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201123190954_Configuration.Designer.cs @@ -10,7 +10,7 @@ namespace SqlServer.Migrations.ConfigurationDb { [DbContext(typeof(ConfigurationDbContext))] - [Migration("20201012150100_Configuration")] + [Migration("20201123190954_Configuration")] partial class Configuration { protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201012150100_Configuration.cs b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201123190954_Configuration.cs similarity index 100% rename from src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201012150100_Configuration.cs rename to src/EntityFramework.Storage/migrations/SqlServer/Migrations/ConfigurationDb/20201123190954_Configuration.cs diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb.sql b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb.sql index 5b5d10041..1bea1a842 100644 --- a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb.sql +++ b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb.sql @@ -24,6 +24,19 @@ CREATE TABLE [DeviceCodes] ( GO +CREATE TABLE [Keys] ( + [Id] nvarchar(450) NOT NULL, + [Version] int NOT NULL, + [Created] datetime2 NOT NULL, + [Algorithm] nvarchar(100) NOT NULL, + [IsX509Certificate] bit NOT NULL, + [DataProtected] bit NOT NULL, + [Data] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Keys] PRIMARY KEY ([Id]) +); + +GO + CREATE TABLE [PersistedGrants] ( [Key] nvarchar(200) NOT NULL, [Type] nvarchar(50) NOT NULL, @@ -61,7 +74,7 @@ CREATE INDEX [IX_PersistedGrants_SubjectId_SessionId_Type] ON [PersistedGrants] GO INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) -VALUES (N'20201012150055_Grants', N'3.1.0'); +VALUES (N'20201123190949_Grants', N'3.1.0'); GO diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201012150055_Grants.Designer.cs b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201123190949_Grants.Designer.cs similarity index 80% rename from src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201012150055_Grants.Designer.cs rename to src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201123190949_Grants.Designer.cs index f1c2d8357..6df325fbc 100644 --- a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201012150055_Grants.Designer.cs +++ b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201123190949_Grants.Designer.cs @@ -10,7 +10,7 @@ namespace SqlServer.Migrations.PersistedGrantDb { [DbContext(typeof(PersistedGrantDbContext))] - [Migration("20201012150055_Grants")] + [Migration("20201123190949_Grants")] partial class Grants { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -71,6 +71,37 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("DeviceCodes"); }); + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Algorithm") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DataProtected") + .HasColumnType("bit"); + + b.Property("IsX509Certificate") + .HasColumnType("bit"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Keys"); + }); + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => { b.Property("Key") diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201012150055_Grants.cs b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201123190949_Grants.cs similarity index 81% rename from src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201012150055_Grants.cs rename to src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201123190949_Grants.cs index 73fe54c70..0a4903b41 100644 --- a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201012150055_Grants.cs +++ b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/20201123190949_Grants.cs @@ -26,6 +26,23 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_DeviceCodes", x => x.UserCode); }); + migrationBuilder.CreateTable( + name: "Keys", + columns: table => new + { + Id = table.Column(nullable: false), + Version = table.Column(nullable: false), + Created = table.Column(nullable: false), + Algorithm = table.Column(maxLength: 100, nullable: false), + IsX509Certificate = table.Column(nullable: false), + DataProtected = table.Column(nullable: false), + Data = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Keys", x => x.Id); + }); + migrationBuilder.CreateTable( name: "PersistedGrants", columns: table => new @@ -78,6 +95,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "DeviceCodes"); + migrationBuilder.DropTable( + name: "Keys"); + migrationBuilder.DropTable( name: "PersistedGrants"); } diff --git a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/PersistedGrantDbContextModelSnapshot.cs b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/PersistedGrantDbContextModelSnapshot.cs index 666d47763..e0af9cc80 100644 --- a/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/PersistedGrantDbContextModelSnapshot.cs +++ b/src/EntityFramework.Storage/migrations/SqlServer/Migrations/PersistedGrantDb/PersistedGrantDbContextModelSnapshot.cs @@ -69,6 +69,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DeviceCodes"); }); + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Algorithm") + .IsRequired() + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DataProtected") + .HasColumnType("bit"); + + b.Property("IsX509Certificate") + .HasColumnType("bit"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Keys"); + }); + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => { b.Property("Key") diff --git a/src/EntityFramework.Storage/src/DbContexts/PersistedGrantDbContext.cs b/src/EntityFramework.Storage/src/DbContexts/PersistedGrantDbContext.cs index 1836d2726..82fc529c0 100644 --- a/src/EntityFramework.Storage/src/DbContexts/PersistedGrantDbContext.cs +++ b/src/EntityFramework.Storage/src/DbContexts/PersistedGrantDbContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. @@ -70,6 +70,14 @@ public PersistedGrantDbContext(DbContextOptions options, OperationalStoreOptions /// public DbSet DeviceFlowCodes { get; set; } + /// + /// Gets or sets the keys. + /// + /// + /// The keys. + /// + public DbSet Keys { get; set; } + /// /// Saves the changes. /// diff --git a/src/EntityFramework.Storage/src/Entities/Key.cs b/src/EntityFramework.Storage/src/Entities/Key.cs new file mode 100644 index 000000000..2d784fbcd --- /dev/null +++ b/src/EntityFramework.Storage/src/Entities/Key.cs @@ -0,0 +1,23 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +#pragma warning disable 1591 + +using System; + +namespace Duende.IdentityServer.EntityFramework.Entities +{ + /// + /// Models storage for keys. + /// + public class Key + { + public string Id { get; set; } + public int Version { get; set; } + public DateTime Created { get; set; } + public string Algorithm { get; set; } + public bool IsX509Certificate { get; set; } + public bool DataProtected { get; set; } + public string Data { get; set; } + } +} diff --git a/src/EntityFramework.Storage/src/Extensions/ModelBuilderExtensions.cs b/src/EntityFramework.Storage/src/Extensions/ModelBuilderExtensions.cs index 5787d997b..0f2be0ead 100644 --- a/src/EntityFramework.Storage/src/Extensions/ModelBuilderExtensions.cs +++ b/src/EntityFramework.Storage/src/Extensions/ModelBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. @@ -171,6 +171,13 @@ public static void ConfigurePersistedGrantContext(this ModelBuilder modelBuilder codes.HasIndex(x => x.DeviceCode).IsUnique(); codes.HasIndex(x => x.Expiration); }); + + modelBuilder.Entity(entity => + { + entity.HasKey(x => x.Id); + entity.Property(x => x.Algorithm).HasMaxLength(100).IsRequired(); + entity.Property(x => x.Data).IsRequired(); + }); } /// diff --git a/src/EntityFramework.Storage/src/Interfaces/IPersistedGrantDbContext.cs b/src/EntityFramework.Storage/src/Interfaces/IPersistedGrantDbContext.cs index fb02cc930..dfdf9a627 100644 --- a/src/EntityFramework.Storage/src/Interfaces/IPersistedGrantDbContext.cs +++ b/src/EntityFramework.Storage/src/Interfaces/IPersistedGrantDbContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. @@ -31,6 +31,16 @@ public interface IPersistedGrantDbContext : IDisposable /// DbSet DeviceFlowCodes { get; set; } + + /// + /// Gets or sets the keys. + /// + /// + /// The keys. + /// + DbSet Keys { get; set; } + + /// /// Saves the changes. /// diff --git a/src/EntityFramework.Storage/src/Stores/SigningKeyStore.cs b/src/EntityFramework.Storage/src/Stores/SigningKeyStore.cs new file mode 100644 index 000000000..b465fd73d --- /dev/null +++ b/src/EntityFramework.Storage/src/Stores/SigningKeyStore.cs @@ -0,0 +1,110 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Duende.IdentityServer.EntityFramework.DbContexts; +using Duende.IdentityServer.EntityFramework.Entities; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Stores; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Duende.IdentityServer.EntityFramework.Stores +{ + /// + /// Implementation of ISigningKeyStore thats uses EF. + /// + /// + public class SigningKeyStore : ISigningKeyStore + { + /// + /// The DbContext. + /// + protected readonly PersistedGrantDbContext Context; + + /// + /// The logger. + /// + protected readonly ILogger Logger; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The logger. + /// context + public SigningKeyStore(PersistedGrantDbContext context, ILogger logger) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Logger = logger; + } + + /// + /// Loads all keys from store. + /// + /// + public async Task> LoadKeysAsync() + { + var entities = await Context.Keys.ToArrayAsync(); + return entities.Select(key => new SerializedKey + { + Id = key.Id, + Created = key.Created, + Version = key.Version, + Algorithm = key.Algorithm, + Data = key.Data, + DataProtected = key.DataProtected, + IsX509Certificate = key.IsX509Certificate + }); + } + + /// + /// Persists new key in store. + /// + /// + /// + public Task StoreKeyAsync(SerializedKey key) + { + var entity = new Key + { + Id = key.Id, + Created = key.Created, + Version = key.Version, + Algorithm = key.Algorithm, + Data = key.Data, + DataProtected = key.DataProtected, + IsX509Certificate = key.IsX509Certificate + }; + Context.Keys.Add(entity); + return Context.SaveChangesAsync(); + } + + /// + /// Deletes key from storage. + /// + /// + /// + public async Task DeleteKeyAsync(string id) + { + var item = await Context.Keys.FirstOrDefaultAsync(x => x.Id == id); + if (item != null) + { + try + { + Context.Keys.Remove(item); + await Context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + Context.Entry(item).State = EntityState.Detached; + // already deleted, so we can eat this exception + Logger.LogDebug("Concurrency exception caught deleting key id {kid}", id); + } + } + } + } +} \ No newline at end of file diff --git a/src/EntityFramework/src/IdentityServerEntityFrameworkBuilderExtensions.cs b/src/EntityFramework/src/IdentityServerEntityFrameworkBuilderExtensions.cs index 5e848c8a1..8a189d528 100644 --- a/src/EntityFramework/src/IdentityServerEntityFrameworkBuilderExtensions.cs +++ b/src/EntityFramework/src/IdentityServerEntityFrameworkBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. @@ -100,6 +100,7 @@ public static IIdentityServerBuilder AddOperationalStore( { builder.Services.AddOperationalDbContext(storeOptionsAction); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddSingleton(); diff --git a/src/IdentityServer/src/Configuration/DependencyInjection/Options/KeyManagementOptions.cs b/src/IdentityServer/src/Configuration/DependencyInjection/Options/KeyManagementOptions.cs index be3c7ca47..f0df8b2a6 100644 --- a/src/IdentityServer/src/Configuration/DependencyInjection/Options/KeyManagementOptions.cs +++ b/src/IdentityServer/src/Configuration/DependencyInjection/Options/KeyManagementOptions.cs @@ -132,6 +132,14 @@ internal void Validate() if (RotationInterval <= TimeSpan.Zero) throw new Exception(nameof(RotationInterval) + " must be greater than zero."); if (RetentionDuration <= TimeSpan.Zero) throw new Exception(nameof(RetentionDuration) + " must be greater than zero."); + if (KeyCacheDuration > PropagationTime / 2) + { + // we should not cache too long, because we need a server to have latest data + // to allow clients/apis time to update their caches. + // todo: error, or just calculate it? + KeyCacheDuration = PropagationTime / 2; + } + if (RotationInterval <= PropagationTime) throw new Exception(nameof(RotationInterval) + " must be longer than " + nameof(PropagationTime)); } } diff --git a/src/IdentityServer/src/Services/Default/KeyManagement/DataProtectionKeyProtector.cs b/src/IdentityServer/src/Services/Default/KeyManagement/DataProtectionKeyProtector.cs index 9be6dab9e..08bf325b0 100644 --- a/src/IdentityServer/src/Services/Default/KeyManagement/DataProtectionKeyProtector.cs +++ b/src/IdentityServer/src/Services/Default/KeyManagement/DataProtectionKeyProtector.cs @@ -40,6 +40,7 @@ public SerializedKey Protect(KeyContainer key) return new SerializedKey { + Version = 1, Created = DateTime.UtcNow, Id = key.Id, Algorithm = key.Algorithm,