Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG]: Error authenticating API requests from Octokit JS client when using private key in Azure key vault #2623

Open
1 task done
danielhardej opened this issue Feb 2, 2024 · 6 comments
Labels
Status: Up for grabs Issues that are ready to be worked on by anyone Type: Bug Something isn't working as documented

Comments

@danielhardej
Copy link

danielhardej commented Feb 2, 2024

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
@danielhardej danielhardej added Status: Triage This is being looked at and prioritized Type: Bug Something isn't working as documented labels Feb 2, 2024
Copy link

github-actions bot commented Feb 2, 2024

👋 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! 🚀

@danielhardej
Copy link
Author

PS, this seems related to:

octokit/auth-app.js#465

@kfcampbell kfcampbell moved this from 🆕 Triage to 🔥 Backlog in 🧰 Octokit Active Feb 2, 2024
@kfcampbell kfcampbell added Status: Up for grabs Issues that are ready to be worked on by anyone and removed Status: Triage This is being looked at and prioritized labels Feb 2, 2024
@danielhardej
Copy link
Author

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:

  1. Download the .pem file from the GitHub app settings.

  2. Go to your terminal, and navigate to the directory where the .pem file is located.

  3. Run the following command to convert the .pem file to a base64 string: cat <your-private-key>.pem | base64

  4. 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.)

  5. Retrieve the secret from the Azure Key Vault in the Azure Function app and decode it from base64 to an ascii 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.

@wolfy1339
Copy link
Member

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

@jonasfroeller
Copy link

jonasfroeller commented Sep 27, 2024

@danielhardej Not sure why it didn't work the way I did it for my other projects, but thank you a lot! I tried to fix it with 3 other solutions over 1-2 hours. That is the only approach that worked in production too. I will start to encode all of my privateKeys in the future, because I guess that is the most robust way.

@davidzenisu
Copy link

davidzenisu commented Oct 20, 2024

@wolfy1339 , while I fully understand and support not implementing a feature for a specific platform, it is a bit frustrating to see the official documentation recommending storing the key in a key vault as "sign-only" without any obvious way to use it that way in the official SDK.

Would it be a viable approach to "inject" a method for generating a JWT rather than passing the private key for auth directly? This way, octokit could handle any refresh issues etc. without adding platform-specific code.

Or am I missing something here, and there is already a similar approach for achieving this? :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Up for grabs Issues that are ready to be worked on by anyone Type: Bug Something isn't working as documented
Projects
Status: 🔥 Backlog
Development

No branches or pull requests

5 participants