Skip to content

Commit 0b0d2e5

Browse files
Merge pull request #2376 from DuendeSoftware/beh/saml-stores
ISamlServiceProviderStore Enhancments
2 parents 204a929 + 6737976 commit 0b0d2e5

File tree

36 files changed

+2061
-4
lines changed

36 files changed

+2061
-4
lines changed

identity-server/src/EntityFramework.Storage/DbContexts/ConfigurationDbContext.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ public ConfigurationDbContext(DbContextOptions<TContext> options)
102102
/// </value>
103103
public DbSet<IdentityProvider> IdentityProviders { get; set; }
104104

105+
/// <summary>
106+
/// Gets or sets the SAML service providers.
107+
/// </summary>
108+
/// <value>
109+
/// The SAML service providers.
110+
/// </value>
111+
public DbSet<SamlServiceProvider> SamlServiceProviders { get; set; }
112+
105113
/// <summary>
106114
/// Override this method to further configure the model that was discovered by convention from the entity types
107115
/// exposed in <see cref="Microsoft.EntityFrameworkCore.DbSet{T}" /> properties on your derived context. The resulting model may be cached
@@ -130,6 +138,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
130138
modelBuilder.ConfigureClientContext(StoreOptions);
131139
modelBuilder.ConfigureResourcesContext(StoreOptions);
132140
modelBuilder.ConfigureIdentityProviderContext(StoreOptions);
141+
modelBuilder.ConfigureSamlServiceProviderContext(StoreOptions);
133142

