Skip to content

Commit

Permalink
Add WithTokenSource
Browse files Browse the repository at this point in the history
  • Loading branch information
WillAbides committed Dec 1, 2023
1 parent 062b611 commit 9f81d6e
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 49 deletions.
120 changes: 120 additions & 0 deletions example/appauth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2023 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// appauth demonstrates using the authenticating as a GitHub App and GitHub
// App Installation. To use this example, you must have a GitHub App and access
// to its private key file.

package main

import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v57/github"
)

const defaultAPIURL = "https://api.github.com"

func main() {
ctx := context.Background()

// GITHUB_TOKEN is a personal access token used to get the GitHub App's ID from its slug.
tkn := os.Getenv("GITHUB_TOKEN")
if tkn == "" {
log.Fatal("GITHUB_TOKEN environment variable is required")
}

var keyFile, apiURL, appSlug string
flag.StringVar(&keyFile, "key", "", "path to private key file")
flag.StringVar(&apiURL, "api-url", defaultAPIURL, "GitHub API URL")
flag.StringVar(&appSlug, "app", "", "GitHub App slug")
flag.Parse()

if appSlug == "" {
log.Fatal("-app is required")
}
if keyFile == "" {
log.Fatal("-key is required")
}

err := listAppInstRepos(ctx, apiURL, tkn, appSlug, keyFile)
if err != nil {
log.Fatal(err)
}
}

func listAppInstRepos(ctx context.Context, apiURL, pat, appSlug string, keyfile string) error {
// Create the base client without authentication.
client, err := github.NewClient(nil).WithEnterpriseURLs(apiURL, "")
if err != nil {
return err
}

// Authenticate with the personal access token to get the app from its slug.
client = client.WithAuthToken(pat)

app, _, err := client.Apps.Get(ctx, appSlug)
if err != nil {
return err
}

fmt.Printf("App %s %d:\n", app.GetName(), app.GetID())

// Use ghinstallation to create the auth provider for the app.
appAuth, err := ghinstallation.NewAppsTransportKeyFromFile(http.DefaultTransport, app.GetID(), keyfile)
if err != nil {
return err
}

var pageOpt github.ListOptions
for {
// Authenticate as the app to list installations.
client = client.WithTokenSource(appAuth)

installations, resp, err := client.Apps.ListInstallations(ctx, &pageOpt)
if err != nil {
return err
}

for _, inst := range installations {
fmt.Printf(" Installation %d:\n", inst.GetID())
fmt.Printf(" Repositories:\n")

// Use ghinstallation to create the auth provider for this installation.
instAuth := ghinstallation.NewFromAppsTransport(appAuth, inst.GetID())

// Authenticate as the installation to list repositories.
client = client.WithTokenSource(instAuth)

var instPageOpt github.ListOptions
for {
repos, resp, err := client.Apps.ListRepos(ctx, &instPageOpt)
if err != nil {
return err
}
for _, repo := range repos.Repositories {
fmt.Printf(" %s\n", repo.GetFullName())
}
if resp.NextPage == 0 {
break
}
instPageOpt.Page = resp.NextPage
}
}

if resp.NextPage == 0 {
break
}
pageOpt.Page = resp.NextPage
}

return nil
}
9 changes: 6 additions & 3 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.17

require (
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4
github.com/bradleyfalzon/ghinstallation/v2 v2.8.0
github.com/gofri/go-github-ratelimit v1.0.3
github.com/google/go-github/v57 v57.0.0
golang.org/x/crypto v0.14.0
Expand All @@ -14,9 +14,9 @@ require (

require (
github.com/cloudflare/circl v1.3.3 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v41 v41.0.0 // indirect
github.com/google/go-github/v56 v56.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
Expand All @@ -25,3 +25,6 @@ require (

// Use version at HEAD, not the latest published.
replace github.com/google/go-github/v57 => ../

// TODO: remove this when changes are merged upstream
replace github.com/bradleyfalzon/ghinstallation/v2 => github.com/willabides/ghinstallation/v2 v2.0.0-20231130215721-5b3e4e4ab2c6
17 changes: 7 additions & 10 deletions example/go.sum
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 h1:tXKVfhE7FcSkhkv0UwkLvPDeZ4kz6OXd0PKPlFqf81M=
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/gofri/go-github-ratelimit v1.0.3 h1:Ocs2jaYokZDzgvqaajX+g04dqFyVqL0JQzoO7d2wmlk=
github.com/gofri/go-github-ratelimit v1.0.3/go.mod h1:OnCi5gV+hAG/LMR7llGhU7yHt44se9sYgKPnafoL7RY=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4=
github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/willabides/ghinstallation/v2 v2.0.0-20231130215721-5b3e4e4ab2c6 h1:Vsq8o/1ebi2DKLbOCYtRxRt2+p3zIoAxih003T52PqU=
github.com/willabides/ghinstallation/v2 v2.0.0-20231130215721-5b3e4e4ab2c6/go.mod h1:fmPmvCiBWhJla3zDv9ZTQSZc8AbwyRnGW1yg5ep1Pcs=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
Expand All @@ -41,7 +39,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
37 changes: 12 additions & 25 deletions example/newfilewithappauth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"log"
"net/http"
"os"
"time"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v57/github"
Expand All @@ -27,20 +26,17 @@ func main() {
log.Fatalf("failed to read pem: %v", err)
}

itr, err := ghinstallation.NewAppsTransport(http.DefaultTransport, 10, privatePem)
// create authentication provider for the app
appAuth, err := ghinstallation.NewAppsTransport(http.DefaultTransport, 10, privatePem)
if err != nil {
log.Fatalf("faild to create app transport: %v\n", err)
}
itr.BaseURL = gitHost

//create git client with app transport
client, err := github.NewClient(
&http.Client{
Transport: itr,
Timeout: time.Second * 30,
},
).WithEnterpriseURLs(gitHost, gitHost)
appAuth.BaseURL = gitHost

// create git client with the app auth
client, err := github.NewClient(nil).
WithTokenSource(appAuth).
WithEnterpriseURLs(gitHost, gitHost)
if err != nil {
log.Fatalf("faild to create git client for app: %v\n", err)
}
Expand All @@ -57,22 +53,13 @@ func main() {
installID = val.GetID()
}

token, _, err := client.Apps.CreateInstallationToken(
context.Background(),
installID,
&github.InstallationTokenOptions{})
if err != nil {
log.Fatalf("failed to create installation token: %v\n", err)
}
// create an authentication provider for the installation
instAuth := ghinstallation.NewFromAppsTransport(appAuth, installID)

apiClient, err := github.NewClient(nil).WithAuthToken(
token.GetToken(),
).WithEnterpriseURLs(gitHost, gitHost)
if err != nil {
log.Fatalf("failed to create new git client with token: %v\n", err)
}
// update the client to use the installation auth
client = client.WithTokenSource(instAuth)

_, resp, err := apiClient.Repositories.CreateFile(
_, resp, err := client.Repositories.CreateFile(
context.Background(),
"repoOwner",
"sample-repo",
Expand Down
53 changes: 42 additions & 11 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
headerRateReset = "X-RateLimit-Reset"
headerOTP = "X-GitHub-OTP"
headerRetryAfter = "Retry-After"
headerAuthorization = "Authorization"

headerTokenExpiration = "GitHub-Authentication-Token-Expiration"

Expand Down Expand Up @@ -153,6 +154,20 @@ const (

var errNonNilContext = errors.New("context must be non-nil")

// A TokenSource provides bearer tokens for use in the Authorization header.
// It is used by Client.WithTokenSource to configure the client.
// Token will be called on every request with the request's Context.
type TokenSource interface {
Token(context.Context) (string, error)
}

// staticTokenSource is a simple TokenSource that always returns the same token.
type staticTokenSource string

func (s staticTokenSource) Token(context.Context) (string, error) {
return string(s), nil
}

// A Client manages communication with the GitHub API.
type Client struct {
clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func.
Expand All @@ -169,6 +184,8 @@ type Client struct {
// User agent used when communicating with the GitHub API.
UserAgent string

tokenSource TokenSource
roundTripper http.RoundTripper
rateMu sync.Mutex
rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls.
secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls.
Expand Down Expand Up @@ -321,20 +338,15 @@ func NewClient(httpClient *http.Client) *Client {
}

// WithAuthToken returns a copy of the client configured to use the provided token for the Authorization header.
// When token is empty, the returned client will not set the Authorization header.
func (c *Client) WithAuthToken(token string) *Client {
return c.WithTokenSource(staticTokenSource(token))
}

func (c *Client) WithTokenSource(source TokenSource) *Client {
c2 := c.copy()
defer c2.initialize()
transport := c2.client.Transport
if transport == nil {
transport = http.DefaultTransport
}
c2.client.Transport = roundTripperFunc(
func(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return transport.RoundTrip(req)
},
)
c2.tokenSource = source
return c2
}

Expand Down Expand Up @@ -388,6 +400,7 @@ func (c *Client) initialize() {
if c.client == nil {
c.client = &http.Client{}
}
c.client.Transport = roundTripperFunc(c.roundTrip)
if c.BaseURL == nil {
c.BaseURL, _ = url.Parse(defaultBaseURL)
}
Expand Down Expand Up @@ -447,6 +460,8 @@ func (c *Client) copy() *Client {
BaseURL: c.BaseURL,
UploadURL: c.UploadURL,
secondaryRateLimitReset: c.secondaryRateLimitReset,
tokenSource: c.tokenSource,
roundTripper: c.roundTripper,
}
c.clientMu.Unlock()
if clone.client == nil {
Expand All @@ -458,6 +473,22 @@ func (c *Client) copy() *Client {
return &clone
}

func (c *Client) roundTrip(req *http.Request) (*http.Response, error) {
if c.tokenSource != nil {
token, err := c.tokenSource.Token(req.Context())
if err != nil {
return nil, fmt.Errorf("could not get token: %v", err)
}
req = req.Clone(req.Context())
req.Header.Set(headerAuthorization, "Bearer "+token)
}
roundTripper := c.roundTripper
if roundTripper == nil {
roundTripper = http.DefaultTransport
}
return roundTripper.RoundTrip(req)
}

// NewClientWithEnvProxy enhances NewClient with the HttpProxy env.
func NewClientWithEnvProxy() *Client {
return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}})
Expand Down

0 comments on commit 9f81d6e

Please sign in to comment.