Skip to content

Commit

Permalink
[Key Vault] Add sample for working with Key Vault in web app (Azure#1…
Browse files Browse the repository at this point in the history
…3139)

## What
Add a doc and sample demonstrating various approaches to fetching data
from Key Vault from a web application. The two approaches shown here are:

1. Using a back end server to fetch secrets
2. Using Azure API Management to fetch secrets

## Why
Key Vault lacks support for CORS policies, so a browser cannot make requests
directly to Key Vault. We've seen multiple cases where this question came up
and wanted to provide a place we can point folks to for guidance.
  • Loading branch information
maorleger authored Jan 21, 2021
1 parent 379a18d commit 8301c9a
Show file tree
Hide file tree
Showing 16 changed files with 609 additions and 13 deletions.
178 changes: 178 additions & 0 deletions samples/cors/arm-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"baseName": {
"type": "string",
"defaultValue": "[resourceGroup().name]",
"metadata": {
"description": "The base resource name."
}
},
"appUri": {
"type": "string",
"defaultValue": "http://localhost:1234",
"metadata": {
"description": "The URI of the web application to allow CORS access. By default parcel will run it on http://localhost:1234"
}
}
},
"variables": {
"serviceName": "[concat(parameters('baseName'), '-api')]",
"vaultName": "[concat(parameters('baseName'), '-keyvault')]"
},
"resources": [
{
"type": "Microsoft.ApiManagement/service",
"apiVersion": "2019-01-01",
"name": "[variables('serviceName')]",
"location": "[resourceGroup().location]",
"properties": {
"enableClientCertificate": true,
"virtualNetworkType": "External",
"publisherEmail": "noreply@example.com",
"publisherName": "api-admin"
},
"sku": {
"name": "Developer"
},
"identity": {
"type": "SystemAssigned"
},
"resources": [
{
"type": "properties",
"apiVersion": "2019-01-01",
"name": "vault-name",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service', variables('serviceName'))]"
],
"properties": {
"displayName": "vault-name",
"value": "[variables('vaultName')]",
"secret": false
}
},
{
"type": "properties",
"apiVersion": "2019-01-01",
"name": "secret-name",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service', variables('serviceName'))]"
],
"properties": {
"displayName": "secret-name",
"value": "test",
"secret": false
}
},
{
"type": "properties",
"apiVersion": "2019-01-01",
"name": "app-uri",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service', variables('serviceName'))]"
],
"properties": {
"displayName": "app-uri",
"value": "[parameters('appUri')]",
"secret": false
}
},
{
"type": "apis",
"apiVersion": "2019-01-01",
"name": "secret-manager",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service', variables('serviceName'))]"
],
"properties": {
"displayName": "secret-manager",
"apiRevision": "1",
"path": "",
"protocols": ["https"],
"isCurrent": true
},
"resources": [
{
"type": "operations",
"apiVersion": "2019-01-01",
"name": "get-secret",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service/apis', variables('serviceName'), 'secret-manager')]"
],
"properties": {
"displayName": "get-secret",
"method": "GET",
"urlTemplate": "get-secret",
"responses": []
},
"resources": [
{
"type": "policies",
"apiVersion": "2019-01-01",
"name": "policy",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service/apis/operations', variables('serviceName'), 'secret-manager', 'get-secret')]",
"[resourceId('Microsoft.ApiManagement/service/properties', variables('serviceName'), 'vault-name')]",
"[resourceId('Microsoft.ApiManagement/service/properties', variables('serviceName'), 'secret-name')]"
],
"properties": {
"value": "<policies>\r\n <inbound>\r\n <base />\r\n <cors>\r\n <allowed-origins>\r\n <origin>{{app-uri}}</origin>\r\n </allowed-origins>\r\n <allowed-methods>\r\n <method>GET</method>\r\n <method>POST</method>\r\n </allowed-methods>\r\n </cors>\r\n <send-request mode=\"new\" response-variable-name=\"vault-secret\" timeout=\"20\" ignore-error=\"false\">\r\n <set-url>https://{{vault-name}}.vault.azure.net/secrets/{{secret-name}}/?api-version=7.0</set-url>\r\n <set-method>GET</set-method>\r\n <authentication-managed-identity resource=\"https://vault.azure.net\" />\r\n </send-request>\r\n </inbound>\r\n <backend>\r\n <return-response response-variable-name=\"existing context variable\">\r\n <set-status code=\"200\" />\r\n <set-body>@(((IResponse)context.Variables[\"vault-secret\"]).Body.As&lt;string&gt;())</set-body>\r\n </return-response>\r\n </backend>\r\n <outbound>\r\n <base />\r\n </outbound>\r\n <on-error>\r\n <base />\r\n </on-error>\r\n</policies>",
"format": "xml"
}
}
]
}
]
}
]
},
{
"type": "Microsoft.KeyVault/vaults",
"name": "[variables('vaultName')]",
"apiVersion": "2018-02-14",
"location": "[resourceGroup().location]",
"dependsOn": ["[resourceId('Microsoft.ApiManagement/service', variables('serviceName'))]"],
"properties": {
"tenantId": "[subscription().tenantId]",
"accessPolicies": [
{
"tenantId": "[reference(resourceId('Microsoft.ApiManagement/service', variables('serviceName')), '2019-01-01', 'Full').identity.tenantId]",
"objectId": "[reference(resourceId('Microsoft.ApiManagement/service', variables('serviceName')), '2019-01-01', 'Full').identity.principalId]",
"permissions": {
"secrets": ["get"]
}
}
],
"sku": {
"family": "A",
"name": "standard"
}
},
"resources": [
{
"type": "secrets",
"name": "test",
"apiVersion": "2018-02-14",
"dependsOn": ["[resourceId('Microsoft.KeyVault/vaults', variables('vaultName'))]"],
"tags": {},
"properties": {
"value": "I am a secret!",
"contentType": "string"
}
}
]
}
],
"outputs": {
"azure_keyvault_name": {
"type": "string",
"value": "[variables('vaultName')]"
},
"azure_api_name": {
"type": "string",
"value": "[variables('serviceName')]"
}
}
}
1 change: 1 addition & 0 deletions samples/cors/ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cache
123 changes: 123 additions & 0 deletions samples/cors/ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Using Azure Key Vault in a Web Application

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the same-origin policy. The same-origin policy prevents a malicious site from reading sensitive data from another site. Sometimes, you might want to allow other sites to make cross-origin requests to your app. That's where [Cross-Origin Resource Sharing (CORS)][cors] policies become necessary.

