Vault Secrets in a browser plugin challenge
Within your organization there is a need to help its members better manage their secrets. Vault centrally manages secrets to reduce secrets sprawl but may increase workflow friction. To make it easier for individuals to use Vault requires the development of tools to integrate best with their current workflow.
This developer focused tutorial challenges you to build a browser extension that authenticates with Vault and reads a secret through the API.
Prerequisites
This tutorial requires the Chrome browser and a code editor.
Retrieve the browser plugin by cloning the hashicorp/vault-guides repository from GitHub.
$ git clone https://github.com/hashicorp/vault-guides.git
This repository contains supporting content for all of the Vault learn tutorials. The content specific to this tutorial can be found within a sub-directory.
Go into the vault-guides/secrets/browser-plugin
directory.
$ cd vault-guides/secrets/browser-plugin
Working directory
This tutorial assumes that the remainder of commands are executed in this directory.
Start Vault
In a new terminal, start a Vault dev server.
$ vault server -dev -dev-root-token-id root
Insecure operation
Do not run a Vault dev server in production. This approach is only used here to simplify the unsealing process for this demonstration.
Return to the first terminal and export an environment variable for the
vault
CLI to address the Vault server.
$ export VAULT_ADDR=http://127.0.0.1:8200
Login with the root token.
$ vault login root
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token root
token_accessor 6NYAtL0ANmAGVmLX3tx4bVgu
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
Setup Vault authentication
The browser plugin uses the Userpass authentication method.
Enable the userpass authentication method.
$ vault auth enable userpass
Success! Enabled userpass auth method at: userpass/
Create a user named browser
with the password browser
.
$ vault write auth/userpass/users/browser password=browser
Success! Data written to: auth/userpass/users/browser
Load the plugin
Launch Chrome.
From the Extensions menu, select Manage Extensions.
From the Extensions page, enable Developer mode.
Select Load unpacked and open the
vault-guides/secrets/browser-plugin/chrome
directory.The plugin loads and appears in the extensions page.
From the Extensions menu, pin the VaultPass extension.
The plugin is loaded.
Perform stubbed authentication
The plugin implements an authentication form that performs a stubbed authentication.
Click the plugin to open it.
Select the Server tab.
Enter
http://localhost:8200
in the Vault server URL field.Enter
browser
in the Username field.Enter
browser
in the Password field.Enter
userpass
in the Auth Mountpoint field.Click Login to Vault.
The view displays a stubbed list of policies and a stubbed token.
Implement authentication
The options page contains the authentication form. This page is managed with an
HTML file, options.html
and a JavaScript file, options.js
.
Open options.js
in an editor.
// Authorize to Vault with the server location, credentials, and auth path.
async function authToVault(vaultServer, username, password, authMount) {
const authinfo = {
client_token: 'STUB_TOKEN',
policies: ['default', 'vault-pass'],
}
// TODO: authToVault
//
// 1. Create the authentication URL with the username
// 2. Perform a fetch of the authenication URL
// - method: 'POST'
// - headers: 'Content-Type': 'application/json'
// - body: JSON.stringify the password.
//
// 3. With a successful response, read the body as JSON
// 4. With an error, notify an error
await browser.storage.local.set({ vaultToken: authinfo.client_token })
await browser.storage.local.set({ vaultTokenPolicies: authinfo.policies })
showAsLoggedInWith(authinfo.client_token, authinfo.policies)
}
The authToVault
function is stubbed. The Vault token and policies are
retrieved from an object that mimics the data returned from the Vault API.
The Vault token and policies are stored in the browser's local storage and
then those values are sent to the showAsLoggedInWith
function. A TODO
in
the function outlines the steps required to implement an actual
authentication request with Vault.
Create an authentication URL with the username.
var loginUrl = `${vaultServer}/v1/auth/${authMount}/login/${username}`
The authentication URL is created from the
vaultServer
, theauthMount
and theusername
.Perform a fetch of the authenication URL.
fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password: password }), })
The
fetch
function requires theloginUrl
as the first parameter. An object that describes the request is provided as a second parameter. The request'smethod
is aPOST
and theContent-Type
ensures that the response is formatted as JSON. Thebody
includes a JSON stringified object with thepassword
.With a successful response, read the body as JSON.
Append to the end of the
fetch
method an invocation ofthen
that is sent an anonymous function to extract the JSON data from the response. Then append another invocation ofthen
that is sent an anonymous function to parse the JSON data..then(response => { return response.json() }).then( json => { const authinfo = json.auth; browser.storage.local.set({ 'vaultToken': authinfo.client_token }); browser.storage.local.set({ 'vaultTokenPolicies': authinfo.policies }); showAsLoggedInWith(authinfo.client_token, authinfo.policies); })
The Vault token and policies are retrieved from the response, stored in to the browser storage and then those values are sent to the
showAsLoggedInWith
function.With an error, notify an error.
Append to the end of the
fetch
method an invocation ofcatch
that is sent an anonymous function to notify an error with an error message..catch(error => { notify.error(` There was an error while calling<br> ${loginUrl}<br> Please check if your username, password and mountpoints are correct. ${error} `); })
This error catches any issues with the URL, failed authentication, or errors parsing the response data.
async function authToVault(vaultServer, username, password, authMount) { var loginUrl = `${vaultServer}/v1/auth/${authMount}/login/${username}` fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password: password }), }) .then((response) => { return response.json() }) .then((json) => { const authinfo = json.auth browser.storage.local.set({ vaultToken: authinfo.client_token }) browser.storage.local.set({ vaultTokenPolicies: authinfo.policies }) showAsLoggedInWith(authinfo.client_token, authinfo.policies) }) .catch((error) => { notify.error(` There was an error while calling<br> ${loginUrl}<br> Please check if your username, password and mountpoints are correct. ${error} `) }) }
Save the changes to
options.js
.From Chrome's extension menu, select Update.
Click the plugin to open it.
Select the Server tab.
Select Logout.
Enter
http://localhost:8200
in the Vault server URL field.Enter
browser
in the Username field.Enter
browser
in the Password field.Enter
userpass
in the Auth Mountpoint field.Click Login to Vault.
The view displays the token's policies and the token.
Setup Vault for secrets
Every browser tab has a URL that maps to a browser specific page or a host. The plugin parses that URL for the hostname and then retrieves a secret from Vault with that value.
Browser Tab | Vault Secret |
---|---|
chrome://extensions/ | /v1/vaultpass/data/extensions |
https://learn.hashicorp.com/ | /v1/vaultpass/data/learn.hashicorp.com |
Enable a KV-V2 secrets engine with the path vaultpass
.
$ vault secrets enable -path=vaultpass kv-v2
Success! Enabled the kv-v2 secrets engine at: vaultpass/
Create a secret for the extensions
page.
$ vault kv put vaultpass/extensions username=extension_user password=extension_password
Key Value
--- -----
created_time 2021-06-29T17:11:12.577896Z
deletion_time n/a
destroyed false
version 1
Create a policy named vault_pass-policy
that grants read access to all secrets
at the path vaultpass
.
$ vault policy write vault_pass-policy - <<EOF
path "vaultpass/*" {
capabilities = [ "read" ]
}
EOF
Update the user named browser
to grant it the vault_pass-policy
policy.
$ vault write auth/userpass/users/browser policies=vault_pass-policy
Success! Data written to: auth/userpass/users/browser
Implement secret retrieval
The popup page contains the secrets retrieved for the current page. This page is
managed with an HTML file, popup.html
and a JavaScript file, popup.js
.
Open popup.js
in an editor.
async function getSecretsAtUrl(secretsUrl, vaultToken, withSecrets) {
// TODO: getSecretsAtUrl
//
// 1. Perform a fetch of the secret URL
// - method: 'GET'
// - headers: 'Content-Type': 'application/json'
// - headers: 'X-Vault-Token': vaultToken
//
// 2. With a successful response, read the body as JSON and send the secrets
// to the `withSecrets` method.
// 3. With an error, notify an error
const stubSecrets = {
data: {
data: {
username: 'stub_username',
password: 'stub_password',
},
},
}
withSecrets(Object.entries(stubSecrets.data.data))
}
The getSecretsAtUrl
function is stubbed. The function creates an Object that
mimics the secret response from the Vault API. The response is mapped to an
array of entries ([ [ 'username', 'stub_username' ], [ 'password', 'stub_password'] ]
)
and sent to the withSecrets
method. A TODO
in the function outlines the
steps required to implement this secrets request.
Perform a fetch of the secret URL
fetch(secretsUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Vault-Token': vaultToken, }, })
The
fetch
function requires thesecretsUrl
as the first parameter. An object that describes the request is provided as a second parameter. The request'smethod
is aGET
. Theheaders
include theContent-Type
to ensure the response is JSON andX-Vault-Token
for authentication.With a successful response, read the body as JSON and send the secrets to the
withSecrets
method.Append to the end of the
fetch
method an invocation ofthen
that is sent an anonymous function to extract the JSON data from the response. Then append another invocation ofthen
that is sent an anonymous function that maps the data to an array of entries that are sent to thewithSecrets
method..then((response) => { return response.json() }) .then((json) => { withSecrets(Object.entries(json.data.data)) })
With an error, notify an error.
Append to the end of the
fetch
method an invocation ofcatch
that is sent an anonymous function to notify an error with an error message that contains thesecretsUrl
and theerror
..catch((error) => { // Catches response errors // Also catches the error when the json does not have the data key notify.error(`Not able to read secrets at ${secretsUrl} <br/> ${error}`, { removeOption: true, }) })
This error catches any issues with the request or errors parsing the response data.
async function getSecretsAtUrl(secretsUrl, vaultToken, withSecrets) { fetch(secretsUrl, { method: 'GET', headers: { 'X-Vault-Token': vaultToken, 'Content-Type': 'application/json', }, }) .then((response) => { return response.json() }) .then((json) => { withSecrets(Object.entries(json.data.data)) }) .catch((error) => { // Catches response errors // Also catches the error when the json does not have the data key notify.error(`Not able to read secrets at ${secretsUrl} <br/> ${error}`, { removeOption: true, }) }) }
Save the changes to
popup.js
.From Chrome's extension menu, select Update.
Click the plugin to open it.
Select Logout and login again with the same credentials.
Select the Secrets tab.
The secrets that appear are the ones set up at the secret path
vaultpass/extensions
.
Next Steps
You imported a browser plugin and then implemented authentication and secrets retrieval. Expand this plugin by providing:
support to create secrets in the plugin. The secrets that appear when on the extensions page were set through through CLI. When the response contains no secrets the plugin would display a form that allowed the user to enter their own secrets for the page.
support for multiple users to have unique secrets. All of the secrets are stored in the
vaultpass
path. Additional users would require the path schema to change.support for more than KV-V2 secrets. When a user visits a page for a cloud provider login (e.g. Amazon, Azure, Google) the plugin could request dynamic credentials to use for login.
support for secret entry on the page. Popular password plugins find the login fields on the page and insert the secrets. Learn more from the vaultPass project which expands on this plugin further.