Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion examples/app_authentication/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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}"
```
7 changes: 7 additions & 0 deletions github/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
55 changes: 36 additions & 19 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
Expand All @@ -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"},
},
},
},
Expand Down Expand Up @@ -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. " +
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -395,25 +403,34 @@ 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.
// Any occurrence of \n in the `pem_file` argument's value
// (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})
}
Expand Down