Files
simpleidp/Program.cs
T
Marek Lesko e4c25bfaa7 Extract IDP configuration to external JSON file
Move all hard-coded identity server settings (API resource, scope, client,
users/roles) from Program.cs into config/idp-config.json and add parsing
logic. The config file is copied to the output directory so it works for
local development and is available at /app/config/idp-config.json in the
Docker image, making it straightforward to override via a volume mount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 11:22:33 +00:00

201 lines
6.8 KiB
C#

// Copyright (c) SimpleIdServer. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SimpleIdServer.IdServer.Builders;
using SimpleIdServer.IdServer.Config;
using SimpleIdServer.IdServer.Domains;
using SimpleIdServer.IdServer.Helpers;
using SimpleIdServer.IdServer.Stores;
var corsPolicyName = "AllowAll";
// ── Load configuration ────────────────────────────────────────────────────────
var configPath = Path.Combine(AppContext.BaseDirectory, "config", "idp-config.json");
if (!File.Exists(configPath))
throw new FileNotFoundException(
$"IDP configuration file not found at '{configPath}'. " +
"Mount your config file to /app/config/idp-config.json when running in Docker.");
var idpConfig = JsonSerializer.Deserialize<IdpConfig>(
File.ReadAllText(configPath),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidOperationException("Failed to deserialize IDP configuration.");
// ── Build realm ───────────────────────────────────────────────────────────────
var realm = RealmBuilder.CreateMaster().Build();
// ── Build API resource ────────────────────────────────────────────────────────
var api = new ApiResource
{
Realms = { realm },
Name = idpConfig.Api.Name,
Audience = idpConfig.Api.Audience
};
// ── Build custom scope with claim mappers ─────────────────────────────────────
var biScope = new Scope
{
Realms = { realm },
ApiResources = { api },
Protocol = ScopeProtocols.OAUTH,
Type = ScopeTypes.APIRESOURCE,
Name = idpConfig.Scope.Name,
Description = idpConfig.Scope.Description,
ClaimMappers = idpConfig.Scope.ClaimMappers.Select(cm => new ScopeClaimMapper
{
IncludeInAccessToken = true,
TokenClaimJsonType = Enum.Parse<TokenClaimJsonTypes>(cm.TokenClaimJsonType, ignoreCase: true),
TargetClaimPath = cm.TargetClaimPath,
MapperType = MappingRuleTypes.USERATTRIBUTE,
IsMultiValued = cm.IsMultiValued,
SourceUserAttribute = cm.SourceUserAttribute,
SourceUserProperty = cm.SourceUserAttribute,
}).ToList(),
};
api.Scopes.Add(biScope);
api.Realms.Add(realm);
var scopes = new List<Scope>
{
new Scope("openid") { Realms = { realm } },
new Scope("profile") { Realms = { realm } },
new Scope("offline_access") { Realms = { realm } },
biScope
};
// ── Build client ──────────────────────────────────────────────────────────────
var client = ClientBuilder
.BuildUserAgentClient(
idpConfig.Client.ClientId,
null,
realm,
idpConfig.Client.RedirectUris.ToArray())
.SetClientName(idpConfig.Client.Name)
.AddScope(scopes.ToArray())
.AddRefreshToken()
.Build();
client.IsPublic = idpConfig.Client.IsPublic;
client.Realms.Add(realm);
var clients = new List<Client> { client };
// ── Build users ───────────────────────────────────────────────────────────────
var users = idpConfig.Users.Select(u =>
{
var userBuilder = UserBuilder
.Create(u.Login, u.Password, u.Name)
.SetEmail(u.Email)
.SetFirstname(u.Firstname)
.SetEmailVerified(u.EmailVerified);
foreach (var role in u.Roles)
userBuilder = userBuilder.AddRole(role);
foreach (var claim in u.Claims)
userBuilder = userBuilder.AddClaim(claim.Key, claim.Value);
foreach (var consent in u.Consents)
userBuilder = userBuilder.AddConsent(consent.Realm, consent.ClientId, consent.Scope);
return userBuilder.Build();
}).ToList();
// ── Wire up realm relationships ───────────────────────────────────────────────
foreach (var user in users)
{
var rUser = new RealmUser { Realm = realm, User = user };
user.Realms.Add(rUser);
realm.Users.Add(rUser);
}
realm.Clients.Add(client);
realm.ApiResources.Add(api);
scopes.ForEach(s => realm.Scopes.Add(s));
// ── Configure services ────────────────────────────────────────────────────────
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(
name: corsPolicyName,
policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
builder
.AddSidIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryRealms(new[] { realm }.ToList())
.AddInMemoryUsers(users)
.AddInMemoryClients(clients)
.AddInMemoryScopes(scopes)
.AddInMemoryLanguages(DefaultLanguages.All)
.AddPwdAuthentication(true);
var app = builder.Build();
using (var sc = app.Services.CreateScope())
{
var s = sc.ServiceProvider.GetRequiredService<IApiResourceRepository>();
s.Add(api);
}
app.Services.SeedData();
app.UseSid();
app.UseCors(corsPolicyName);
await app.RunAsync();
// ── Configuration model ───────────────────────────────────────────────────────
record IdpConfig(
ApiConfig Api,
ScopeConfig Scope,
ClientConfig Client,
List<UserConfig> Users);
record ApiConfig(
string Name,
string Audience);
record ScopeConfig(
string Name,
string Description,
List<ClaimMapperConfig> ClaimMappers);
record ClaimMapperConfig(
string TargetClaimPath,
string SourceUserAttribute,
string TokenClaimJsonType,
bool IsMultiValued = false);
record ClientConfig(
string ClientId,
string Name,
List<string> RedirectUris,
bool IsPublic = true);
record UserConfig(
string Login,
string Password,
string Name,
string Email,
string Firstname,
bool EmailVerified,
List<string> Roles,
Dictionary<string, string> Claims,
List<ConsentConfig> Consents);
record ConsentConfig(
string Realm,
string ClientId,
string Scope);