[BUG]: Error authenticating API requests from Octokit JS client when using private key in Azure key vault
danielhardej opened this issue · comments
What happened?
Note: this could be more to do with a gap in documentation, or possibly even a limitation of using Microsoft's Azure Key Vault with a GitHub app, but I feel it's still pertinent given that it is related to advice given in GitHub's documentation (See: Secure your app's credentials under Best practices for creating a GitHub App)
The problem
When running Octokit.JS in an Azure function that is part of a GitHub app, the error Error: secretOrPrivateKey must be an asymmetric key when using RS256
gets thrown when making an API request such as:
const adminMembers = await octokit.rest.orgs.listMembers({
org: orgName,
role: 'admin',
});
This happens when storing the app private key used to authenticate Octokit in Azure key vault as a key or a secret, as an environment variable, or just as a text string.
This happens with the following set up:
- GitHub app created and subscribed to Repository, Issue, and PR events
- The app delivers a payload containing data on these events via webhook
- Payload gets sent to an Azure function app (via the function URL), created in Node.js
- The app's webhook payload successfully triggers the function's HTTP trigger
- Within the function app, an Octokit instance is created and attempts to authenticate
- API requests, such as list members or list issues, are attempted (but the errors start appearing)
Storing as a key in Azure Key Vault
The first scenario was storing the .pem
file (the one downloaded from the Private keys
section of the app's settings) as a key in Azure key vault, in line with the guidance in the documentation on Private keys. It can be accessed by the Azure function app from the key vault without errors.
The private key is obtained in the following way, in line with the guidance in Microsoft's documentation: Azure Key Vault Key client library for JavaScript
const { Octokit } = require("@octokit/rest");
const { createAppAuth } = require("@octokit/auth-app");
const { DefaultAzureCredential } = require("@azure/identity");
const { KeyClient } = require("@azure/keyvault-keys");
const credential = new DefaultAzureCredential();
const vaultName = process.env.KEY_VAULT_NAME;
const vaultURL = `https://${vaultName}.vault.azure.net`;
const client = new KeyClient(vaultURL, credential);
const keyName = process.env.KEY_NAME;
const keyBundle = await client.getKey(keyName);
context.log(`Key bundle: ${JSON.stringify(keyBundle.key, null, 2)}`);
const appPrivateKey = keyBundle.key.e;
Storing as a secret in Azure Key Vault
I then also tried storing the private key (the contents of the .pem
file, rather than the file itself) as a secret in AKV, rather than a key, and then accessing it with:
const { DefaultAzureCredential } = require("@azure/identity");
const { SecretClient } = require("@azure/keyvault-secrets");
const credential = new DefaultAzureCredential();
const vaultName = process.env.KEY_VAULT_NAME;
const url = `https://${vaultName}.vault.azure.net`;
const client = new SecretClient(url, credential);
const secretName = process.env.SECRET_NAME;
const secret = await client.getSecret(secretName);
This ended up with the same problem: Error: secretOrPrivateKey must be an asymmetric key when using RS256
.
Additionally, attempting to print the key to the console revealed that AKV provides a buffer object, rather than the key itself. This doesn't seem to be very useful, as the guidelines on Authentication in the Octokit README suggest that the private key needs to be passed as a string.
Using environment variables/plain text
Putting Azure key vault aside, I also tried:
- Storing the private key as an environment variable
- Passing the key in plain text in the function code
Both gave the Error: secretOrPrivateKey must be an asymmetric key when using RS256
again.
Another interesting thing
One curious thing is when the errors are thrown: no errors are thrown when a new instance of Octokit
is created and authenticated as a GitHub app installation (whether that's using Azure key vault, env variables, or plain text.)
Instead, errors are only thrown at the point when an API request is make with Octokit.
The Octokit instance is created in the following way in accordance with the guidelines in the README (note the installation ID is obtained from the webhook payload):
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: process.env.APP_ID,
privateKey: appPrivateKey,
installationId: req.body.installation.id,
},
});
Versions
@octokit/rest version: 20.0.2
Node version v18.12.1
Relevant log output
No response
Code of Conduct
- I agree to follow this project's Code of Conduct
👋 Hi! Thank you for this contribution! Just to let you know, our GitHub SDK team does a round of issue and PR reviews twice a week, every Monday and Friday! We have a process in place for prioritizing and responding to your input. Because you are a part of this community please feel free to comment, add to, or pick up any issues/PRs that are labled with Status: Up for grabs
. You & others like you are the reason all of this works! So thank you & happy coding! 🚀
PS, this seems related to:
So, after digging into this, it looks like this might a documentation opportunity rather than a bug, as it's more to do with a limitation of the Azure Key Vault and the guideines in the GitHub documentation.
Going back to the drawing board and trying to store the private key as a secret - not a key - in AKV seems to work. But there's a catch: the RSA private key you download for the GitHub app needs to be encoded as a base64
string before it's added as an AKV secret.
Here's the TL;DR of what you need to do to get it to work:
-
Download the
.pem
file from the GitHub app settings. -
Go to your terminal, and navigate to the directory where the
.pem
file is located. -
Run the following command to convert the
.pem
file to a base64 string:cat <your-private-key>.pem | base64
-
Copy the
base64
encoded string from the terminal output and store it as a secret in the Azure Key Vault. Assign it a descriptive name in AKV, and provide that name in the function app (below I store it as an env variable.) -
Retrieve the secret from the Azure Key Vault in the Azure Function app and decode it from
base64
to anascii
string with the following code:
const vaultName = process.env.KEY_VAULT_NAME;
const keyName = process.env.KEY_NAME;
const appId = process.env.APP_ID;
const vaultURL = `https://${vaultName}.vault.azure.net`;
const credential = new DefaultAzureCredential();
const client = new SecretClient(vaultURL, credential);
const secretBundle = await client.getSecret(keyName);
const privateKeyString = Buffer.from(secretBundle.value, 'base64').toString('ascii');
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: { appId, privateKey: privateKeyString, installationId },
});
BUT, that being said, it would be interesting to find out if there's any opportunity to add a new feature to Octokit that would allow users to pass the key object from AKV to the new Octokit
instance in the app to authenticate it?
It would be good to get the 2cents of the Octokit team on this. Specifically to find what the appetite for this would be with respect to i) whether or not enough users have a desire for this feature, and ii) whether the API/Octokit product and engineering teams see this as something that's worth the time/effort.
We wouldn't want to add a feature for only one platform. If it uses a standardized process that is supported on many platforms then it would be fine