From e54d4c095aa1adf72cb7b6e9d68fd176c72b9cc1 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 2 Oct 2026 13:44:26 +0200 Subject: [PATCH 1/3] Update preview samples to IdentityServer 8.1.0-preview.4 and User Management 2.0.0-preview.3 Migrate Storage, AccountLockout and Spaces to the new storage registration APIs (AddStorage + AddConfigurationStorage/AddOperationalStorage, IStorageInstanceSchema, SchemaId.OidcIdentityProvider). Lengthen the Spaces demo password to meet the new 15-character minimum. --- IdentityServer/v8/Spaces/Directory.Packages.props | 10 +++++----- IdentityServer/v8/Spaces/README.md | 2 +- IdentityServer/v8/Spaces/src/Spaces/Program.cs | 8 ++++---- .../v8/Spaces/src/Spaces/Seed/DemoCredentials.cs | 2 +- .../v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs | 2 +- IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs | 2 +- .../v8/Spaces/src/Spaces/appsettings.Development.json | 2 +- .../tests/Spaces.PlaywrightTests/Tests/LoginTests.cs | 4 ++-- IdentityServer/v8/Storage/Storage.csproj | 4 ++-- .../AccountLockout/AccountLockout.csproj | 6 +++--- .../AccountLockout/AccountLockout/Program.cs | 6 +++--- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/IdentityServer/v8/Spaces/Directory.Packages.props b/IdentityServer/v8/Spaces/Directory.Packages.props index abbdbcd4..c7d4781b 100644 --- a/IdentityServer/v8/Spaces/Directory.Packages.props +++ b/IdentityServer/v8/Spaces/Directory.Packages.props @@ -4,11 +4,11 @@ true - - - - - + + + + + diff --git a/IdentityServer/v8/Spaces/README.md b/IdentityServer/v8/Spaces/README.md index 7115af12..11e87faf 100644 --- a/IdentityServer/v8/Spaces/README.md +++ b/IdentityServer/v8/Spaces/README.md @@ -60,7 +60,7 @@ Since origin based routing takes precedence, navigating to https://space1.dev.lo ## Demo Credentials -All passwords are `Pa$$Word123`. +All passwords are `Pa$$Word123456789`. | Space | Space-Specific User | Shared User | |-------|-------------------|-------------| diff --git a/IdentityServer/v8/Spaces/src/Spaces/Program.cs b/IdentityServer/v8/Spaces/src/Spaces/Program.cs index f08af109..ef1cd158 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Program.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Program.cs @@ -38,14 +38,14 @@ options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; }) + .AddStorage(x => x.AddSqliteInMemory()) + .AddConfigurationStorage() + .AddOperationalStorage() .AddUserManagement(um => { um.Authentication(x => { }); - - um.AddSqliteInMemoryStore(); }) .AddProfileService() - .AddStorage(x => x.AddSqliteInMemoryStore()) .AddInMemoryDataExtensionSchemas([new SchemaConfiguration() { SchemaId = SchemaId.UserProfile, @@ -67,7 +67,7 @@ { var sp = scope.ServiceProvider; - await sp.GetRequiredService().MigrateAsync(CancellationToken.None); + await sp.GetRequiredService().MigrateAsync(CancellationToken.None); await new SeedData(sp).Seed(CancellationToken.None); } diff --git a/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoCredentials.cs b/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoCredentials.cs index d53ee8df..afb9f79e 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoCredentials.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoCredentials.cs @@ -10,7 +10,7 @@ namespace Spaces.Seed; public static class DemoCredentials { /// Demo password shared across all seeded users. - public const string Password = "Pa$$Word123"; + public const string Password = "Pa$$Word123456789"; /// /// Returns the demo username for a given space, or null if no demo user is seeded for that space. diff --git a/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs b/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs index 9a4ccee6..a8566863 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs @@ -81,7 +81,7 @@ internal static class OidcProviderSchema /// public static readonly SchemaConfiguration Schema = new() { - SchemaId = SchemaId.IdentityProvider("oidc"), + SchemaId = SchemaId.OidcIdentityProvider, DisplayName = "OIDC Identity Provider", Description = "Built-in schema for OpenID Connect identity providers.", AttributeDefinitions = diff --git a/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs b/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs index 0d59c117..0ae7330c 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs @@ -68,7 +68,7 @@ private async Task CreateIdp(SpaceId spaceId, string name, CancellationToken ct) var spacedService = services.GetServiceForSpace(spaceId); var schemaStore = services.GetRequiredService(); - var oidcSchema = await schemaStore.GetAsync(SchemaId.IdentityProvider("oidc"), ct); + var oidcSchema = await schemaStore.GetAsync(SchemaId.OidcIdentityProvider, ct); var values = new AttributeValueCollection(oidcSchema); values.Set(OidcProviderSchema.Authority, "https://demo.duendesoftware.com"); values.Set(OidcProviderSchema.ClientId, "interactive.confidential"); diff --git a/IdentityServer/v8/Spaces/src/Spaces/appsettings.Development.json b/IdentityServer/v8/Spaces/src/Spaces/appsettings.Development.json index c8fe18bb..6f8ab54b 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/appsettings.Development.json +++ b/IdentityServer/v8/Spaces/src/Spaces/appsettings.Development.json @@ -11,7 +11,7 @@ // ⚠ LOCAL DEVELOPMENT DEFAULTS — demo credentials for the sample only. // Never copy these values to a production or staging environment. "Demo": { - "Password": "Pa$$Word123", + "Password": "Pa$$Word123456789", "ClientSecret": "secret", "ShowCredentials": true } diff --git a/IdentityServer/v8/Spaces/tests/Spaces.PlaywrightTests/Tests/LoginTests.cs b/IdentityServer/v8/Spaces/tests/Spaces.PlaywrightTests/Tests/LoginTests.cs index d1604f07..37b5ab4a 100644 --- a/IdentityServer/v8/Spaces/tests/Spaces.PlaywrightTests/Tests/LoginTests.cs +++ b/IdentityServer/v8/Spaces/tests/Spaces.PlaywrightTests/Tests/LoginTests.cs @@ -14,7 +14,7 @@ public async Task Can_login_to_default_space() await page.GotoAsync(fixture.BaseUrl); await page.ClickAsync("a:has-text('Sign in')"); await page.FillAsync("[name=Username]", "default-user"); - await page.FillAsync("[name=Password]", "Pa$$Word123"); + await page.FillAsync("[name=Password]", "Pa$$Word123456789"); await page.ClickAsync("button[type=submit]"); await page.WaitForURLAsync(url => !url.Contains("/Account/Login")); @@ -30,7 +30,7 @@ public async Task Can_login_to_space1_via_hostname() await page.ClickAsync("a:has-text('space1.dev.localhost')"); await page.ClickAsync("a:has-text('Sign in')"); await page.FillAsync("[name=Username]", "space1-user"); - await page.FillAsync("[name=Password]", "Pa$$Word123"); + await page.FillAsync("[name=Password]", "Pa$$Word123456789"); await page.ClickAsync("button[type=submit]"); await page.WaitForURLAsync(url => !url.Contains("/Account/Login")); diff --git a/IdentityServer/v8/Storage/Storage.csproj b/IdentityServer/v8/Storage/Storage.csproj index eabd93e5..753772ce 100644 --- a/IdentityServer/v8/Storage/Storage.csproj +++ b/IdentityServer/v8/Storage/Storage.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/AccountLockout.csproj b/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/AccountLockout.csproj index f9c7ac08..13e40b0a 100644 --- a/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/AccountLockout.csproj +++ b/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/AccountLockout.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/Program.cs b/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/Program.cs index 3927d1d1..8175290a 100644 --- a/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/Program.cs +++ b/IdentityServer/v8/UserManagement/AccountLockout/AccountLockout/Program.cs @@ -61,6 +61,8 @@ options.Events.RaiseSuccessEvents = true; }) .AddDeveloperSigningCredential(persistKey: false) + .AddStorage(storage => storage.AddSqlite(options => options.ConnectionString = $"Data Source={databasePath}")) + .AddDynamicSchemas() .AddUserManagement(userManagement => { userManagement.Authentication(authentication => @@ -74,8 +76,6 @@ _ = authentication.UseSmtpOtpDispatcher(options => builder.Configuration.GetSection("Smtp").Bind(options)); }); - - userManagement.AddSqliteStore(options => options.ConnectionString = $"Data Source={databasePath}"); }); // Replace the default passkey sign-in step so policy runs after WebAuthn verification, @@ -89,7 +89,7 @@ var services = scope.ServiceProvider; // A sample starts from an empty SQLite database, so create storage, schema, and users in order. - await services.GetRequiredService().MigrateAsync(CancellationToken.None); + await services.GetRequiredService().MigrateAsync(CancellationToken.None); await services.GetRequiredService().BootstrapAsync(CancellationToken.None); await services.GetRequiredService().SeedAsync(CancellationToken.None); } From b7640ac062dd9ed5b10729202ddffb6852c17c40 Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 2 Oct 2026 13:44:26 +0200 Subject: [PATCH 2/3] Demonstrate multiple storage instances in the Storage sample Store configuration and operational data in separate named storage instances and migrate each one separately. --- IdentityServer/v8/Storage/Program.cs | 20 +++++++++-- IdentityServer/v8/Storage/README.md | 42 ++++++++++++++++++---- IdentityServer/v8/Storage/SamplePage.cs | 16 +++++++-- IdentityServer/v8/Storage/SampleStorage.cs | 16 +++++++++ 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 IdentityServer/v8/Storage/SampleStorage.cs diff --git a/IdentityServer/v8/Storage/Program.cs b/IdentityServer/v8/Storage/Program.cs index 3c3f4d86..4c1d7c41 100644 --- a/IdentityServer/v8/Storage/Program.cs +++ b/IdentityServer/v8/Storage/Program.cs @@ -17,7 +17,6 @@ using Storage; const string defaultBaseUrl = "https://localhost:5006"; -const string databaseName = "IdentityServerStorageSample"; var builder = WebApplication.CreateBuilder(args); var publicBaseUri = GetPublicBaseUri(builder.Configuration["SampleBaseUrl"] ?? defaultBaseUrl); @@ -41,14 +40,29 @@ }) .AddDeveloperSigningCredential(persistKey: false) .AddServerSideSessions() - .AddStorage(storage => storage.AddSqliteInMemoryStore(databaseName)) + // AddStorage only registers a database for a named instance; it does not route any + // IdentityServer data to it. AddConfigurationStorage/AddOperationalStorage below route + // each data category to one of these two databases. A category left unmapped would fall + // back to a default instance, but this sample maps both categories explicitly, so no + // default instance is registered. + .AddStorage(SampleStorage.Configuration, storage => storage.AddSqliteInMemory("IdentityServerStorageSample-Configuration")) + .AddStorage(SampleStorage.Operational, storage => storage.AddSqliteInMemory("IdentityServerStorageSample-Operational")) + .AddConfigurationStorage(SampleStorage.Configuration) + .AddOperationalStorage(SampleStorage.Operational) .AddInMemoryDataExtensionSchemas([SampleData.ClientSchema]) .AddTestUsers(SampleUsers.Users) .AddCustomTokenRequestValidator(); var app = builder.Build(); -await app.Services.GetRequiredService().MigrateAsync(CancellationToken.None); +// Each storage instance is a separate database and is migrated separately. +var schemaFactory = app.Services.GetRequiredService(); +foreach (var instance in new[] { SampleStorage.Configuration, SampleStorage.Operational }) +{ + var schema = await schemaFactory.GetStorageInstanceSchema(instance, CancellationToken.None); + await schema.MigrateAsync(CancellationToken.None); +} + await SampleData.InitializeAsync(app.Services, publicBaseUrl, CancellationToken.None); app.UseHostFiltering(); diff --git a/IdentityServer/v8/Storage/README.md b/IdentityServer/v8/Storage/README.md index 6c9e20cc..fcc3b8df 100644 --- a/IdentityServer/v8/Storage/README.md +++ b/IdentityServer/v8/Storage/README.md @@ -1,6 +1,6 @@ # Duende Storage sample -This sample runs IdentityServer with configuration and operational data stored in a shared in-memory SQLite database through Duende Storage. It uses the IdentityServer administration APIs to create all IdentityServer configuration at startup and registers a fixed in-memory schema for client extension properties. +This sample runs IdentityServer with configuration and operational data stored in two separate in-memory SQLite databases through Duende Storage. It uses the IdentityServer administration APIs to create all IdentityServer configuration at startup and registers a fixed in-memory schema for client extension properties. The sample demonstrates: @@ -8,15 +8,16 @@ The sample demonstrates: - creating scopes, identity resources, clients, secrets, and extension-property values with the administration APIs; - obtaining a client-credentials token from configuration loaded through Duende Storage; - enforcing an `allowed_region` client extension property in a custom token-request validator; -- signing in a test user with authorization code and PKCE to create a storage-backed server-side session; and -- searching stored clients and inspecting stored sessions. +- signing in a test user with authorization code and PKCE to create a storage-backed server-side session; +- searching stored clients and inspecting stored sessions; and +- routing IdentityServer configuration data and operational data to two independently registered and migrated storage instances. ## Run the sample The sample uses the latest prerelease packages required by this functionality: -- `Duende.IdentityServer` `8.1.0-preview.3` -- `Duende.Storage.Sqlite` `2.0.0-preview.2` +- `Duende.IdentityServer` `8.1.0-preview.4` +- `Duende.Storage.Sqlite` `2.0.0-preview.3` From this directory, run: @@ -24,7 +25,36 @@ From this directory, run: dotnet run ``` -Open `https://localhost:5006` if the browser does not open automatically. The database is held in memory and is reset whenever the process stops. +Open `https://localhost:5006` if the browser does not open automatically. Both databases are held in memory and reset whenever the process stops. + +## Multiple storage instances + +`SampleStorage` names two Duende Storage instances: `configuration` and `operational`. `Program.cs` registers a separate in-memory SQLite database for each and routes IdentityServer's data categories to them: + +```csharp +builder.Services + .AddIdentityServer() + // ... + .AddStorage(SampleStorage.Configuration, storage => storage.AddSqliteInMemory("IdentityServerStorageSample-Configuration")) + .AddStorage(SampleStorage.Operational, storage => storage.AddSqliteInMemory("IdentityServerStorageSample-Operational")) + .AddConfigurationStorage(SampleStorage.Configuration) + .AddOperationalStorage(SampleStorage.Operational); +``` + +`AddStorage` only registers a database for a named instance; it does not route any data to it. `AddConfigurationStorage`/`AddOperationalStorage` route a data category (clients, identity providers, and resources for configuration; persisted grants, server-side sessions, and keys for operational) to the instance passed to them. A data category left unmapped falls back to a default instance, but this sample maps both categories explicitly, so it registers no default instance. + +Each instance is a separate database and must be migrated separately, through `IStorageInstanceSchemaFactory`: + +```csharp +var schemaFactory = app.Services.GetRequiredService(); +foreach (var instance in new[] { SampleStorage.Configuration, SampleStorage.Operational }) +{ + var schema = await schemaFactory.GetStorageInstanceSchema(instance, CancellationToken.None); + await schema.MigrateAsync(CancellationToken.None); +} +``` + +In production, each instance would typically be a separate database and connection string, for example `AddPostgreSql()` for one instance and `AddMsSql()` for another, migrated separately and independently scaled. Each instance can use a different storage provider. ## Client credentials and the region extension diff --git a/IdentityServer/v8/Storage/SamplePage.cs b/IdentityServer/v8/Storage/SamplePage.cs index 246fb932..86b98cab 100644 --- a/IdentityServer/v8/Storage/SamplePage.cs +++ b/IdentityServer/v8/Storage/SamplePage.cs @@ -31,9 +31,19 @@ internal static string Create(string baseUrl)

