forked from Azure/azure-sdk-for-js
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Key Vault] Add sample for working with Key Vault in web app (Azure#1…
…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
Showing
16 changed files
with
609 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<string>())</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')]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.