From 47d5aa55e39ba0089cc6b925fd564c3b38c55dc1 Mon Sep 17 00:00:00 2001 From: u-kai <76635578+u-kai@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:22:16 +0900 Subject: [PATCH 1/2] feat: Support pre-signed JWT for GitHub App authentication Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com> --- github/apps.go | 7 ++++++ github/provider.go | 55 ++++++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/github/apps.go b/github/apps.go index b35350b748..3d125777da 100644 --- a/github/apps.go +++ b/github/apps.go @@ -15,6 +15,13 @@ import ( "github.com/go-jose/go-jose/v3/jwt" ) +// GenerateOAuthTokenFromJWT exchanges a pre-signed GitHub App JWT for an installation access token. +// The JWT must be signed by the GitHub App's private key and contain the correct claims. +// This allows signing to be handled externally (e.g., by a secrets manager or KMS). +func GenerateOAuthTokenFromJWT(apiURL *url.URL, appInstallationID, appJWT string) (string, error) { + return getInstallationAccessToken(apiURL, appJWT, appInstallationID) +} + // GenerateOAuthTokenFromApp generates a GitHub OAuth access token from a set of valid GitHub App credentials. // The returned token can be used to interact with both GitHub's REST and GraphQL APIs. func GenerateOAuthTokenFromApp(apiURL *url.URL, appID, appInstallationID, pemData string) (string, error) { diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..0f8ccc63bb 100644 --- a/github/provider.go +++ b/github/provider.go @@ -103,7 +103,7 @@ func Provider() *schema.Provider { Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.EnvDefaultFunc("GITHUB_APP_ID", nil), Description: descriptions["app_auth.id"], }, @@ -114,11 +114,20 @@ func Provider() *schema.Provider { Description: descriptions["app_auth.installation_id"], }, "pem_file": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("GITHUB_APP_PEM_FILE", nil), - Description: descriptions["app_auth.pem_file"], + Type: schema.TypeString, + Optional: true, + Sensitive: true, + DefaultFunc: schema.EnvDefaultFunc("GITHUB_APP_PEM_FILE", nil), + Description: descriptions["app_auth.pem_file"], + ExactlyOneOf: []string{"app_auth.0.pem_file", "app_auth.0.jwt"}, + }, + "jwt": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + DefaultFunc: schema.EnvDefaultFunc("GITHUB_APP_JWT", nil), + Description: descriptions["app_auth.jwt"], + ExactlyOneOf: []string{"app_auth.0.pem_file", "app_auth.0.jwt"}, }, }, }, @@ -324,7 +333,8 @@ func init() { "`token`. Anonymous mode is enabled if both `token` and `app_auth` are not set.", "app_auth.id": "The GitHub App ID.", "app_auth.installation_id": "The GitHub App installation instance ID.", - "app_auth.pem_file": "The GitHub App PEM file contents.", + "app_auth.pem_file": "The GitHub App PEM file contents. Exactly one of `pem_file` or `jwt` must be set.", + "app_auth.jwt": "A pre-signed GitHub App JWT. Exactly one of `pem_file` or `jwt` must be set.", "write_delay_ms": "Amount of time in milliseconds to sleep in between writes to GitHub API. " + "Defaults to 1000ms or 1s if not set.", "read_delay_ms": "Amount of time in milliseconds to sleep in between non-write requests to GitHub API. " + @@ -381,12 +391,10 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc { if appAuth, ok := d.Get("app_auth").([]any); ok && len(appAuth) > 0 && appAuth[0] != nil { appAuthAttr := appAuth[0].(map[string]any) - var appID, appInstallationID, appPemFile string + var appID, appInstallationID string if v, ok := appAuthAttr["id"].(string); ok && v != "" { appID = v - } else { - return nil, wrapErrors([]error{fmt.Errorf("app_auth.id must be set and contain a non-empty value")}) } if v, ok := appAuthAttr["installation_id"].(string); ok && v != "" { @@ -395,7 +403,21 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc { return nil, wrapErrors([]error{fmt.Errorf("app_auth.installation_id must be set and contain a non-empty value")}) } - if v, ok := appAuthAttr["pem_file"].(string); ok && v != "" { + apiPath := "" + if isGHES { + apiPath = GHESRESTAPIPath + } + appAPIURL := baseURL.JoinPath(apiPath) + + var appToken string + var err error + + if v, ok := appAuthAttr["jwt"].(string); ok && v != "" { + appToken, err = GenerateOAuthTokenFromJWT(appAPIURL, appInstallationID, v) + } else if v, ok := appAuthAttr["pem_file"].(string); ok && v != "" { + if appID == "" { + return nil, wrapErrors([]error{fmt.Errorf("app_auth.id must be set when using pem_file")}) + } // The Go encoding/pem package only decodes PEM formatted blocks // that contain new lines. Some platforms, like Terraform Cloud, // do not support new lines within Environment Variables. @@ -403,17 +425,12 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc { // (explicit value, or default value taken from // GITHUB_APP_PEM_FILE Environment Variable) is replaced with an // actual new line character before decoding. - appPemFile = strings.ReplaceAll(v, `\n`, "\n") + appPemFile := strings.ReplaceAll(v, `\n`, "\n") + appToken, err = GenerateOAuthTokenFromApp(appAPIURL, appID, appInstallationID, appPemFile) } else { - return nil, wrapErrors([]error{fmt.Errorf("app_auth.pem_file must be set and contain a non-empty value")}) - } - - apiPath := "" - if isGHES { - apiPath = GHESRESTAPIPath + return nil, wrapErrors([]error{fmt.Errorf("exactly one of app_auth.pem_file or app_auth.jwt must be set")}) } - appToken, err := GenerateOAuthTokenFromApp(baseURL.JoinPath(apiPath), appID, appInstallationID, appPemFile) if err != nil { return nil, wrapErrors([]error{err}) } From 98b761b7453022fee14cc9f512c3ef24af097221 Mon Sep 17 00:00:00 2001 From: u-kai <76635578+u-kai@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:27:08 +0900 Subject: [PATCH 2/2] docs: add pre-signed JWT authentication example to app_authentication Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com> --- examples/app_authentication/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/examples/app_authentication/README.md b/examples/app_authentication/README.md index 7f2852d8dc..9225060cba 100644 --- a/examples/app_authentication/README.md +++ b/examples/app_authentication/README.md @@ -4,7 +4,7 @@ This example demonstrates authenticating using a GitHub App. The example will create a repository in the specified organization. -You may use variables passed via command line: +## Using a PEM file ```console export GITHUB_OWNER= @@ -13,6 +13,18 @@ export GITHUB_APP_INSTALLATION_ID= export GITHUB_APP_PEM_FILE= ``` +## Using a pre-signed JWT + +If you sign the GitHub App JWT externally (e.g., using AWS KMS or HashiCorp Vault), +you can pass the signed JWT directly instead of providing a PEM file. +In this case, `GITHUB_APP_ID` is not required. + +```console +export GITHUB_OWNER= +export GITHUB_APP_INSTALLATION_ID= +export GITHUB_APP_JWT= +``` + ```console terraform apply -var "organization=${GITHUB_ORG}" ```