Skip to content

Instantly share code, notes, and snippets.

@lox
Created July 14, 2021 02:36
Show Gist options
  • Save lox/6d806d425b3584814a5163db30e010dc to your computer and use it in GitHub Desktop.
Save lox/6d806d425b3584814a5163db30e010dc to your computer and use it in GitHub Desktop.
A utility for generating GitHub Access Tokens from a GitHub App
package main
import (
"bytes"
"crypto/rsa"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"time"
"github.com/golang-jwt/jwt"
)
// CLI Flags
var (
appID = flag.String("app-id", "", "The GitHub App Identifier")
installationID = flag.String("installation-id", "", "The GitHub App Installation ID")
keyFile = flag.String("key-file", "", "The GitHub App Private Key PEM file")
repo = flag.String("repo", "", "The repository name to limit access to")
)
func usage() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] --app-id=... --installation-id=... --keyfile=... \n\nOptions: \n", path.Base(os.Args[0]))
flag.PrintDefaults()
fmt.Println()
}
func main() {
flag.Usage = usage
flag.Parse()
if *appID == "" || *installationID == "" || *keyFile == "" {
usage()
os.Exit(1)
}
gh, err := newGitHubAppClient(*appID, *installationID, *keyFile)
if err != nil {
log.Fatal(err)
}
var params AccessTokenParams
if *repo != "" {
params.InstallationID = *installationID
params.Repositories = []string{*repo}
params.Permissions = map[string]string{
"contents": "read",
}
}
accessToken, err := gh.AccessToken(params)
if err != nil {
log.Fatal(err)
}
log.Printf("Access Token: %s", accessToken)
}
type gitHubAppClient struct {
privateKey *rsa.PrivateKey
appID string
installationID string
client *http.Client
}
func newGitHubAppClient(appID, installationID, pkPemFile string) (*gitHubAppClient, error) {
keyBytes, err := ioutil.ReadFile(pkPemFile)
if err != nil {
return nil, err
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
if err != nil {
return nil, err
}
return &gitHubAppClient{
appID: appID,
installationID: installationID,
privateKey: key,
client: &http.Client{
Timeout: time.Second * 2,
},
}, nil
}
func (gh *gitHubAppClient) signJWTToken() (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
IssuedAt: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: time.Now().Add(5 * time.Minute).Unix(),
Issuer: gh.appID,
})
return token.SignedString(gh.privateKey)
}
const (
gitHubApplicationContentType = `application/vnd.github.v3+json`
)
type errorResponse struct {
Message string `json:"message"`
DocumentationURL string `json:"documentation_url"`
}
func (e errorResponse) Error() string {
return e.Message
}
type AccessTokenParams struct {
InstallationID string `json:"installation_id,omitempty"`
Repositories []string `json:"repositories,omitempty"`
Permissions map[string]string `json:"permissions,omitempty"`
}
func (gh *gitHubAppClient) AccessToken(params AccessTokenParams) (string, error) {
var requestBody bytes.Buffer
if err := json.NewEncoder(&requestBody).Encode(params); err != nil {
return "", err
}
u := fmt.Sprintf(`https://api.github.com/app/installations/%s/access_tokens`, gh.installationID)
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(requestBody.Bytes()))
if err != nil {
return "", err
}
token, err := gh.signJWTToken()
if err != nil {
return "", err
}
req.Header.Set(`Authorization`, fmt.Sprintf("Bearer %s", token))
req.Header.Set(`Accept`, gitHubApplicationContentType)
// dump, _ := httputil.DumpRequest(req, true)
// log.Printf("%s", dump)
resp, err := gh.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// dumpResp, _ := httputil.DumpResponse(resp, true)
// log.Printf("%s", dumpResp)
// handle error responses which have json errors in them
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
var errResp errorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return "", err
}
return "", errResp
}
var accessTokenResponse struct {
Token string `json:"token"`
}
if err := json.NewDecoder(resp.Body).Decode(&accessTokenResponse); err != nil {
return "", err
}
return accessTokenResponse.Token, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment