Summary
Following the article Part.2, I would like to share about how the sample code api-management-sample works. The secret management with Key Vault follow the concpet from Azure Pipelines secret management with Key Vault.
TOC
API deployment
In the deployment pipeline, it builds .NET API, generates the API documentation swagger.json
, and deploys it to API Management. To generate swagger.json
, the .NET project file needs to install Swashbuckle.AspNetCore.Cli
package.
Project file example AzureAdAuth.csproj
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
In Program.cs
, the API documentation should be defined
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new() { Title = "AzureAdAuthAPI", Version = "v1" });
});
For a .NET API project, you can use .NET CLI local tool. The command below creates ./.config/dotnet-tools.json
.
dotnet new tool-manifest
Initially dotnet-tools.json
file looks like this.
{
"version": 1,
"isRoot": true,
"tools": {}
}
Then you can run the command below to install Swashbuckle.AspNetCore.Cli
. You have to match the version of it with the NugetPackage version defined in the proejct file above.
dotnet tool install Swashbuckle.AspNetCore.Cli --version 6.3.1
After the command, dotnet-tools.json
file looks like this.
{
"version": 1,
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "6.3.1",
"commands": [
"swagger"
]
}
}
}
Then you run the command below to generate swagger.json
. v1
should match the version determined in Program.cs
above.
dotnet swagger tofile --output swagger.json .\bin\Debug\net6.0\AzureAdAuth.dll v1
The process of generating swagger.json
above can be executed in the Azure Pipelines. In the Yaml file example below, swagger.json
generated in the tasks is deployed in API Management.
- task: DotNetCoreCLI@2
displayName: dotnet new tool-manifest
inputs:
command: custom
custom: new
arguments: tool-manifest
workingDirectory: $(ApiDirectory)
- task: DotNetCoreCLI@2
displayName: dotnet tool install
inputs:
command: custom
custom: tool
arguments: install Swashbuckle.AspNetCore.Cli --version $(SwashbuckleVersion)
workingDirectory: $(ApiDirectory)
- task: DotNetCoreCLI@2
displayName: dotnet swagger tofile
inputs:
command: custom
custom: swagger
arguments: tofile --output swagger.json $(DllPath) $(SwaggerVersion)
workingDirectory: $(ApiDirectory)
- task: AzureCLI@2
displayName: Deploy API to API Management
inputs:
azureSubscription: $(AZURE_SVC_NAME)
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
az apim api import -g $(ResourceGroupName) `
--service-name $(APIManagementName) `
--api-id ${{AppServiceInstance.api}} `
--path ${{AppServiceInstance.path}} `
--specification-format OpenApiJson `
--specification-path $(APIManagementSwaggerPath) `
--service-url $(APIManagementServiceUrl)
Subscription Key Validation vs Gateway Validation
The architecture of Subscription Key Validation and Gateway Validation is explained in the previous articles. In this section, I am going to explain what is the code difference between two architectures for the bicep and Yaml files.
Subscription Key Validation
Gateway Validation
Bicep
azuredeploy_subscription.bicep
azuredeploy_gateway.bicep
Subscription Key validation | Gateway Validation | |
---|---|---|
subscriptionRequired | true |
false |
AzureAdAuthAPI policy | Get a token from Azure AD and remove subscription key from header | Validate Azure AD token from client app |
BasicAuthAPI policy | Set username and password and remove subscription key from header | Validate basic auth username and password |
CertificateAuthAPI policy | Get a certificate from Key Vault and remove subscription key from header | Validate a client certificate and get a certificate from Key Vault |
Azure Piplines Yaml
The only difference is which bicep file the pipeline execute, azuredeploy_subscription.bicep
or azuredeploy_gateway.bicep
iac_pipeline_subscription.yml
iac_pipeline_gateway.yml
Integration test
The CI/CD pipeline api_pipeline.yml includes six types of integration test, Subscription Key Valition for three APIs and Gateway Validation for three APIs. All of integration tests are executing every time, and if the architecture is the Subscription Key Validation, three tests are succeeded and other three are failed. For the Gateway Validation architecture, it is vice versa.
Subscription key
Subscription key is stored in the Key Vault during the IaC pipeline deployment. The pipeline downloads the subscription key from the Key Vault and attaches to the header and then send requests to the API.
- task: AzurePowerShell@5
continueOnError: true
displayName: Integration test - Subscription key - ${{AppServiceInstance.name}}
inputs:
azureSubscription: $(AZURE_SVC_NAME)
azurePowerShellVersion: latestVersion
ScriptType: InlineScript
Inline: |
$subscriptionKey = Get-AzKeyVaultSecret -VaultName $env:KeyVaultName -Name $env:KVSECRET_NAME_SUBSCRIPTION_KEY -AsPlainText
$uri = "https://$env:APIManagementName.azure-api.net/${{AppServiceInstance.name}}/Weatherforecast/RequireAuth"
$headers = @{
"Ocp-Apim-Subscription-Key" = $subscriptionKey
}
Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
Azure AD token
- All of necessary parameters such as application ID and secrests are from Pipeline Library and Key vault.
- Use OAuth2.0 Client Credential flow because this is service authentication.
- task: AzurePowerShell@5
continueOnError: true
displayName: Integration test - Gateway validation - AzureAdAuth
inputs:
azureSubscription: $(AZURE_SVC_NAME)
azurePowerShellVersion: latestVersion
ScriptType: InlineScript
Inline: |
$clientSecret = Get-AzKeyVaultSecret -VaultName $env:KeyVaultName -Name $env:KVSECRET_NAME_CLIENTSECRET -AsPlainText
$authorizeUri = "https://login.microsoftonline.com/$env:AAD_TENANTID/oauth2/v2.0/token"
$body = 'grant_type=client_credentials' + `
'&client_id=$(AAD_APPID_CLIENT)' + `
'&client_secret=' + $clientSecret + `
'&scope=api://$(AAD_APPID_BACKEND)/.default'
$token = (Invoke-RestMethod -Method Post -Uri $authorizeUri -Body $body).access_token
$Uri = "https://$env:APIManagementName.azure-api.net/AzureAdAuth/Weatherforecast/RequireAuth"
$headers = @{
"Authorization" = 'Bearer ' + $token
}
Invoke-RestMethod -Uri $Uri -Method Get -Headers $headers
Basic auth
- Password is downloaded from the Key Vault.
- The username and password are converted to Base64 encoding.
- task: AzurePowerShell@5
continueOnError: true
displayName: Integration test - Gateway validation - BasicAuth
inputs:
azureSubscription: $(AZURE_SVC_NAME)
azurePowerShellVersion: latestVersion
ScriptType: InlineScript
Inline: |
$basicAuthUsername = $env:BASIC_AUTH_USER
$basicAuthSecret = Get-AzKeyVaultSecret -VaultName $env:KeyVaultName -Name $env:KVSECRET_NAME_BASIC_PASS -AsPlainText
$bytes = [System.Text.Encoding]::ASCII.GetBytes($basicAuthUsername + ':' + $basicAuthSecret)
$authHeader = [Convert]::ToBase64String($bytes)
$Uri = "https://$env:APIManagementName.azure-api.net/BasicAuth/Weatherforecast/RequireAuth"
$headers = @{
"Authorization" = 'Basic ' + $authHeader
}
Invoke-RestMethod -Uri $Uri -Method Get -Headers $headers
Certificate auth
- Download the certificate from the Key Vault with PFX file format.
- The downloaded PFX file is attached to the request.
- task: AzurePowerShell@5
continueOnError: true
displayName: Integration test - Gateway validation - CertificateAuth
inputs:
azureSubscription: $(AZURE_SVC_NAME)
azurePowerShellVersion: latestVersion
ScriptType: InlineScript
Inline: |
$cert = Get-AzKeyVaultCertificate -VaultName $env:KeyVaultName -Name $env:KVCERT_NAME_API
$secret = Get-AzKeyVaultSecret -VaultName $env:KeyVaultName -Name $cert.Name
$secretValueText = '';
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret.SecretValue)
try {
$secretValueText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}
$secretByte = [Convert]::FromBase64String($secretValueText)
$x509Cert = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
$x509Cert.Import($secretByte, "", "Exportable,PersistKeySet")
$type = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx
$pfxFileByte = $x509Cert.Export($type, $password)
[System.IO.File]::WriteAllBytes("KeyVault.pfx", $pfxFileByte)
$parameters = @{
Method = "GET"
Uri = "https://$env:APIManagementName.azure-api.net/CertificateAuth/Weatherforecast/RequireAuth"
Certificate = (Get-PfxCertificate "./KeyVault.pfx")
}
Invoke-RestMethod @parameters
Top comments (0)