Compare commits

...

11 Commits

Author SHA1 Message Date
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
Marek Lesko 0a585dd5a2 Update .gitlab-ci.yml file 2025-09-05 14:22:46 +00:00
Marek Lesko 54e888e240 Update .gitlab-ci.yml file 2025-09-05 14:19:30 +00:00
Marek Lesko 3b353c6660 Update .gitlab-ci.yml file 2025-09-05 14:17:09 +00:00
Marek Lesko ca6c565b2b Update .gitlab-ci.yml file 2025-09-05 12:19:16 +00:00
Marek Lesko 4ac1f94249 Update Dockerfile for multi-stage .NET build process
Implemented a multi-stage build in the Dockerfile, adding a build stage with the .NET SDK for restoring and publishing the application. Introduced a runtime stage using the ASP.NET image, created a non-root user for enhanced security, and set necessary environment variables. The published output is now copied from the build stage, and the entry point is configured to launch the application.
2025-09-04 14:09:52 +02:00
Marek Lesko 8cc3f8e308 FIXED ApiResource 2025-09-03 15:26:54 +00:00
Marek Lesko 1250160b91 FIXED devcontainer 2025-09-03 15:26:40 +00:00
Marek Lesko b6c52c502d ADDED devcontainer 2025-09-03 14:59:27 +00:00
Marek Lesko a3eaf0f5b7 WIP 2025-09-02 17:43:26 +02:00
Marek Lesko b18a89b087 UPDATED gitignore 2025-08-19 17:01:14 +02:00
8 changed files with 322 additions and 63 deletions
+4
View File
@@ -0,0 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine
# Optional: install Docker CLI (not Docker daemon)
RUN apk add --no-cache docker-cli bash git
+27
View File
@@ -0,0 +1,27 @@
// The Dev Container format allows you to configure your environment. At the heart of it
// is a Docker image or Dockerfile which controls the tools available in your environment.
//
// See https://aka.ms/devcontainer.json for more information.
{
"name": "Simple Id Server",
// Use "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
// instead of the build to use a pre-built image.
"build": {
"context": ".",
"dockerfile": "Dockerfile"
},
"runArgs": [
"--privileged"
],
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
],
"remoteUser": "root"
// Features add additional features to your environment. See https://containers.dev/features
// Beware: features are not supported on all platforms and may have unintended side-effects.
// "features": {
// "ghcr.io/devcontainers/features/docker-in-docker": {
// "moby": false
// }
// }
}
+1
View File
@@ -2,6 +2,7 @@
.vsCode .vsCode
bin bin
obj obj
lib
*.vssscc *.vssscc
*.vspscc *.vspscc
*.user *.user
+29
View File
@@ -0,0 +1,29 @@
stages: # Define the stages of the pipeline.
- build
docker-build:
stage: build
image: docker:latest
tags:
- shared
services:
- name: docker:dind
alias: docker
variables:
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
before_script:
- docker info
script:
- docker build -t $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:${CI_PIPELINE_IID} -t mareklesko/simpleidp:latest -t mareklesko/simpleidp:${CI_PIPELINE_IID} -f Dockerfile .
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:${CI_PIPELINE_IID}
- docker login -u mareklesko --password $DOCKER_HUB_PASSWORD
- docker push mareklesko/simpleidp:latest
- docker push mareklesko/simpleidp:${CI_PIPELINE_IID}
only:
- dev
- main
+35
View File
@@ -0,0 +1,35 @@
# =========================
# Build stage
# =========================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy everything (simple + reliable for single-project Razor Pages apps)
COPY . .
# Restore & publish (self-contained trimming can be added later if desired)
RUN dotnet restore
RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish --no-restore
# =========================
# Runtime stage
# =========================
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# (Optional) Create non-root user for better security
RUN useradd -m appuser
ENV ASPNETCORE_URLS=http://+:8080 \
ASPNETCORE_ENVIRONMENT=Production \
DOTNET_RUNNING_IN_CONTAINER=true
EXPOSE 8080
# Copy published output
COPY --from=build /app/publish ./
# Switch to non-root
USER appuser
# Start the Razor Pages app
ENTRYPOINT ["dotnet", "SimpleIdp.dll"]
+157 -63
View File
@@ -1,106 +1,200 @@
// Copyright (c) SimpleIdServer. All rights reserved. // Copyright (c) SimpleIdServer. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Text.Json;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SimpleIdServer.IdServer.Builders; using SimpleIdServer.IdServer.Builders;
using SimpleIdServer.IdServer.Config; using SimpleIdServer.IdServer.Config;
using SimpleIdServer.IdServer.Domains; using SimpleIdServer.IdServer.Domains;
using SimpleIdServer.IdServer.Helpers;
using SimpleIdServer.IdServer.Stores;
var corsPolicyName = "AllowAll"; var corsPolicyName = "AllowAll";
var users = new List<User> // ── 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
{ {
UserBuilder Realms = { realm },
.Create("administrator", "password", "Administrator") Name = idpConfig.Api.Name,
.SetEmail("adm@mail.com") Audience = idpConfig.Api.Audience
.SetFirstname("Administrator")
.AddRole("BI.PORTAL_ADMIN")
.AddRole("BI.TENANT_ADMIN")
.AddClaim("tid", "cbaa13c2-e95b-470a-bbcb-18911d5a6025")
.Build(),
}; };
var api = ApiResourceBuilder.Create("urn:bighand:api:bi:portal", "BI Portal API").Build(); // ── Build custom scope with claim mappers ─────────────────────────────────────
var biScope = new Scope
var clients = new List<Client>
{
ClientBuilder
.BuildUserAgentClient("foo", null, null, new[] { "http://localhost:4200/loggedin" })
.AddScope(new Scope("openid"), new Scope("profile"), new Scope("offline_access"))
.AddRefreshToken()
.Build(),
};
var scopes = new List<Scope> { ScopeBuilder.CreateRoleScope(clients[0], "bi.portal", "").Build() };
var biScope = new Scope()
{ {
Realms = { realm },
ApiResources = { api }, ApiResources = { api },
Name = "bi.portal", Protocol = ScopeProtocols.OAUTH,
Clients = { clients[0] }, Type = ScopeTypes.APIRESOURCE,
Description = "BI Portal Scope", Name = idpConfig.Scope.Name,
ClaimMappers = Description = idpConfig.Scope.Description,
ClaimMappers = idpConfig.Scope.ClaimMappers.Select(cm => new ScopeClaimMapper
{ {
new ScopeClaimMapper() IncludeInAccessToken = true,
{ TokenClaimJsonType = Enum.Parse<TokenClaimJsonTypes>(cm.TokenClaimJsonType, ignoreCase: true),
IncludeInAccessToken = true, TargetClaimPath = cm.TargetClaimPath,
TokenClaimJsonType = TokenClaimJsonTypes.STRING, MapperType = MappingRuleTypes.USERATTRIBUTE,
TargetClaimPath = "role", IsMultiValued = cm.IsMultiValued,
MapperType = MappingRuleTypes.USERATTRIBUTE, SourceUserAttribute = cm.SourceUserAttribute,
SourceUserAttribute = "role", SourceUserProperty = cm.SourceUserAttribute,
SourceUserProperty = "role", }).ToList(),
},
new ScopeClaimMapper()
{
IncludeInAccessToken = true,
TokenClaimJsonType = TokenClaimJsonTypes.STRING,
TargetClaimPath = "tid",
MapperType = MappingRuleTypes.USERATTRIBUTE,
SourceUserAttribute = "tid",
SourceUserProperty = "tid",
},
new ScopeClaimMapper()
{
IncludeInAccessToken = true,
TokenClaimJsonType = TokenClaimJsonTypes.STRING,
TargetClaimPath = "email",
MapperType = MappingRuleTypes.USERATTRIBUTE,
SourceUserAttribute = "email",
SourceUserProperty = "email",
},
},
}; };
clients[0].Scopes.Add(biScope); 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); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy( options.AddPolicy(
name: corsPolicyName, name: corsPolicyName,
policy => policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
{
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
}
);
}); });
builder builder
.AddSidIdentityServer() .AddSidIdentityServer()
.AddDeveloperSigningCredential() .AddDeveloperSigningCredential()
.AddInMemoryRealms(new[] { realm }.ToList())
.AddInMemoryUsers(users) .AddInMemoryUsers(users)
.AddInMemoryClients(clients) .AddInMemoryClients(clients)
.AddInMemoryScopes([biScope]) .AddInMemoryScopes(scopes)
.AddInMemoryLanguages(DefaultLanguages.All) .AddInMemoryLanguages(DefaultLanguages.All)
.AddPwdAuthentication(true); .AddPwdAuthentication(true);
var app = builder.Build(); var app = builder.Build();
using (var sc = app.Services.CreateScope())
{
var s = sc.ServiceProvider.GetRequiredService<IApiResourceRepository>();
s.Add(api);
}
app.Services.SeedData(); app.Services.SeedData();
app.UseSid(); app.UseSid();
app.UseCors(corsPolicyName); app.UseCors(corsPolicyName);
await app.RunAsync(); 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);
+5
View File
@@ -10,6 +10,11 @@
<PackageReference Include="SimpleIdServer.IdServer" Version="6.0.*-*" /> <PackageReference Include="SimpleIdServer.IdServer" Version="6.0.*-*" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" /> <PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="config\idp-config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Resources\AccountsResource.Designer.cs"> <Compile Update="Resources\AccountsResource.Designer.cs">
<DependentUpon>AccountsResource.resx</DependentUpon> <DependentUpon>AccountsResource.resx</DependentUpon>
+64
View File
@@ -0,0 +1,64 @@
{
"realm": "master",
"api": {
"name": "BI Portal API",
"audience": "urn:bighand:api:bi:portal"
},
"scope": {
"name": "bi.portal",
"description": "BI Portal Scope",
"claimMappers": [
{
"targetClaimPath": "roles",
"sourceUserAttribute": "role",
"tokenClaimJsonType": "STRING",
"isMultiValued": true
},
{
"targetClaimPath": "tid",
"sourceUserAttribute": "tid",
"tokenClaimJsonType": "STRING",
"isMultiValued": false
},
{
"targetClaimPath": "upn",
"sourceUserAttribute": "email",
"tokenClaimJsonType": "STRING",
"isMultiValued": false
}
]
},
"client": {
"clientId": "212C9DB96C2A4B6DA0AFDB2222F6EEAA.bighand.com",
"name": "BI Portal",
"redirectUris": [
"http://localhost:4200/loggedin"
],
"isPublic": true
},
"users": [
{
"login": "administrator",
"password": "password",
"name": "Administrator",
"email": "adm@mail.com",
"firstname": "Administrator",
"emailVerified": true,
"roles": [
"BI.PORTAL_ADMIN",
"BI.TENANT_ADMIN"
],
"claims": {
"tid": "cbaa13c2-e95b-470a-bbcb-18911d5a6025",
"aud": "urn:bighand:api:bi:portal"
},
"consents": [
{
"realm": "master",
"clientId": "212C9DB96C2A4B6DA0AFDB2222F6EEAA.bighand.com",
"scope": "bi.portal"
}
]
}
]
}