Storage-backed IdentityServer

- This host stores IdentityServer configuration and operational data in an in-memory SQLite database. - Startup registers a fixed in-memory schema, then creates scopes, identity resources, clients, and typed - client extension-property values through the administration APIs. + This host stores IdentityServer configuration and operational data in two separate in-memory + SQLite databases, one per named storage instance. Startup registers a fixed in-memory schema, + then creates scopes, identity resources, clients, and typed client extension-property values + through the administration APIs. +

+ +

Storage instances

+

+ This sample registers two named Duende Storage instances, each its own in-memory SQLite + database migrated separately: {{SampleStorage.Configuration}} holds clients, + identity resources, and API scopes; {{SampleStorage.Operational}} holds server-side + sessions, persisted grants, and signing keys. /sample/clients reads from the + configuration instance and /sample/sessions reads from the operational instance.

Inspect configuration

diff --git a/IdentityServer/v8/Storage/SampleStorage.cs b/IdentityServer/v8/Storage/SampleStorage.cs new file mode 100644 index 00000000..4aaeba76 --- /dev/null +++ b/IdentityServer/v8/Storage/SampleStorage.cs @@ -0,0 +1,16 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Duende.Storage; + +namespace Storage; + +/// +/// Names the two Duende Storage instances this sample registers: one for IdentityServer +/// configuration data and one for operational data. +/// +internal static class SampleStorage +{ + internal static readonly StorageInstanceId Configuration = StorageInstanceId.Create("configuration"); + internal static readonly StorageInstanceId Operational = StorageInstanceId.Create("operational"); +} From 33df2cdd77d93ddf81777f249480e9e1b93c226c Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Fri, 2 Oct 2026 14:08:54 +0200 Subject: [PATCH 3/3] Use the built-in OIDC provider and user profile schemas in the Spaces sample Replace the copied OIDC provider schema with DefaultOidcProviderSchema, and extend BuiltInSchemas.UserProfile with the username attribute instead of redefining email. The built-in email is required and unique, so seeded users get an email and external users without one get a per-user placeholder. --- .../Account/ExternalLoginCallback.cshtml.cs | 10 +-- .../v8/Spaces/src/Spaces/Program.cs | 8 +- .../src/Spaces/Seed/DemoUserAttributes.cs | 79 +------------------ .../v8/Spaces/src/Spaces/Seed/SeedData.cs | 16 ++-- 4 files changed, 20 insertions(+), 93 deletions(-) diff --git a/IdentityServer/v8/Spaces/src/Spaces/Pages/Account/ExternalLoginCallback.cshtml.cs b/IdentityServer/v8/Spaces/src/Spaces/Pages/Account/ExternalLoginCallback.cshtml.cs index d81ee4ff..ecf83d85 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Pages/Account/ExternalLoginCallback.cshtml.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Pages/Account/ExternalLoginCallback.cshtml.cs @@ -67,16 +67,16 @@ public async Task OnGetAsync(string? returnUrl) ?? principal.FindFirst(ClaimTypes.Name)?.Value ?? "name"; + // The built-in user profile schema requires a unique email. Fall back to a + // per-user placeholder when the external provider doesn't supply one. var email = principal.FindFirst(JwtClaimTypes.Email)?.Value - ?? principal.FindFirst(ClaimTypes.Email)?.Value ?? "test@test.nl"; + ?? principal.FindFirst(ClaimTypes.Email)?.Value + ?? $"{userId}@example.invalid"; var schema = await profileSelfService.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set(DemoUserAttributes.UserName, name); - if (email is not null) - { - attributes.Set(DemoUserAttributes.Email, email); - } + attributes.Set(OidcStandardAttributes.Email, email); var saved = await profileSelfService.TryCreateAsync(userId, attributes.Validate(), ct); if (saved is null) diff --git a/IdentityServer/v8/Spaces/src/Spaces/Program.cs b/IdentityServer/v8/Spaces/src/Spaces/Program.cs index ef1cd158..f9c06f4f 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Program.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Program.cs @@ -46,11 +46,9 @@ um.Authentication(x => { }); }) .AddProfileService() - .AddInMemoryDataExtensionSchemas([new SchemaConfiguration() - { - SchemaId = SchemaId.UserProfile, - AttributeDefinitions = [DemoUserAttributes.Email, DemoUserAttributes.UserName] - }]); + // The built-in user profile schema already contains email, name, given_name and + // family_name. Extend it with the unique username used for password login. + .AddInMemoryDataExtensionSchemas([BuiltInSchemas.UserProfile.Extend(DemoUserAttributes.UserName)]); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs b/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs index a8566863..a385e959 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Seed/DemoUserAttributes.cs @@ -1,14 +1,13 @@ // Copyright (c) Duende Software. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using Duende.IdentityServer.Models; -using Duende.IdentityServer.Stores.Storage; using Duende.Storage.EntityAttributeValue; namespace Spaces.Seed; /// -/// Set of example attributes that are used during authentication +/// Example attributes added to the built-in user profile schema, which already contains +/// email, name, given_name and family_name. /// public class DemoUserAttributes { @@ -21,78 +20,4 @@ public class DemoUserAttributes // for username / password authentication. IsUnique = true }; - - public static readonly AttributeDefinition Email = new() - { - Code = AttributeCode.Create("Email"), - AttributeType = new ScalarAttributeType(ScalarDataType.String), - Description = AttributeDescription.Create("The Email of the user") - }; -} - -internal static class OidcProviderSchema -{ - /// The base address of the OIDC provider (e.g. https://idp.example.com). - public static readonly TypedAttributeDefinition Authority = - new(AttributeCode.Create("Authority"), new ScalarAttributeType(ScalarDataType.String)); - - /// The client ID used to authenticate with the external OIDC provider. - public static readonly TypedAttributeDefinition ClientId = - new(AttributeCode.Create("ClientId"), new ScalarAttributeType(ScalarDataType.String)); - - /// The client secret used to authenticate with the external OIDC provider. - public static readonly TypedAttributeDefinition ClientSecret = - new(AttributeCode.Create("ClientSecret"), new ScalarAttributeType(ScalarDataType.String)); - - /// The response type (e.g. id_token). Defaults to id_token if not set. - public static readonly TypedAttributeDefinition ResponseType = - new(AttributeCode.Create("ResponseType"), new ScalarAttributeType(ScalarDataType.String)); - - /// Space-separated scope values (e.g. openid profile). Defaults to openid if not set. - public static readonly TypedAttributeDefinition Scope = - new(AttributeCode.Create("Scope"), new ScalarAttributeType(ScalarDataType.String)); - - /// - /// Whether to contact the userinfo endpoint. Stored as "true" or "false". - /// Defaults to true if not set. - /// - /// - /// Defined as string (not bool) because reads these - /// values from the Properties dictionary via string comparison, and - /// must round-trip them as strings. - /// - public static readonly TypedAttributeDefinition GetClaimsFromUserInfoEndpoint = - new(AttributeCode.Create("GetClaimsFromUserInfoEndpoint"), new ScalarAttributeType(ScalarDataType.String)); - - /// - /// Whether PKCE should be used. Stored as "true" or "false". - /// Defaults to true if not set. - /// - /// - /// Defined as string (not bool) because reads these - /// values from the Properties dictionary via string comparison, and - /// must round-trip them as strings. - /// - public static readonly TypedAttributeDefinition UsePkce = - new(AttributeCode.Create("UsePkce"), new ScalarAttributeType(ScalarDataType.String)); - - /// - /// The built-in schema for OIDC identity providers, registered with schema ID idp:oidc. - /// - public static readonly SchemaConfiguration Schema = new() - { - SchemaId = SchemaId.OidcIdentityProvider, - DisplayName = "OIDC Identity Provider", - Description = "Built-in schema for OpenID Connect identity providers.", - AttributeDefinitions = - [ - Authority, - ClientId, - ClientSecret, - ResponseType, - Scope, - GetClaimsFromUserInfoEndpoint, - UsePkce - ] - }; } diff --git a/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs b/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs index 0ae7330c..6368078f 100644 --- a/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs +++ b/IdentityServer/v8/Spaces/src/Spaces/Seed/SeedData.cs @@ -7,10 +7,12 @@ using Duende.IdentityServer.Admin.IdentityProviders; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores.Storage; +using Duende.IdentityServer.Stores.Storage.IdentityProviders; using Duende.Spaces; using Duende.Storage.EntityAttributeValue; using Duende.UserManagement; using Duende.UserManagement.Authentication; +using Duende.UserManagement.Profiles; namespace Spaces.Seed; @@ -70,11 +72,11 @@ private async Task CreateIdp(SpaceId spaceId, string name, CancellationToken ct) var oidcSchema = await schemaStore.GetAsync(SchemaId.OidcIdentityProvider, ct); var values = new AttributeValueCollection(oidcSchema); - values.Set(OidcProviderSchema.Authority, "https://demo.duendesoftware.com"); - values.Set(OidcProviderSchema.ClientId, "interactive.confidential"); - values.Set(OidcProviderSchema.ClientSecret, "secret"); - values.Set(OidcProviderSchema.ResponseType, "code"); - values.Set(OidcProviderSchema.GetClaimsFromUserInfoEndpoint, "true"); + values.Set(DefaultOidcProviderSchema.Authority, "https://demo.duendesoftware.com"); + values.Set(DefaultOidcProviderSchema.ClientId, "interactive.confidential"); + values.Set(DefaultOidcProviderSchema.ClientSecret, "secret"); + values.Set(DefaultOidcProviderSchema.ResponseType, "code"); + values.Set(DefaultOidcProviderSchema.GetClaimsFromUserInfoEndpoint, "true"); var saveResult = await spacedService.Service.CreateAsync(new CreateIdentityProvider() { @@ -120,9 +122,11 @@ private async Task CreateUser(SpaceId spaceId, string userName, CancellationToke using var spacedUserAdmin = services.GetServiceForSpace(spaceId); var profileAdmin = spacedUserAdmin.Service.Profiles; - // First, we'll need to create a profile for the user. It has a unique username. + // First, we'll need to create a profile for the user. It has a unique username, and the + // built-in schema requires a unique email. var attributes = new AttributeValueCollection(await profileAdmin.GetSchemaAsync(ct)); attributes.Set(DemoUserAttributes.UserName, userName); // This username is used for username / password login. + attributes.Set(OidcStandardAttributes.Email, $"{userName}@example.com"); var userSubjectId = UserSubjectId.New(); var saved = await profileAdmin.TryAddAsync(userSubjectId, attributes.Validate(), ct);