// 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( 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(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 { 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 }; // ── 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(); 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 Users); record ApiConfig( string Name, string Audience); record ScopeConfig( string Name, string Description, List ClaimMappers); record ClaimMapperConfig( string TargetClaimPath, string SourceUserAttribute, string TokenClaimJsonType, bool IsMultiValued = false); record ClientConfig( string ClientId, string Name, List RedirectUris, bool IsPublic = true); record UserConfig( string Login, string Password, string Name, string Email, string Firstname, bool EmailVerified, List Roles, Dictionary Claims, List Consents); record ConsentConfig( string Realm, string ClientId, string Scope);