From 9b3df9be60ed80bec71f8b0aaf9d1f62786a75dc Mon Sep 17 00:00:00 2001 From: Ole Kristian Losvik Date: Tue, 24 Mar 2026 18:06:30 +0100 Subject: [PATCH] Auth: Prepare client sync for authorization code flow - Add RedirectUris and PostLogoutRedirectUris to ClientDefinition - Add LoginPath to AuthEndpointSettings - Implement authorization_code grant in ClientSyncInitializer - Update README with new configuration options --- src/Ignis.Api/appsettings.json | 5 +- src/Ignis.Auth/AuthSettings.cs | 3 + src/Ignis.Auth/README.md | 74 +++++++------------ .../Services/ClientSyncInitializer.cs | 30 +++++++- .../Ignis.Api.Tests/AuthConfigurationTests.cs | 8 +- 5 files changed, 68 insertions(+), 52 deletions(-) diff --git a/src/Ignis.Api/appsettings.json b/src/Ignis.Api/appsettings.json index fe44edb..691415a 100644 --- a/src/Ignis.Api/appsettings.json +++ b/src/Ignis.Api/appsettings.json @@ -18,7 +18,10 @@ "DisplayName": "Ignis Client", "AllowedGrantTypes": ["client_credentials"] } - ] + ], + "Endpoints": { + "LoginPath": "/connect/login" + } }, "SparkSettings": { "Endpoint": "https://localhost:5201/fhir", diff --git a/src/Ignis.Auth/AuthSettings.cs b/src/Ignis.Auth/AuthSettings.cs index 9e03a78..d8e639b 100644 --- a/src/Ignis.Auth/AuthSettings.cs +++ b/src/Ignis.Auth/AuthSettings.cs @@ -19,6 +19,7 @@ public class AuthCertificateSettings public class AuthEndpointSettings { public string TokenEndpointPath { get; set; } = "connect/token"; + public string LoginPath { get; set; } = "connect/login"; } public class ClientDefinition @@ -27,4 +28,6 @@ public class ClientDefinition public string ClientSecret { get; set; } = ""; public string DisplayName { get; set; } = ""; public List AllowedGrantTypes { get; set; } = []; + public List RedirectUris { get; set; } = []; + public List PostLogoutRedirectUris { get; set; } = []; } diff --git a/src/Ignis.Auth/README.md b/src/Ignis.Auth/README.md index 27761ed..447bb46 100644 --- a/src/Ignis.Auth/README.md +++ b/src/Ignis.Auth/README.md @@ -1,24 +1,32 @@ # Ignis.Auth -OAuth 2.0 token service for Ignis, built on [OpenIddict](https://documentation.openiddict.com/) with MongoDB storage. - -The library provides an `AuthorizationHandler` containing the token endpoint logic, while the host application supplies a thin controller that delegates to it. Currently supports the `client_credentials` grant type. +OAuth 2.0 authorization server built on [OpenIddict](https://documentation.openiddict.com/) with MongoDB storage. Currently supports the `client_credentials` grant type; `authorization_code` with mandatory PKCE and Pushed Authorization Requests (PAR) is prepared in settings and client sync but not yet enabled in the server pipeline. ## Configuration ```json { "AuthSettings": { - "Enabled": true, "ConnectionString": "mongodb://localhost:27017/ignis", "Clients": [ { - "ClientId": "my-client", + "ClientId": "my-backend", "ClientSecret": "my-secret", - "DisplayName": "My Client", + "DisplayName": "Backend Service", "AllowedGrantTypes": ["client_credentials"] + }, + { + "ClientId": "my-web-app", + "ClientSecret": "web-secret", + "DisplayName": "My Web App", + "AllowedGrantTypes": ["authorization_code"], + "RedirectUris": ["https://app.example.com/callback"], + "PostLogoutRedirectUris": ["https://app.example.com"] } ], + "Endpoints": { + "LoginPath": "/connect/login" + }, "Certificates": { "SigningCertificatePath": "certs/signing.pfx", "SigningCertificatePassword": "", @@ -29,56 +37,26 @@ The library provides an `AuthorizationHandler` containing the token endpoint log } ``` -## Certificates - -OpenIddict requires a signing certificate and an encryption certificate for token generation and validation. +All clients are confidential and require a `ClientSecret`. `AllowedGrantTypes` is required. -### Development - -In development mode, OpenIddict automatically generates ephemeral development certificates. No configuration needed. - -### Production +## Certificates -Generate self-signed PFX certificates for signing and encryption: +Development mode uses ephemeral auto-generated certificates. For production, generate PFX certificates: ```bash mkdir -p certs - -# Signing certificate -openssl req -x509 -nodes -newkey rsa:2048 \ - -keyout certs/signing-key.pem -out certs/signing-cert.pem \ - -days 365 -subj "/CN=Ignis Token Signing" -openssl pkcs12 -export -out certs/signing.pfx \ - -inkey certs/signing-key.pem -in certs/signing-cert.pem \ - -passout pass: - -# Encryption certificate -openssl req -x509 -nodes -newkey rsa:2048 \ - -keyout certs/encryption-key.pem -out certs/encryption-cert.pem \ - -days 365 -subj "/CN=Ignis Token Encryption" -openssl pkcs12 -export -out certs/encryption.pfx \ - -inkey certs/encryption-key.pem -in certs/encryption-cert.pem \ - -passout pass: - -# Clean up PEM files +for NAME in signing encryption; do + openssl req -x509 -nodes -newkey rsa:2048 \ + -keyout certs/$NAME-key.pem -out certs/$NAME-cert.pem \ + -days 365 -subj "/CN=Ignis ${NAME^}" + openssl pkcs12 -export -out certs/$NAME.pfx \ + -inkey certs/$NAME-key.pem -in certs/$NAME-cert.pem -passout pass: +done rm certs/*.pem ``` -The `certs/` directory is gitignored. Mount certificates via volume or secrets in production. +The `certs/` directory is gitignored. Mount via volume or secrets in production. ## Client sync -Clients defined in `AuthSettings.Clients` are synced to MongoDB on startup: - -- New clients are created -- Existing clients are updated (secret, display name) -- Clients not in config are removed - -## Usage - -```bash -curl -X POST https://localhost:5201/connect/token \ - -d grant_type=client_credentials \ - -d client_id=my-client \ - -d client_secret=my-secret -``` +Clients in `AuthSettings.Clients` are synced to MongoDB on startup — created, updated, or removed to match configuration. diff --git a/src/Ignis.Auth/Services/ClientSyncInitializer.cs b/src/Ignis.Auth/Services/ClientSyncInitializer.cs index d4dd425..2aa0b06 100644 --- a/src/Ignis.Auth/Services/ClientSyncInitializer.cs +++ b/src/Ignis.Auth/Services/ClientSyncInitializer.cs @@ -88,6 +88,8 @@ private OpenIddictApplicationDescriptor BuildDescriptor(ClientDefinition client) } }; + var hasRedirectUris = client.RedirectUris.Count > 0; + foreach (var grantType in client.AllowedGrantTypes) { switch (grantType) @@ -97,8 +99,32 @@ private OpenIddictApplicationDescriptor BuildDescriptor(ClientDefinition client) break; case GrantTypes.AuthorizationCode: - throw new NotImplementedException( - $"Grant type '{GrantTypes.AuthorizationCode}' is not yet supported."); + if (!hasRedirectUris) + { + _logger.LogWarning( + "Client {ClientId} allows authorization_code but has no RedirectUris configured – skipping auth code permissions.", + client.ClientId); + break; + } + + descriptor.Permissions.Add(Permissions.Endpoints.Authorization); + descriptor.Permissions.Add(Permissions.Endpoints.PushedAuthorization); + descriptor.Permissions.Add(Permissions.GrantTypes.AuthorizationCode); + descriptor.Permissions.Add(Permissions.ResponseTypes.Code); + + descriptor.Requirements.Add(Requirements.Features.ProofKeyForCodeExchange); + descriptor.Requirements.Add(Requirements.Features.PushedAuthorizationRequests); + + foreach (var uri in client.RedirectUris) + descriptor.RedirectUris.Add(new Uri(uri)); + + if (client.PostLogoutRedirectUris.Count > 0) + { + descriptor.Permissions.Add(Permissions.Endpoints.EndSession); + foreach (var uri in client.PostLogoutRedirectUris) + descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); + } + break; default: _logger.LogWarning( diff --git a/tests/Ignis.Api.Tests/AuthConfigurationTests.cs b/tests/Ignis.Api.Tests/AuthConfigurationTests.cs index 0c5aaeb..d499dd1 100644 --- a/tests/Ignis.Api.Tests/AuthConfigurationTests.cs +++ b/tests/Ignis.Api.Tests/AuthConfigurationTests.cs @@ -58,7 +58,13 @@ private WebApplicationFactory CreateFactory( { return new WebApplicationFactory().WithWebHostBuilder(builder => { - builder.ConfigureAppConfiguration((_, c) => c.AddInMemoryCollection(config)); + builder.ConfigureAppConfiguration((_, c) => + { + // Re-add environment variables then override with in-memory config, + // ensuring test values take precedence over leaked env vars. + c.AddEnvironmentVariables(); + c.AddInMemoryCollection(config); + }); builder.UseEnvironment(environment); }); }