134143
base.OnModelCreating(modelBuilder);
135144
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#pragma warning disable 1591
5+
6+
namespace Duende.IdentityServer.EntityFramework.Entities;
7+
8+
public class SamlServiceProvider
9+
{
10+
public int Id { get; set; }
11+
public string EntityId { get; set; }
12+
public string DisplayName { get; set; }
13+
public string Description { get; set; }
14+
public bool Enabled { get; set; } = true;
15+
16+
// TimeSpan? stored as ticks
17+
public long? ClockSkewTicks { get; set; }
18+
public long? RequestMaxAgeTicks { get; set; }
19+
20+
// ACS binding (enum stored as int)
21+
public int AssertionConsumerServiceBinding { get; set; }
22+
23+
// SLO endpoint (flattened from SamlEndpointType?)
24+
public string SingleLogoutServiceUrl { get; set; }
25+
public int? SingleLogoutServiceBinding { get; set; }
26+
27+
public bool RequireSignedAuthnRequests { get; set; }
28+
public bool EncryptAssertions { get; set; }
29+
public bool RequireConsent { get; set; }
30+
public bool AllowIdpInitiated { get; set; }
31+
32+
public string DefaultNameIdFormat { get; set; } = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
33+
public string DefaultPersistentNameIdentifierClaimType { get; set; }
34+
35+
// SamlSigningBehavior? stored as int?
36+
public int? SigningBehavior { get; set; }
37+
38+
// Navigation properties
39+
public List<SamlServiceProviderAssertionConsumerService> AssertionConsumerServiceUrls { get; set; }
40+
public List<SamlServiceProviderSigningCertificate> SigningCertificates { get; set; }
41+
public List<SamlServiceProviderEncryptionCertificate> EncryptionCertificates { get; set; }
42+
public List<SamlServiceProviderClaimMapping> ClaimMappings { get; set; }
43+
44+
// Audit fields (matching Client entity pattern)
45+
public DateTime Created { get; set; } = DateTime.UtcNow;
46+
public DateTime? Updated { get; set; }
47+
public DateTime? LastAccessed { get; set; }
48+
public bool NonEditable { get; set; }
49+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#pragma warning disable 1591
5+
6+
namespace Duende.IdentityServer.EntityFramework.Entities;
7+
8+
public class SamlServiceProviderAssertionConsumerService
9+
{
10+
public int Id { get; set; }
11+
public string Url { get; set; }
12+
public int SamlServiceProviderId { get; set; }
13+
public SamlServiceProvider SamlServiceProvider { get; set; }
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#pragma warning disable 1591
5+
6+
namespace Duende.IdentityServer.EntityFramework.Entities;
7+
8+
public class SamlServiceProviderClaimMapping
9+
{
10+
public int Id { get; set; }
11+
/// <summary>
12+
/// The claim type (dictionary key).
13+
/// </summary>
14+
public string ClaimType { get; set; }
15+
/// <summary>
16+
/// The SAML attribute name (dictionary value).
17+
/// </summary>
18+
public string SamlAttributeName { get; set; }
19+
public int SamlServiceProviderId { get; set; }
20+
public SamlServiceProvider SamlServiceProvider { get; set; }
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#pragma warning disable 1591
5+
6+
namespace Duende.IdentityServer.EntityFramework.Entities;
7+
8+
public class SamlServiceProviderEncryptionCertificate
9+
{
10+
public int Id { get; set; }
11+
/// <summary>
12+
/// Base64-encoded DER (raw bytes) of the X.509 certificate.
13+
/// </summary>
14+
public string Data { get; set; }
15+
public int SamlServiceProviderId { get; set; }
16+
public SamlServiceProvider SamlServiceProvider { get; set; }
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#pragma warning disable 1591
5+
6+
namespace Duende.IdentityServer.EntityFramework.Entities;
7+
8+
public class SamlServiceProviderSigningCertificate
9+
{
10+
public int Id { get; set; }
11+
/// <summary>
12+
/// Base64-encoded DER (raw bytes) of the X.509 certificate.
13+
/// </summary>
14+
public string Data { get; set; }
15+
public int SamlServiceProviderId { get; set; }
16+
public SamlServiceProvider SamlServiceProvider { get; set; }
17+
}

identity-server/src/EntityFramework.Storage/Extensions/ModelBuilderExtensions.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,76 @@ public static void ConfigureIdentityProviderContext(this ModelBuilder modelBuild
391391
entity.HasIndex(x => x.Scheme).IsUnique();
392392
});
393393
}
394+
395+
/// <summary>
396+
/// Configures the SAML service provider context.
397+
/// </summary>
398+
/// <param name="modelBuilder">The model builder.</param>
399+
/// <param name="storeOptions">The store options.</param>
400+
public static void ConfigureSamlServiceProviderContext(this ModelBuilder modelBuilder, ConfigurationStoreOptions storeOptions)
401+
{
402+
if (!string.IsNullOrWhiteSpace(storeOptions.DefaultSchema))
403+
{
404+
modelBuilder.HasDefaultSchema(storeOptions.DefaultSchema);
405+
}
406+
407+
modelBuilder.Entity<SamlServiceProvider>(sp =>
408+
{
409+
sp.ToTable(storeOptions.SamlServiceProvider);
410+
sp.HasKey(x => x.Id);
411+
412+
sp.Property(x => x.EntityId).HasMaxLength(200).IsRequired();
413+
sp.Property(x => x.DisplayName).HasMaxLength(200);
414+
sp.Property(x => x.Description).HasMaxLength(1000);
415+
sp.Property(x => x.DefaultNameIdFormat).HasMaxLength(500);
416+
sp.Property(x => x.DefaultPersistentNameIdentifierClaimType).HasMaxLength(500);
417+
sp.Property(x => x.SingleLogoutServiceUrl).HasMaxLength(2000);
418+
419+
sp.HasIndex(x => x.EntityId).IsUnique();
420+
421+
sp.HasMany(x => x.AssertionConsumerServiceUrls)
422+
.WithOne(x => x.SamlServiceProvider)
423+
.HasForeignKey(x => x.SamlServiceProviderId).IsRequired().OnDelete(DeleteBehavior.Cascade);
424+
sp.HasMany(x => x.SigningCertificates)
425+
.WithOne(x => x.SamlServiceProvider)
426+
.HasForeignKey(x => x.SamlServiceProviderId).IsRequired().OnDelete(DeleteBehavior.Cascade);
427+
sp.HasMany(x => x.EncryptionCertificates)
428+
.WithOne(x => x.SamlServiceProvider)
429+
.HasForeignKey(x => x.SamlServiceProviderId).IsRequired().OnDelete(DeleteBehavior.Cascade);
430+
sp.HasMany(x => x.ClaimMappings)
431+
.WithOne(x => x.SamlServiceProvider)
432+
.HasForeignKey(x => x.SamlServiceProviderId).IsRequired().OnDelete(DeleteBehavior.Cascade);
433+
});
434+
435+
modelBuilder.Entity<SamlServiceProviderAssertionConsumerService>(acs =>
436+
{
437+
acs.ToTable(storeOptions.SamlServiceProviderAssertionConsumerService);
438+
acs.HasKey(x => x.Id);
439+
acs.Property(x => x.Url).HasMaxLength(2000).IsRequired();
440+
acs.HasIndex(x => new { x.SamlServiceProviderId, x.Url }).IsUnique();
441+
});
442+
443+
modelBuilder.Entity<SamlServiceProviderSigningCertificate>(cert =>
444+
{
445+
cert.ToTable(storeOptions.SamlServiceProviderSigningCertificate);
446+
cert.HasKey(x => x.Id);
447+
cert.Property(x => x.Data).HasMaxLength(4000).IsRequired();
448+
});
449+
450+
modelBuilder.Entity<SamlServiceProviderEncryptionCertificate>(cert =>
451+
{
452+
cert.ToTable(storeOptions.SamlServiceProviderEncryptionCertificate);
453+
cert.HasKey(x => x.Id);
454+
cert.Property(x => x.Data).HasMaxLength(4000).IsRequired();
455+
});
456+
457+
modelBuilder.Entity<SamlServiceProviderClaimMapping>(mapping =>
458+
{
459+
mapping.ToTable(storeOptions.SamlServiceProviderClaimMapping);
460+
mapping.HasKey(x => x.Id);
461+
mapping.Property(x => x.ClaimType).HasMaxLength(250).IsRequired();
462+
mapping.Property(x => x.SamlAttributeName).HasMaxLength(250).IsRequired();
463+
mapping.HasIndex(x => new { x.SamlServiceProviderId, x.ClaimType }).IsUnique();
464+
});
465+
}
394466
}

identity-server/src/EntityFramework.Storage/Interfaces/IConfigurationDbContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ public interface IConfigurationDbContext : IDisposable
6363
/// </value>
6464
DbSet<IdentityProvider> IdentityProviders { get; set; }
6565

66+
/// <summary>
67+
/// Gets or sets the SAML service providers.
68+
/// </summary>
69+
/// <value>
70+
/// The SAML service providers.
71+
/// </value>
72+
DbSet<SamlServiceProvider> SamlServiceProviders { get; set; }
73+
6674
/// <summary>
6775
/// Saves the changes.
6876
/// </summary>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
using Duende.IdentityServer.EntityFramework.Entities;
6+
using Duende.IdentityServer.Models;
7+
8+
namespace Duende.IdentityServer.EntityFramework.Mappers;
9+
10+
/// <summary>
11+
/// Extension methods to map to/from entity/model for SAML service providers.
12+
/// </summary>
13+
public static class SamlServiceProviderMappers
14+
{
15+
/// <summary>
16+
/// Maps an entity to a model.
17+
/// </summary>
18+
/// <param name="entity">The entity.</param>
19+
/// <returns></returns>
20+
public static Models.SamlServiceProvider ToModel(this Entities.SamlServiceProvider entity) =>
21+
new Models.SamlServiceProvider
22+
{
23+
EntityId = entity.EntityId,
24+
DisplayName = entity.DisplayName,
25+
Description = entity.Description,
26+
Enabled = entity.Enabled,
27+
ClockSkew = entity.ClockSkewTicks.HasValue
28+
? TimeSpan.FromTicks(entity.ClockSkewTicks.Value) : null,
29+
RequestMaxAge = entity.RequestMaxAgeTicks.HasValue
30+
? TimeSpan.FromTicks(entity.RequestMaxAgeTicks.Value) : null,
31+
AssertionConsumerServiceBinding = (SamlBinding)entity.AssertionConsumerServiceBinding,
32+
AssertionConsumerServiceUrls = entity.AssertionConsumerServiceUrls?
33+
.Select(a => new Uri(a.Url)).ToHashSet() ?? new HashSet<Uri>(),
34+
SingleLogoutServiceUrl = entity.SingleLogoutServiceUrl != null
35+
? new SamlEndpointType
36+
{
37+
Location = new Uri(entity.SingleLogoutServiceUrl),
38+
Binding = (SamlBinding)entity.SingleLogoutServiceBinding!.Value
39+
} : null,
40+
RequireSignedAuthnRequests = entity.RequireSignedAuthnRequests,
41+
#pragma warning disable SYSLIB0057 // Type or member is obsolete
42+
// TODO - Use X509CertificateLoader in a future release (when we drop NET8 support)
43+
SigningCertificates = entity.SigningCertificates?
44+
.Select(c => new X509Certificate2(Convert.FromBase64String(c.Data))).ToList(),
45+
EncryptionCertificates = entity.EncryptionCertificates?
46+
.Select(c => new X509Certificate2(Convert.FromBase64String(c.Data))).ToList(),
47+
#pragma warning restore SYSLIB0057 // Type or member is obsolete
48+
EncryptAssertions = entity.EncryptAssertions,
49+
RequireConsent = entity.RequireConsent,
50+
AllowIdpInitiated = entity.AllowIdpInitiated,
51+
ClaimMappings = entity.ClaimMappings?
52+
.ToDictionary(m => m.ClaimType, m => m.SamlAttributeName)
53+
?? new Dictionary<string, string>(),
54+
DefaultNameIdFormat = entity.DefaultNameIdFormat,
55+
DefaultPersistentNameIdentifierClaimType = entity.DefaultPersistentNameIdentifierClaimType,
56+
SigningBehavior = entity.SigningBehavior.HasValue
57+
? (SamlSigningBehavior)entity.SigningBehavior.Value : null,
58+
};
59+
60+
/// <summary>
61+
/// Maps a model to an entity.
62+
/// </summary>
63+
/// <param name="model">The model.</param>
64+
/// <returns></returns>
65+
public static Entities.SamlServiceProvider ToEntity(this Models.SamlServiceProvider model) =>
66+
new Entities.SamlServiceProvider
67+
{
68+
EntityId = model.EntityId,
69+
DisplayName = model.DisplayName,
70+
Description = model.Description,
71+
Enabled = model.Enabled,
72+
ClockSkewTicks = model.ClockSkew?.Ticks,
73+
RequestMaxAgeTicks = model.RequestMaxAge?.Ticks,
74+
AssertionConsumerServiceBinding = (int)model.AssertionConsumerServiceBinding,
75+
AssertionConsumerServiceUrls = model.AssertionConsumerServiceUrls?
76+
.Select(u => new SamlServiceProviderAssertionConsumerService { Url = u.AbsoluteUri })
77+
.ToList() ?? new List<SamlServiceProviderAssertionConsumerService>(),
78+
SingleLogoutServiceUrl = model.SingleLogoutServiceUrl?.Location.AbsoluteUri,
79+
SingleLogoutServiceBinding = model.SingleLogoutServiceUrl != null
80+
? (int)model.SingleLogoutServiceUrl.Binding : null,
81+
RequireSignedAuthnRequests = model.RequireSignedAuthnRequests,
82+
SigningCertificates = model.SigningCertificates?
83+
.Select(c => new SamlServiceProviderSigningCertificate
84+
{
85+
Data = Convert.ToBase64String(c.RawData)
86+
}).ToList() ?? new List<SamlServiceProviderSigningCertificate>(),
87+
EncryptionCertificates = model.EncryptionCertificates?
88+
.Select(c => new SamlServiceProviderEncryptionCertificate
89+
{
90+
Data = Convert.ToBase64String(c.RawData)
91+
}).ToList() ?? new List<SamlServiceProviderEncryptionCertificate>(),
92+
EncryptAssertions = model.EncryptAssertions,
93+
RequireConsent = model.RequireConsent,
94+
AllowIdpInitiated = model.AllowIdpInitiated,
95+
ClaimMappings = model.ClaimMappings?
96+
.Select(kvp => new SamlServiceProviderClaimMapping
97+
{
98+
ClaimType = kvp.Key,
99+
SamlAttributeName = kvp.Value
100+
}).ToList() ?? new List<SamlServiceProviderClaimMapping>(),
101+
DefaultNameIdFormat = model.DefaultNameIdFormat,
102+
DefaultPersistentNameIdentifierClaimType = model.DefaultPersistentNameIdentifierClaimType,
103+
SigningBehavior = model.SigningBehavior.HasValue
104+
? (int)model.SigningBehavior.Value : null,
105+
};
106+
}

identity-server/src/EntityFramework.Storage/Options/ConfigurationStoreOptions.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,35 @@ public class ConfigurationStoreOptions
213213
/// </summary>
214214
public TableConfiguration IdentityProvider { get; set; } = new TableConfiguration("IdentityProviders");
215215

216+
/// <summary>
217+
/// Gets or sets the SAML service providers table configuration.
218+
/// </summary>
219+
public TableConfiguration SamlServiceProvider { get; set; } = new TableConfiguration("SamlServiceProviders");
220+
221+
/// <summary>
222+
/// Gets or sets the SAML service provider assertion consumer services table configuration.
223+
/// </summary>
224+
public TableConfiguration SamlServiceProviderAssertionConsumerService { get; set; } =
225+
new TableConfiguration("SamlServiceProviderAssertionConsumerServices");
226+
227+
/// <summary>
228+
/// Gets or sets the SAML service provider signing certificates table configuration.
229+
/// </summary>
230+
public TableConfiguration SamlServiceProviderSigningCertificate { get; set; } =
231+
new TableConfiguration("SamlServiceProviderSigningCertificates");
232+
233+
/// <summary>
234+
/// Gets or sets the SAML service provider encryption certificates table configuration.
235+
/// </summary>
236+
public TableConfiguration SamlServiceProviderEncryptionCertificate { get; set; } =
237+
new TableConfiguration("SamlServiceProviderEncryptionCertificates");
238+
239+
/// <summary>
240+
/// Gets or sets the SAML service provider claim mappings table configuration.
241+
/// </summary>
242+
public TableConfiguration SamlServiceProviderClaimMapping { get; set; } =
243+
new TableConfiguration("SamlServiceProviderClaimMappings");
244+
216245
/// <summary>
217246
/// Gets or set if EF DbContext pooling is enabled.
218247
/// </summary>

0 commit comments

Comments
 (0)