While CORS is configurable for some Azure services, Azure Key Vault does not currently support CORS natively. So what can you do if you'd like to integrate with Azure Key Vault from a client side web application?

Fortunately, there are a few options:

- Use a back end server to route requests to Azure Key Vault.
- Use [Azure API Management][azureapimanagement] to route requests to Azure Key Vault.

> Remember: CORS is a _browser_ restriction, and is not a concern for Node applications.
Interested in enabling CORS in Key Vault? [Let us know!](https://feedback.azure.com/forums/906355-azure-key-vault/suggestions/34753195-enable-cors-for-key-vault)

## Use a back end server

With this approach you'll use a server process (like [ASP.NET][asp] or [Express][express]) to route requests to Azure Key Vault. Since you own the client and server processes you can freely add CORS policies to support your requests.

If your web application already has a back end component this will be the most straightforward approach. You'll simply add a new API endpoint to your server which will be responsible for fetching secrets from Azure Key Vault and returning it to the user. This allows you to have complete control and customization over the security and authorization of calls to Key Vault since all calls will go through a privileged environment (your server).

## Use Azure API Management

But what if you don't have a server in place for your Single Page Application? Thankfully Azure provides a managed API service that can sit between your web application and Azure Key Vault and provide the CORS policy that you need to interact with Azure Key Vault.

It provides all of the same benefits as having your own back end API while avoiding the need to separately deploy, manage, and secure your own server. You can define a single API that is responsible for retrieving secrets and apply best-in-class security to control access to your Key Vault.

# Sample code

This simple example demonstrates how to get started with both of these approaches. In this example, we create a simple [Express][express] application, connect and upload a single test secret, and fetch it via a web application using either our back end server or Azure API Management.

## Security considerations

This sample demonstrates a few alternatives to integrating with Azure Key Vault from the browser, and is purposely kept simple. Remember, the browser is an _insecure_ environment and we encourage you to familiarize yourself with Azure's security policies to avoid leaking credentials to an unauthorized user in a production application. Please refer to Azure Key Vault's [security overview][keyvaultsecurity] to learn more about securing your Key Vault's data.

## Prerequisites

The sample is compatible with Node.js >= 8.0.0

Before running the samples in Node, they must be compiled to JavaScript using the TypeScript compiler. For more information on TypeScript, see the [TypeScript documentation][typescript].

You need [an Azure subscription][freesub] and the following resources created to run this sample:

- An Azure Key Vault. Please refer to the [Key Vault documentation][keyvault] for additional information on Azure Key Vault.
- An Azure API Management. Please refer to [Azure API Management][azureapimanagement] for additional information on Azure API Management.

To quickly create the necessary resources in Azure and to receive the necessary environment variables for them, you can deploy our sample template by clicking:

[![](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-sdk-for-js%2Fmaster%2Fsamples%2Fcors%2Farm-template.json)

The above template will create the necessary resources for you and the output tab will contain the environment variables you'll need as soon as deployment succeeds. When the deployment is finished, head over to the "outputs" tab and copy the outputs to a local file - you'll need them in the next step.

> Azure API Management can take a while to deploy so we recommend starting the deployment now before reading the rest of this document.
Next, create a service principal for the backend application and configure its access to Azure Key Vault:

```Bash
az ad sp create-for-rbac -n <your-application-name, can be anything unique> --skip-assignment
```

Output:

```json
{
"appId": "generated-app-ID",
"displayName": "dummy-app-name",
"name": "http://dummy-app-name",
"password": "random-password",
"tenant": "tenant-ID"
}
```

Save the values returned in a safe location as follows:

```
AZURE_CLIENT_ID=<appId>
AZURE_CLIENT_SECRET=<password>
AZURE_TENANT_ID=<tenant>
```

Take note of the service principal objectId:

```PowerShell
az ad sp show --id <appId> --query objectId
```

Output:

```
"<your-service-principal-object-id>"
```

Grant the above mentioned application authorization to perform key operations on the keyvault:

```Bash
az keyvault set-policy --name <AZURE_KEYVAULT_NAME from deployment outputs tab> --spn <your-service-principal-object-id> --secret-permissions get set
```

## Running the sample

Once the above is created you'll want to ensure the necessary environment variables are set. You'll do this for both the client and server:

### Client environment variables

Copy `client/sample.env` as `client/.env` and provide the `AZURE_API_NAME` environment variable which you should have received from the outputs of the ARM template.

You can find the value of `AZURE_API_NAME` in the outputs tab of your deployment.

### Server environment variables

Copy `server/sample.env` as `server/.env` and provide the necessary environment variables.

The values for `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_TENANT_ID` are the values you saved in a previous step.

You can find the value of `AZURE_KEYVAULT_NAME` in the outputs tab of your deployment.

[cors]: https://developer.mozilla.org/docs/Web/HTTP/CORS
[azureapimanagement]: https://docs.microsoft.com/azure/api-management/api-management-key-concepts
[express]: https://expressjs.com/
[keyvaultsecurity]: https://docs.microsoft.com/azure/key-vault/general/security-overview
[asp]: https://dotnet.microsoft.com/apps/aspnet
[freesub]: https://azure.microsoft.com/free
[keyvault]: https://docs.microsoft.com/azure/key-vault/
81 changes: 81 additions & 0 deletions samples/cors/ts/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1"
crossorigin="anonymous"
/>
</head>
<body>
<div class="app container p-4">
<div class="row">
<div class="col-sm-6">
<div class="card md">
<div class="card-header">Use Azure API Management to access Azure Key Vault</div>
<div class="card-body">
<div class="row">
<div class="col-sm-8">
<input
type="password"
class="form-control"
id="subscription-key"
placeholder="Enter subscription key..."
/>
</div>
<div class="col-sm-4">
<button id="fetch-from-azure" class="btn btn-primary">Fetch secret</button>
</div>
</div>
<div id="azure-api-info" class="row pt-4">
<p>
To keep this example simple we're using your Azure API Management subscription key
to provide access to Azure API Management.
</p>
<p>
To find it: go to your API Management, click on "Subscriptions", and copy the
Primary key of the "Built-in all-access subscription".
</p>
<p>
Remember: this just keeps our example simple. Do not share this key or include it
in your bundle. <br />
Instead, secure your Key Vault API with one of the many best-in-class security
features offered by Azure API Management. <br />
For more information please refer to
<a
href="https://docs.microsoft.com/azure/api-management/howto-protect-backend-frontend-azure-ad-b2c"
target="_blank"
>this document</a
>.
</p>
</div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card md">
<div class="card-header">Use a local server to access Azure Key Vault</div>
<div class="card-body">
<div class="row px-4">
<button id="fetch-from-server" class="btn btn-primary">Fetch secret</button>
</div>
<div class="row p-4">
Using a local server hosted on http://localhost:4000 we can access Azure Key Vault
by sending a request to http://localhost:4000/api/secret which has been set up to
fetch a hardcoded test secret.
</div>
</div>
</div>
<div class="card md mt-4">
<h5 class="card-header">Secret will display here</h5>
<div class="card-body">
<span id="secret-display"></span>
</div>
</div>
</div>
</div>
</div>
<script src="./index.ts"></script>
</body>
</html>
Loading

0 comments on commit 8301c9a

Please sign in to comment.