Skip to content
This repository has been archived by the owner on Jan 5, 2025. It is now read-only.

feat: support app service certificate store pds integration #9

Merged
merged 20 commits into from
Apr 18, 2024
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
@@ -81,7 +81,8 @@
"jsondecode",
"strcontains",
"substr",
"endfor"
"endfor",
"signkey"
],
"version": "0.2"
}
3 changes: 3 additions & 0 deletions .pipelines/cd-pipeline.yaml
Original file line number Diff line number Diff line change
@@ -102,6 +102,9 @@ stages:
az login --service-principal -u $(sp-dex-deployment-client-id) -p $(sp-dex-deployment-client-secret) --tenant $(AZURE_TENANT_ID)
token=$(az account get-access-token --resource=$(fhir_server_url) --query accessToken --output tsv)
FHIR_SERVER_URL=$(frontend_address) FHIR_SERVER_AUTH_TOKEN=$token docker compose -f docker/docker-compose.yml up data-init --exit-code-from data-init --no-deps
env:
PACKAGE_ID: 'fhir.r4.ukcore.stu3.currentbuild'
PACKAGE_VERSION: '0.0.8-pre-release'

- job: PushTemplates
dependsOn: DeployAPI
3 changes: 2 additions & 1 deletion docs/developer/configuration-structure.md
Original file line number Diff line number Diff line change
@@ -12,7 +12,8 @@ The `appsettings.json` file contains the default/common configuration for the ap
The other files are used to override the default configuration based on the environment the application is running in. The `appsettings.Local.json` file is used for local development, and the `appsettings.Integration.json` file is used for integration testing. The `appsettings.Development.json` file is used for development environment, and the `appsettings.Production.json` file is used for production and staging.
The following setting are being overridden by terraform:

- "Pds__Fhir__Authentication__Certificate"
- "Pds__Fhir__Authentication__UseCertificateStore"
- "Pds__Fhir__Authentication__CertificateThumbprint"
- "DataHubFhirServer__Authentication__Scope"
- "ApplicationInsightsAgent_EXTENSION_VERSION"
- "AzureStorageConnectionString"
24 changes: 24 additions & 0 deletions docs/national-services/certificate-preparation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Certificate Preparation

The Healthcare Data Exchange uses signed JWT authentication to access some application-restricted RESTful APIs. Notably the [Personal Demographics Service](./personal-demographics-service.md).

NHS Digital provide extensive [documentation](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/application-restricted-restful-apis-signed-jwt-authentication) explaining how this integration works.

An important step in this process is generating and signing a JWT. This happens at runtime. The JWT is then used to authenticate with the PDS.

In the `JwtHandler` we generate an instance of `SigningCredentials` using a `X509Certificate2` object. This certificate is stored in Azure Key Vault and loaded into the application container at runtime. Details of this process can be found [at the ssl certificate configuration guide](https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate-in-code).

## Convert certificate to PKCS12 x509 certificate

Before importing the certificate into Azure Key Vault, it must be converted to a PKCS12 x509 certificate.

The NHS Digital guide produces a self-signed certificate and a private key in X.509 PEM format. These components can be temporarily stored in `.pem` files and used to create a PKCS12 x509 certificate.

The following commands can be used to convert the certificate and private key to a PKCS12 x509 certificate:

```bash
openssl x509 -in certificate.pem -out x509certificate.pem -signkey privatekey.pem
openssl pkcs12 -export -out certificate.pfx -in x509certificate.pem -inkey privatekey.pem
```

This command will prompt for a password to secure the PKCS12 x509 certificate. This password will be required when importing the certificate into Azure Key Vault.
2 changes: 1 addition & 1 deletion infrastructure/app-registration.tf
Original file line number Diff line number Diff line change
@@ -74,7 +74,7 @@ resource "azuread_application_pre_authorized" "azcli" {
}

