ghinstallation

repository·master·Indexed 18 days ago

https://github.com/bradleyfalzon/ghinstallation

A Go library providing an http.RoundTripper implementation that automatically handles authentication for GitHub App installations. It supports authentication via private key files, raw bytes, or RSA private keys, and is compatible with the google/go-github library and standard Go HTTP clients. Features include support for GitHub Enterprise, custom JWT signing via the Signer interface, and automatic token refreshing.

Tokens
3.7K
Snippets
16
Records
18
Agent score
63%

What's inside ghinstallation

  1. Understanding App ID and Installation ID

    master

    To use this library, you need two specific identifiers from GitHub:

    1. App ID: The unique identifier for your GitHub App. Found in your GitHub App settings under: Settings > Developer > settings > GitHub App > About item.
    2. Installation ID: The ID of the specific installation of your app. This is typically found in the payload of a Webhook request sent by GitHub:
    WebHook request
    ...
      "installation": {
        "id": `installation ID`
      }
  2. Authenticate with GitHub using a private key file

    master

    To authenticate as a GitHub App installation, use ghinstallation.NewKeyFromFile. This wraps an existing http.RoundTripper (like http.DefaultTransport) to provide automatic authentication. You must provide the App ID, the Installation ID, and the path to your .pem private key file. The resulting transport can be used directly in an http.Client or passed to github.NewClient from the google/go-github library.

    import (
    	"log"
    	"net/http"
    
    	"github.com/bradleyfalzon/ghinstallation/v2"
    	"github.com/google/go-github"
    )
    
    func main() {
    	// Shared transport to reuse TCP connections.
    	tr := http.DefaultTransport
    
    	// Wrap the shared transport for use with the app ID 1 authenticating with installation ID 99.
    	itr, err := ghinstallation.NewKeyFromFile(tr, 1, 99, "2016-10-19.private-key.pem")
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Use installation transport with github.com/google/go-github
    	client := github.NewClient(&http.Client{Transport: itr})
    }
  3. Authenticate with GitHub Enterprise

    master

    When using GitHub Enterprise, create the installation transport as usual, but set the BaseURL field on the returned transport to your enterprise's API endpoint. Then, use github.NewEnterpriseClient to initialize the GitHub client.

    import (
    	"log"
    	"net/http"
    
    	"github.com/bradleyfalzon/ghinstallation/v2"
    	"github.com/google/go-github"
    )
    
    const GitHubEnterpriseURL = "https://github.example.com/api/v3"
    
    func main() {
    	// Shared transport to reuse TCP connections.
    	tr := http.DefaultTransport
    
    	// Wrap the shared transport for use with the app ID 1 authenticating with installation ID 99.
    	itr, err := ghinstallation.NewKeyFromFile(tr, 1, 99, "2016-10-19.private-key.pem")
    	if err != nil {
    		log.Fatal(err)
    	}
    	// Set the BaseURL for Enterprise
    	itr.BaseURL = GitHubEnterpriseURL
    
    	// Use installation transport with github.com/google/go-github
    	client := github.NewEnterpriseClient(GitHubEnterpriseURL, GitHubEnterpriseURL, &http.Client{Transport: itr})
    }
  4. Customizing signing behavior with a Signer

    master

    If you need to use keys stored in an external system like a KMS, you can implement the Signer interface and pass it to NewAppsTransportWithOptions. This allows you to control how the JWTs used for authentication are signed. You can then create a transport from this AppsTransport using NewFromAppsTransport.

    // Example of using a custom signer (e.g., for KMS integration)
    signer := &myCustomSigner{
      key: "https://url/to/key/vault",
    }
    
    // Create the AppsTransport with the custom signer
    atr := NewAppsTransportWithOptions(http.DefaultTransport, 1, WithSigner(signer))
    
    // Create the final transport for a specific installation
    tr := NewFromAppsTransport(atr, 99)
  5. Configure InstallationTokenOptions

    master

    The Transport struct includes an InstallationTokenOptions field (of type *github.InstallationTokenOptions). You can use this to restrict the scope of the generated access token by specifying required permissions or repository access.

    // Note: This requires importing github.com/google/go-github/v88/github
    options := &github.InstallationTokenOptions{
        // Add specific options here as defined by the GitHub API
    }
    
    // To use these options, you must manually construct the Transport
    // or use a pattern that allows setting this field before the first token fetch.
    transport.InstallationTokenOptions = options
  6. Use Transport as an http.RoundTripper

    master

    Because Transport implements http.RoundTripper, you can use it directly in an http.Client. This allows you to make standard HTTP requests that are automatically signed with the correct GitHub App installation token.

    When RoundTrip is called, the Transport checks if the current token is expired (or near expiration) and refreshes it if necessary before proceeding with the request.

    // Create the transport
    transport, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, appID, installationID, "key.pem")
    if err != nil {
        log.Fatal(err)
    }
    
    // Use it in a standard http.Client
    client := &http.Client{
        Transport: transport,
    }
    
    // This request will automatically include the 'Authorization: token <token>' header
    resp, err := client.Get("https://api.github.com/repos/owner/repo")
  7. Initialize AppsTransport for GitHub App authentication

    master

    The AppsTransport type implements http.RoundTripper and automatically handles GitHub App authentication by injecting the required JWT into the Authorization header of outgoing requests.

    You can initialize it using several methods depending on how you store your private key:

    1. From a file path: Use NewAppsTransportKeyFromFile to read a PEM-encoded private key from the filesystem.
    2. From raw bytes: Use NewAppsTransport to parse a PEM-encoded byte slice.
    3. From an RSA private key: Use NewAppsTransportFromPrivateKey if you already have a *rsa.PrivateKey object.
    4. With custom options: Use NewAppsTransportWithOptions to provide custom configuration, such as a custom Signer via WithSigner.

    Note: It is recommended to share the underlying http.RoundTripper across multiple installations to ensure efficient reuse of TCP connections.

    // Example: Initializing from a file
    transport, err := ghinstallation.NewAppsTransportKeyFromFile(http.DefaultTransport, 12345, "path/to/key.pem")
    if err != nil {
        log.Fatal(err)
    }
    
    client := &http.Client{
        Transport: transport,
    }
    
    // Now use the client to make authenticated requests to GitHub
    resp, err := client.Get("https://api.github.com/app/repos")
  8. RSASigner.Sign

    master

    Signs the provided JWT claims using the RSA key and method configured in the RSASigner instance.

    Parameters:

    • claims: An object implementing the jwt.Claims interface.

    Returns:

    • A signed JWT token as a string.
    • An error if the signing process fails.
    func (s *RSASigner) Sign(claims jwt.Claims) (string, error)
  9. Inspect installation permissions and repositories

    master

    Once a Transport has successfully performed its first request (or you manually trigger a token refresh), you can inspect the permissions and repositories associated with the current installation token using the Permissions() and Repositories() methods.

    permissions, err := transport.Permissions()
    if err != nil {
        // handle error
    }
    
    repos, err := transport.Repositories()
    if err != nil {
        // handle error
    }
  10. Use the Signer interface to sign JWT tokens

    master

    The Signer interface provides a way to sign JWT claims using predetermined key material. It is a wrapper around jwt.SigningMethod that simplifies the signing process by abstracting away the specific key and method details. To use it, implement the Sign(claims jwt.Claims) (string, error) method.

    type Signer interface {
    	Sign(claims jwt.Claims) (string, error)
    }
  11. Handle HTTPError during token refresh

    master

    If a token refresh fails (e.g., due to network issues or invalid credentials), the Transport returns an *HTTPError. This error type allows you to inspect the root cause and the original HTTP response from GitHub.

    // Example of checking for HTTPError
    if err != nil {
        var httpErr *ghinstallation.HTTPError
        if errors.As(err, &httpErr) {
            fmt.Printf("Error: %s\n", httpErr.Message)
            fmt.Printf("Status: %s\n", httpErr.Response.Status)
            fmt.Printf("Root Cause: %v\n", httpErr.RootCause)
        }
    }