From 5b602ba9974afea5928ae4e4f34d924d4deee680 Mon Sep 17 00:00:00 2001 From: Steve Temple Date: Thu, 30 Jul 2026 10:41:27 +0100 Subject: [PATCH] Add certificate authentication option --- README-uptov12.md | 2 +- README-v13.md | 2 +- README-v14plus.md | 2 +- README-v15plus.md | 2 +- README.md | 12 +++++++++--- .../AzureSSOConfiguration.cs | 5 +++++ src/Umbraco.Community.AzureSSO/CredentialType.cs | 3 ++- .../MicrosoftAccountAuthenticationExtensions.cs | 11 +++++++++++ .../Settings/AzureSSOSettings.cs | 2 ++ ...settings-schema.UmbracoCommunityAzureSSO.json | 16 +++++++++++++--- 10 files changed, 46 insertions(+), 11 deletions(-) diff --git a/README-uptov12.md b/README-uptov12.md index f785008..d47469e 100644 --- a/README-uptov12.md +++ b/README-uptov12.md @@ -39,7 +39,7 @@ You'll need to configure these settings based on the values in Azure: | Domain | The value in Primary domain in the Microsoft Entra ID Directory Overview | | TenantId | The value in Directory (tenant) ID on the App Registration Overview | | ClientId | The value in Application (Client) ID on the App Registration Overview | -| ClientSecret | The client secret created for the App Registration (not required when using a [managed or workload identity](README.md#managed-identity--workload-identity)) | +| ClientSecret | The client secret created for the App Registration (not required when using a [managed identity, workload identity or certificate](README.md#managed-identity--workload-identity)) | You can also customise the configuration by setting these settings: diff --git a/README-v13.md b/README-v13.md index ebe4005..212a109 100644 --- a/README-v13.md +++ b/README-v13.md @@ -39,7 +39,7 @@ You'll need to configure these settings based on the values in Azure: | Domain | The value in Primary domain in the Microsoft Entra ID Directory Overview | | TenantId | The value in Directory (tenant) ID on the App Registration Overview | | ClientId | The value in Application (Client) ID on the App Registration Overview | -| ClientSecret | The client secret created for the App Registration (not required when using a [managed or workload identity](README.md#managed-identity--workload-identity)) | +| ClientSecret | The client secret created for the App Registration (not required when using a [managed identity, workload identity or certificate](README.md#managed-identity--workload-identity)) | You can also customise the configuration by setting these settings: diff --git a/README-v14plus.md b/README-v14plus.md index 9b9290e..2723b60 100644 --- a/README-v14plus.md +++ b/README-v14plus.md @@ -37,7 +37,7 @@ You'll need to configure these settings based on the values in Azure: | Domain | The value in Primary domain in the Microsoft Entra ID Directory Overview | | TenantId | The value in Directory (tenant) ID on the App Registration Overview | | ClientId | The value in Application (Client) ID on the App Registration Overview | -| ClientSecret | The client secret created for the App Registration (not required when using a [managed or workload identity](README.md#managed-identity--workload-identity)) | +| ClientSecret | The client secret created for the App Registration (not required when using a [managed identity, workload identity or certificate](README.md#managed-identity--workload-identity)) | You can also customise the configuration by setting these settings: diff --git a/README-v15plus.md b/README-v15plus.md index a3f57b2..a4b88f5 100644 --- a/README-v15plus.md +++ b/README-v15plus.md @@ -38,7 +38,7 @@ You'll need to configure these settings based on the values in Azure: | Domain | The value in Primary domain in the Microsoft Entra ID Directory Overview | | TenantId | The value in Directory (tenant) ID on the App Registration Overview | | ClientId | The value in Application (Client) ID on the App Registration Overview | -| ClientSecret | The client secret created for the App Registration (not required when using a [managed or workload identity](README.md#managed-identity--workload-identity)) | +| ClientSecret | The client secret created for the App Registration (not required when using a [managed identity, workload identity or certificate](README.md#managed-identity--workload-identity)) | You can also customise the configuration by setting these settings: diff --git a/README.md b/README.md index b5ee692..d22b4f3 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ In which case you'll need to add AddMicrosoftAccountAuthentication() to your Con ### Managed Identity / Workload Identity -By default the package authenticates against the App Registration using a client secret. Alternatively you can authenticate using an Azure Managed Identity or a Workload Identity (federated credentials, e.g. on AKS), which removes the need to store a client secret, by setting `CredentialType` in the `Credentials` section: +By default the package authenticates against the App Registration using a client secret. Alternatively you can authenticate using an Azure Managed Identity, a Workload Identity (federated credentials, e.g. on AKS) or a certificate, which removes the need to store a client secret, by setting `CredentialType` in the `Credentials` section: ``` "AzureSSO": { @@ -73,10 +73,12 @@ By default the package authenticates against the App Registration using a client | Setting | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| CredentialType | How the application authenticates to Entra ID: `Secret` (the default, uses ClientSecret), `ManagedIdentity` or `WorkloadIdentity` | +| CredentialType | How the application authenticates to Entra ID: `Secret` (the default, uses ClientSecret), `ManagedIdentity`, `WorkloadIdentity` or `Certificate` | | ManagedIdentityClientId | Only used when CredentialType is `ManagedIdentity`. Set to the client ID of a user-assigned managed identity, or leave empty to use the system-assigned managed identity | +| CertificateThumbprint | Only used when CredentialType is `Certificate`. The thumbprint of the certificate to use | +| CertificateStorePath | Only used when CredentialType is `Certificate`. The certificate store to load the certificate from, in `StoreLocation/StoreName` format. Defaults to `CurrentUser/My` | -When `CredentialType` is `ManagedIdentity` or `WorkloadIdentity`, `ClientSecret` is not required and is ignored. +When `CredentialType` is `ManagedIdentity`, `WorkloadIdentity` or `Certificate`, `ClientSecret` is not required and is ignored. #### Managed Identity @@ -86,6 +88,10 @@ The managed identity is used as a federated identity credential for the App Regi Workload identity authenticates using the federated token file issued by the Azure Workload Identity webhook. The `AZURE_FEDERATED_TOKEN_FILE` environment variable must be set — on AKS this means the workload identity webhook is enabled and the pod has the `azure.workload.identity/use: "true"` label. The App Registration needs a federated credential trusting the Kubernetes service account, and `TenantId` and `ClientId` must be set explicitly in the configuration. If the environment variable is missing the site will throw an error on startup. +#### Certificate + +The certificate must be uploaded to the App Registration (Certificates & secrets) and installed in a certificate store the application can read, identified by `CertificateThumbprint`. `CertificateStorePath` selects which store to load it from (e.g. `CurrentUser/My` or `LocalMachine/My`) and defaults to `CurrentUser/My` if not set. + ### Debugging LogUnmappedRolesAsWarning diff --git a/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs b/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs index 047d544..915956a 100644 --- a/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs +++ b/src/Umbraco.Community.AzureSSO/AzureSSOConfiguration.cs @@ -82,6 +82,8 @@ public class AzureSSOCredentials public string ClientId { get; set; } = ""; public string ManagedIdentityClientId { get; set; } = ""; public string ClientSecret { get; set; } = ""; + public string CertificateThumbprint { get; set; } = ""; + public string CertificateStorePath { get; set; } = ""; public string CallbackPath { get; set; } = ""; public string SignedOutCallbackPath { get; set; } = ""; @@ -91,6 +93,8 @@ public class AzureSSOCredentials /// ManagedIdentity uses Azure Managed Identity; for a user-assigned identity set ManagedIdentityClientId, /// for system-assigned leave it empty. /// WorkloadIdentity uses federated credentials (e.g. AKS); TenantId and ClientId must be provided explicitly. + /// Certificate uses a certificate from a local certificate store, identified by CertificateThumbprint + /// (and optionally CertificateStorePath, which defaults to "CurrentUser/My"). /// public CredentialType CredentialType { get; set; } = CredentialType.Secret; @@ -101,6 +105,7 @@ public bool IsValid() !string.IsNullOrEmpty(TenantId) && !string.IsNullOrEmpty(ClientId) && (CredentialType != CredentialType.Secret || !string.IsNullOrEmpty(ClientSecret)) && + (CredentialType != CredentialType.Certificate || !string.IsNullOrEmpty(CertificateThumbprint)) && !string.IsNullOrEmpty(CallbackPath) && !string.IsNullOrEmpty(SignedOutCallbackPath); } diff --git a/src/Umbraco.Community.AzureSSO/CredentialType.cs b/src/Umbraco.Community.AzureSSO/CredentialType.cs index d7a3dbe..d16ed33 100644 --- a/src/Umbraco.Community.AzureSSO/CredentialType.cs +++ b/src/Umbraco.Community.AzureSSO/CredentialType.cs @@ -4,6 +4,7 @@ public enum CredentialType { Secret = 0, ManagedIdentity = 1, - WorkloadIdentity = 2 + WorkloadIdentity = 2, + Certificate = 3 } } diff --git a/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs b/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs index 66de93e..87d53a5 100644 --- a/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs +++ b/src/Umbraco.Community.AzureSSO/MicrosoftAccountAuthenticationExtensions.cs @@ -139,6 +139,17 @@ private static void CopyCredentials(MicrosoftIdentityOptions options, AzureSsoCr } }; break; + case CredentialType.Certificate: + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.StoreWithThumbprint, + CertificateStorePath = string.IsNullOrEmpty(settings.CertificateStorePath) ? "CurrentUser/My" : settings.CertificateStorePath, + CertificateThumbprint = settings.CertificateThumbprint, + } + }; + break; case CredentialType.Secret: default: options.ClientSecret = settings.ClientSecret; diff --git a/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs b/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs index f4b1c35..27e919d 100644 --- a/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs +++ b/src/Umbraco.Community.AzureSSO/Settings/AzureSSOSettings.cs @@ -44,6 +44,8 @@ public class AzureSsoCredentialSettings(AzureSSOCredentials credentials) public string ClientId => credentials.ClientId; public string ManagedIdentityClientId => credentials.ManagedIdentityClientId; public string ClientSecret => credentials.ClientSecret; + public string CertificateThumbprint => credentials.CertificateThumbprint; + public string CertificateStorePath => credentials.CertificateStorePath; public string CallbackPath => credentials.CallbackPath; public string SignedOutCallbackPath => credentials.SignedOutCallbackPath; public CredentialType CredentialType => credentials.CredentialType; diff --git a/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json b/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json index 5160069..bf0f5e1 100644 --- a/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json +++ b/src/Umbraco.Community.AzureSSO/appsettings-schema.UmbracoCommunityAzureSSO.json @@ -57,6 +57,14 @@ "type": "string", "description": "The client secret created for the app registration. Only required when CredentialType is Secret." }, + "CertificateThumbprint": { + "type": "string", + "description": "The thumbprint of a certificate in the certificate store to use when CredentialType is Certificate." + }, + "CertificateStorePath": { + "type": "string", + "description": "The certificate store to load the certificate from when CredentialType is Certificate, in \"StoreLocation/StoreName\" format. Defaults to \"CurrentUser/My\"." + }, "CredentialType": { "description": "How the application authenticates to Entra ID (defaults to Secret)", "default": "Secret", @@ -161,16 +169,18 @@ }, "CredentialType": { "type": "string", - "description": "How the application authenticates to Entra ID. Secret uses ClientSecret. ManagedIdentity uses Azure Managed Identity (set ManagedIdentityClientId for user-assigned, leave empty for system-assigned). WorkloadIdentity uses federated credentials (e.g. AKS) via the AZURE_FEDERATED_TOKEN_FILE environment variable.", + "description": "How the application authenticates to Entra ID. Secret uses ClientSecret. ManagedIdentity uses Azure Managed Identity (set ManagedIdentityClientId for user-assigned, leave empty for system-assigned). WorkloadIdentity uses federated credentials (e.g. AKS) via the AZURE_FEDERATED_TOKEN_FILE environment variable. Certificate uses a certificate from a local certificate store, identified by CertificateThumbprint and CertificateStorePath.", "x-enumNames": [ "Secret", "ManagedIdentity", - "WorkloadIdentity" + "WorkloadIdentity", + "Certificate" ], "enum": [ "Secret", "ManagedIdentity", - "WorkloadIdentity" + "WorkloadIdentity", + "Certificate" ] }