Azure API Management authentication - Part.3
Published: 2022-04-23 16:51:57

# Summary
Following the article Part.2, I would like to share about how the sample code [api-management-sample](https://github.com/koheikawata/api-management-sample) works. The secret management with Key Vault follow the concpet from [Azure Pipelines secret management with Key Vault](https://dev.to/koheikawata/azure-pipelines-secret-management-with-key-vault-3e01).

- [Azure API Management authentication - Part.1](https://dev.to/koheikawata/azure-api-management-authentication-part1-1198)
- [Azure API Management authentication - Part.2](https://dev.to/koheikawata/azure-api-management-authentication-part2-pd9)

# TOC
- [API deployment](#api-deployment)
- [Subscription Key Validation vs Gateway Validation](#vs)
  - [Bicep](#bicep)
  - [Azure Piplines Yaml](#yaml)
- [Integration test](#integration-test)

# API deployment <a name="api-deployment"></a>

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](https://github.com/koheikawata/api-management-sample/blob/main/src/api/AzureAdAuth/AzureAdAuth.csproj)
```
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
```

In `Program.cs`, the API documentation should be defined

[Program.cs](https://github.com/koheikawata/api-management-sample/blob/main/src/api/AzureAdAuth/Program.cs)
```
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.

[api_pipeline.yml](https://github.com/koheikawata/api-management-sample/blob/main/pipelines/api_pipeline.yml)
```
- 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 <a name="vs"></a>

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**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6p2t5izokzfnnqy43dq2.png) 

**Gateway Validation**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nc3gpp5mo98vycvb7c2a.png) 

## Bicep <a name="bicep"></a>

[azuredeploy_subscription.bicep](https://github.com/koheikawata/api-management-sample/blob/main/src/bicep/azuredeploy_subscription.bicep)
[azuredeploy_gateway.bicep](https://github.com/koheikawata/api-management-sample/blob/main/src/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 <a name="yaml"></a>

The only difference is which bicep file the pipeline execute, `azuredeploy_subscription.bicep` or `azuredeploy_gateway.bicep`

[iac_pipeline_subscription.yml](https://github.com/koheikawata/api-management-sample/blob/main/pipelines/iac_pipeline_subscription.yml)
[iac_pipeline_gateway.yml](https://github.com/koheikawata/api-management-sample/blob/main/pipelines/iac_pipeline_gateway.yml)

# Integration test <a name="integration-test"></a>

The CI/CD pipeline [api_pipeline.yml](https://github.com/koheikawata/api-management-sample/blob/main/pipelines/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
```