resource "azuread_service_principal" "app" {
client_id = azuread_application.app.application_id
client_id = azuread_application.app.client_id
owners = var.app_registration_owners
tags = [
"AppServiceIntegratedApp",
47 changes: 29 additions & 18 deletions infrastructure/services/app-plan.tf
Original file line number Diff line number Diff line change
@@ -27,24 +27,26 @@ resource "azurerm_linux_web_app" "web_app" {
}

app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.app_insights.instrumentation_key
"APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.app_insights.connection_string
"ASPNETCORE_ENVIRONMENT" = lookup(local.env_mapping, var.env, "Development")
"Pds__Fhir__Authentication__Certificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.pds_fhir_certificate_private.id})"
"DataHubFhirServer__Authentication__Scope" = "${var.fhir_url}/.default"
"ASPNETCORE_URLS" = "http://+:80"
"ApplicationInsightsAgent_EXTENSION_VERSION" = "~3"
"AzureStorageConnectionString" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.azure_storage_connection_string.id})"
"AzureTableStorageCache__Endpoint" = azurerm_storage_account.dex_storage_account.primary_table_endpoint
"DataHubFhirServer__BaseUrl" = var.fhir_url
"DataHubFhirServer__TemplateImage" = "${azurerm_container_registry.acr.login_server}/api:${var.image_tag_suffix}"
"Mesh__Authentication__RootCertificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.nhs_root_certificate.id})"
"Mesh__Authentication__ClientCertificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.ndop_mesh_client_certificate_private.id})"
"Mesh__Authentication__SubCertificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.nhs_sub_certificate.id})"
"Ndop__Mesh__MailboxId" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.ndop_mesh_mailbox_id.id})"
"Ndop__Mesh__MailboxPassword" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.ndop_mesh_mailbox_password.id})"
"Pds__Mesh__MailboxId" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.pds_mesh_mailbox_id.id})"
"Pds__Mesh__MailboxPassword" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.pds_mesh_mailbox_password.id})"
"APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.app_insights.instrumentation_key
"APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.app_insights.connection_string
"ASPNETCORE_ENVIRONMENT" = lookup(local.env_mapping, var.env, "Development")
"Pds__Fhir__Authentication__UseCertificateStore" = true
"Pds__Fhir__Authentication__CertificateThumbprint" = azurerm_key_vault_certificate.pds_fhir_certificate_private.thumbprint
"DataHubFhirServer__Authentication__Scope" = "${var.fhir_url}/.default"
"ASPNETCORE_URLS" = "http://+:80"
"ApplicationInsightsAgent_EXTENSION_VERSION" = "~3"
"AzureStorageConnectionString" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.azure_storage_connection_string.id})"
"AzureTableStorageCache__Endpoint" = azurerm_storage_account.dex_storage_account.primary_table_endpoint
"DataHubFhirServer__BaseUrl" = var.fhir_url
"DataHubFhirServer__TemplateImage" = "${azurerm_container_registry.acr.login_server}/api:${var.image_tag_suffix}"
"Mesh__Authentication__RootCertificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.nhs_root_certificate.id})"
"Mesh__Authentication__ClientCertificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.ndop_mesh_client_certificate_private.id})"
"Mesh__Authentication__SubCertificate" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.nhs_sub_certificate.id})"
"Ndop__Mesh__MailboxId" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.ndop_mesh_mailbox_id.id})"
"Ndop__Mesh__MailboxPassword" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.ndop_mesh_mailbox_password.id})"
"Pds__Mesh__MailboxId" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.pds_mesh_mailbox_id.id})"
"Pds__Mesh__MailboxPassword" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.pds_mesh_mailbox_password.id})"
"WEBSITE_LOAD_CERTIFICATES" = azurerm_key_vault_certificate.pds_fhir_certificate_private.thumbprint
}

key_vault_reference_identity_id = azurerm_user_assigned_identity.identity.id
@@ -73,6 +75,15 @@ resource "azurerm_linux_web_app" "web_app" {
]
}

resource "azurerm_app_service_certificate" "pds_fhir_certificate_private" {
name = "pds-cert-dex-${var.env}"
app_service_plan_id = azurerm_service_plan.app_service_plan.id

location = var.location
resource_group_name = var.resource_group_name
key_vault_secret_id = azurerm_key_vault_certificate.pds_fhir_certificate_private.secret_id
}

# Setup private-link for web app
resource "azurerm_private_endpoint" "web-app-endpoint" {
name = "pe-dex-${var.env}-app"
10 changes: 6 additions & 4 deletions infrastructure/services/keyvault.tf
Original file line number Diff line number Diff line change
@@ -86,7 +86,7 @@ data "azurerm_key_vault" "common_kv" {
}

data "azurerm_key_vault_secret" "pds_fhir_certificate" {
name = var.env == "prd" || var.env == "stg" ? "pds-fhir-production-certificate-private" : "pds-fhir-integration-certificate-private"
name = var.env == "prd" || var.env == "stg" ? "pds-fhir-production-pfx-private" : "pds-fhir-integration-pfx-private"
key_vault_id = data.azurerm_key_vault.common_kv.id
}

@@ -125,10 +125,12 @@ data "azurerm_key_vault_secret" "pds_mesh_mailbox_password" {
key_vault_id = data.azurerm_key_vault.common_kv.id
}

resource "azurerm_key_vault_secret" "pds_fhir_certificate_private" {
name = "pds-fhir-certificate-private"
resource "azurerm_key_vault_certificate" "pds_fhir_certificate_private" {
name = "pds-fhir-certificate-pfx-private"
key_vault_id = azurerm_key_vault.kv.id
value = data.azurerm_key_vault_secret.pds_fhir_certificate.value
certificate {
contents = data.azurerm_key_vault_secret.pds_fhir_certificate.value
}

depends_on = [
azurerm_key_vault_access_policy.terraform_user_access
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ nav:
- National Data Opt-Out: national-services/national-data-opt-out.md
- Organisation Data Service: national-services/organisation-data-service.md
- MESH: national-services/mesh.md
- Certificate Preparation: national-services/certificate-preparation.md

copyright: "© NHS Dorset"

4 changes: 3 additions & 1 deletion src/Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,9 @@
"TokenUrl": "<TOKEN_URL>",
"ClientId": "<CLIENT_ID>",
"Kid": "HEALTHCARE_DATA_EXCHANGE",
"Certificate": "<CERTIFICATE>"
"UseCertificateStore": false,
"Certificate": "<CERTIFICATE>",
"CertificateThumbprint": "<THUMBPRINT>"
}
},
"Mesh": {
24 changes: 20 additions & 4 deletions src/Infrastructure/Common/Authentication/JwtHandler.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System.IdentityModel.Tokens.Jwt;
using System.IO.Abstractions;
using System.Security.Claims;
using System.Security.Cryptography;
using Infrastructure.Pds;
using Infrastructure.Pds.Configuration;
using System.Security.Cryptography.X509Certificates;
using Infrastructure.Pds.Fhir.Configuration;
using Microsoft.IdentityModel.Tokens;

@@ -33,9 +31,27 @@ public string GenerateJwt()
}

private SigningCredentials GetSigningCredentials()
{
return authConfig.UseCertificateStore && (authConfig.CertificateThumbprint != null)
? GetSigningCredentialsFromStore(authConfig.CertificateThumbprint, authConfig.Kid)
: GetSigningCredentialsFromConfig(authConfig.Kid);
}

private SigningCredentials GetSigningCredentialsFromStore(string thumbprint, string kid)
{
var certificateInBytes = File.ReadAllBytes($"/var/ssl/private/{thumbprint}.p12");
var cert = new X509Certificate2(certificateInBytes);

return new SigningCredentials(
new X509SecurityKey(cert, kid),
SecurityAlgorithms.RsaSha512
);
}

private SigningCredentials GetSigningCredentialsFromConfig(string kid)
{
var rsa = GenerateRsaFromPrivateKey();
var rsaSecurityKey = new RsaSecurityKey(rsa) { KeyId = authConfig.Kid };
var rsaSecurityKey = new RsaSecurityKey(rsa) { KeyId = kid };

return new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha512) { CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false } };
}
Original file line number Diff line number Diff line change
@@ -27,9 +27,9 @@ private async Task AddAuthenticationHeader(HttpRequestMessage request)
var authenticationToken = await tokenFactory.GetAccessToken();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authenticationToken);
}
catch (Exception)
catch (Exception ex)
liammoat marked this conversation as resolved.
Show resolved Hide resolved
{
throw new HttpRequestException("Unable to authenticate with backend service.");
throw new HttpRequestException("Unable to authenticate with backend service.", ex);
}
}

5 changes: 3 additions & 2 deletions src/Infrastructure/Pds/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -77,8 +77,9 @@ private static IServiceCollection AddPdsFhirClient(this IServiceCollection servi
{
var pdsConfiguration = configuration.GetSection(PdsConfiguration.SectionKey).Get<PdsConfiguration>()
?? throw new Exception("Pds section has not been configured.");
if (pdsConfiguration.Fhir.Authentication is { IsEnabled: true, Certificate: null or "" })
throw new Exception("Pds FhirCertificate is not set or empty");
if (pdsConfiguration.Fhir.Authentication is { IsEnabled: true, UseCertificateStore: true, CertificateThumbprint: null or "" }
or { IsEnabled: true, UseCertificateStore: false, Certificate: null or "" })
throw new Exception("Pds Fhir Certificate is not set or empty");

services.AddSingleton(pdsConfiguration.Fhir.Authentication);
services.AddSingleton<IFileSystem, FileSystem>();
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@

public record PdsFhirConfiguration(string BaseUrl, PdsAuthConfiguration Authentication);

public record PdsAuthConfiguration(bool IsEnabled = true, string TokenUrl = "", string ClientId = "", string Kid = "", string? Certificate = null);
public record PdsAuthConfiguration(bool IsEnabled = true, string TokenUrl = "", string ClientId = "", string Kid = "", bool UseCertificateStore = false, string? CertificateThumbprint = null, string? Certificate = null);