Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support GitHub Apps Installation authentication #69

Merged
merged 20 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
43 changes: 43 additions & 0 deletions cmd/app-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"context"
"log"
"time"

abs "github.com/microsoft/kiota-abstractions-go"
"github.com/octokit/go-sdk/pkg"
"github.com/octokit/go-sdk/pkg/github/installation"
)

func main() {
client, err := pkg.NewApiClient(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fluent syntax here makes me want to consider how we might be able to propagate that to other SDKs - it feels really good.

pkg.WithUserAgent("my-user-agent"),
pkg.WithRequestTimeout(5*time.Second),
pkg.WithBaseUrl("https://api.github.com"),
// pkg.WithAuthorizationToken(os.Getenv("GITHUB_TOKEN")),
pkg.WithGitHubAppAuthentication("/home/kfcampbell/github/dev/go-sdk/kfcampbell-terraform-provider.2024-04-30.private-key.pem", 131977, 20570954),
)

// equally valid:
//client, err := pkg.NewApiClient()
if err != nil {
log.Fatalf("error creating client: %v", err)
}

queryParams := &installation.RepositoriesRequestBuilderGetQueryParameters{}
requestConfig := &abs.RequestConfiguration[installation.RepositoriesRequestBuilderGetQueryParameters]{
QueryParameters: queryParams,
}
repos, err := client.Installation().Repositories().Get(context.Background(), requestConfig)
if err != nil {
log.Fatalf("error getting repositories: %v", err)
}

if len(repos.GetRepositories()) > 0 {
log.Printf("Repositories:\n")
for _, repo := range repos.GetRepositories() {
log.Printf("%v\n", *repo.GetFullName())
}
}
}
5 changes: 2 additions & 3 deletions cmd/example/main.go → cmd/token-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ func main() {
}
zen, err := client.Zen().Get(context.Background(), requestConfig)
if err != nil {
fmt.Printf("error getting Zen: %v\n", err)
return
log.Fatalf("error getting repositories: %v", err)
}
fmt.Printf("%v\n", *zen)
fmt.Printf("GitHub Zen principle: %v\n", *zen)
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@ module github.com/octokit/go-sdk
go 1.21.5

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0
github.com/microsoft/kiota-abstractions-go v1.6.0
github.com/microsoft/kiota-http-go v1.3.3
github.com/microsoft/kiota-serialization-form-go v1.0.0
github.com/microsoft/kiota-serialization-json-go v1.0.7
github.com/microsoft/kiota-serialization-multipart-go v1.0.0
github.com/microsoft/kiota-serialization-text-go v1.0.0
golang.org/x/sync v0.7.0
)

require (
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/google/go-github/v60 v60.0.0 // indirect
)

require (
github.com/cjlapao/common-go v0.0.39 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/std-uritemplate/std-uritemplate/go v0.0.55 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 h1:XWuWBRFEpqVrHepQob9yPS3Xg4K3Wr9QCx4fu8HbUNg=
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0/go.mod h1:qoGA4DxWPaYTgVCrmEspVSjlTu4WYAiSxMIhorMRXXc=
github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA=
github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand All @@ -7,8 +9,15 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
Expand Down Expand Up @@ -39,6 +48,9 @@ go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGX
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
4 changes: 2 additions & 2 deletions pkg/authentication/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ type Request struct {
*abs.RequestInformation
}

// WithAuthorization sets the Authorization header to the given token,
// WithTokenAuthentication sets the Authorization header to the given token,
// prepended by the AuthType
func (r *Request) WithAuthorization(token string) {
func (r *Request) WithTokenAuthentication(token string) {
if r.Headers.ContainsKey(headers.AuthorizationKey) {
r.Headers.Remove(headers.AuthorizationKey)
}
Expand Down
14 changes: 9 additions & 5 deletions pkg/authentication/token_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ import (
abs "github.com/microsoft/kiota-abstractions-go"
)

// TokenProvider may use a token to authenticate each request. It also can be
// used to configure UserAgent strings, API Versions, and other request configuration.
// Note that GitHub App authentication is set at the client transport level. See the
// docs for pkg.NewApiClient for more.
type TokenProvider struct {
options []TokenProviderOption
}

// TokenProviderOption provides a functional option
// for configuring a TokenProvider.
type TokenProviderOption func(*TokenProvider, *Request)

// WithAuthorizationToken sets the AuthorizationToken for each request to the given token.
func WithAuthorizationToken(token string) TokenProviderOption {
// WithTokenAuthentication sets the AuthorizationToken for each request to the given token.
func WithTokenAuthentication(token string) TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithAuthorization(token)
r.WithTokenAuthentication(token)
}
}

Expand Down Expand Up @@ -55,7 +61,6 @@ func NewTokenProvider(options ...TokenProviderOption) *TokenProvider {
provider := &TokenProvider{
options: options,
}

return provider
}

Expand All @@ -80,6 +85,5 @@ func (t *TokenProvider) AuthenticateRequest(context context.Context, request *ab
for _, option := range t.options {
option(t, reqWrapper)
}

return nil
}
18 changes: 8 additions & 10 deletions pkg/authentication/token_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

func TestTokenIsSetInAuthenticatedRequest(t *testing.T) {
token := "help i'm trapped in a Go binary"
provider := authentication.NewTokenProvider(authentication.WithAuthorizationToken(token))
provider := authentication.NewTokenProvider(authentication.WithTokenAuthentication(token))

reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})
Expand All @@ -41,7 +41,7 @@ func TestTokenIsSetInAuthenticatedRequest(t *testing.T) {
// TODO(kfcampbell): this code could be refactored to use table-based tests
func TestDefaultRequestOptions(t *testing.T) {
token := "this is not the token you're looking for"
provider := authentication.NewTokenProvider(authentication.WithAuthorizationToken(token))
provider := authentication.NewTokenProvider(authentication.WithTokenAuthentication(token))
reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

Expand Down Expand Up @@ -74,7 +74,7 @@ func TestOverwritingDefaultRequestOptions(t *testing.T) {
apiVersion := "i'm totally a real API version"
userAgent := "i'm totally a real user agent"
provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(token),
authentication.WithTokenAuthentication(token),
authentication.WithAPIVersion(apiVersion),
authentication.WithUserAgent(userAgent))

Expand Down Expand Up @@ -125,7 +125,7 @@ func TestAnonymousAuthIsAllowed(t *testing.T) {
func TestTokenSetInRequestIsNotOverwritten(t *testing.T) {
providerToken := "dit dit dit / dat dat dat / dit dit dit"
provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(providerToken),
authentication.WithTokenAuthentication(providerToken),
)

requestToken := "dit dit dit dit / dit / dit dat dit dit / dit dat dat dit"
Expand Down Expand Up @@ -156,7 +156,7 @@ func TestHappyPathIntegration(t *testing.T) {
}

provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(token),
authentication.WithTokenAuthentication(token),
)

adapter, err := http.NewNetHttpRequestAdapter(provider)
Expand All @@ -170,17 +170,15 @@ func TestHappyPathIntegration(t *testing.T) {

// Create a new instance of abstractions.RequestConfiguration
requestConfig := &abstractions.RequestConfiguration[user.EmailsRequestBuilderGetQueryParameters]{
Headers: headers,
Headers: headers,
}



userEmails, err := client.User().Emails().Get(context.Background(), requestConfig)
if err != nil {
log.Fatalf("%v\n", err)
log.Fatalf("%v\n", err)
}

for _, v := range userEmails {
fmt.Printf("%v\n", *v.GetEmail())
fmt.Printf("%v\n", *v.GetEmail())
}
}
48 changes: 44 additions & 4 deletions pkg/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package pkg

import (
"fmt"
"net/http"
"time"

"github.com/bradleyfalzon/ghinstallation/v2"
kiotaHttp "github.com/microsoft/kiota-http-go"
auth "github.com/octokit/go-sdk/pkg/authentication"
"github.com/octokit/go-sdk/pkg/github"
Expand All @@ -22,17 +24,38 @@ func NewApiClient(optionFuncs ...ClientOptionFunc) (*Client, error) {
rateLimitHandler := handlers.NewRateLimitHandler()
middlewares := options.Middleware
middlewares = append(middlewares, rateLimitHandler)
netHttpClient := kiotaHttp.GetDefaultClient(middlewares...)
defaultTransport := kiotaHttp.GetDefaultTransport()
netHttpClient := &http.Client{
Transport: defaultTransport,
}

if options.RequestTimeout != 0 {
netHttpClient.Timeout = options.RequestTimeout
}

// Configure GitHub App authentication if required fields are provided
if options.GitHubAppID != 0 && options.GitHubAppInstallationID != 0 && options.GitHubAppPemFilePath != "" {
existingTransport := netHttpClient.Transport
appTransport, err := ghinstallation.NewKeyFromFile(existingTransport, options.GitHubAppID, options.GitHubAppInstallationID, options.GitHubAppPemFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create transport from GitHub App: %v", err)
}
netHttpClient.Transport = appTransport
kfcampbell marked this conversation as resolved.
Show resolved Hide resolved
}

// Middleware must be applied after App transport is set, otherwise App token will fail to be
// renewed with a 400 Bad Request error (even though the request is identical to a successful one.)
finalTransport := kiotaHttp.NewCustomTransportWithParentTransport(netHttpClient.Transport, middlewares...)
netHttpClient.Transport = finalTransport

tokenProviderOptions := []auth.TokenProviderOption{
auth.WithAPIVersion(options.APIVersion),
auth.WithUserAgent(options.UserAgent),
}
if options.Token != "" {
tokenProviderOptions = append(tokenProviderOptions, auth.WithAuthorizationToken(options.Token))

// If a PAT is provided and GitHub App information is not, configure token authentication
if options.Token != "" && (options.GitHubAppID == 0 && options.GitHubAppInstallationID == 0 && options.GitHubAppPemFilePath == "") {
tokenProviderOptions = append(tokenProviderOptions, auth.WithTokenAuthentication(options.Token))
}

tokenProvider := auth.NewTokenProvider(tokenProviderOptions...)
Expand Down Expand Up @@ -70,7 +93,16 @@ type ClientOptions struct {
RequestTimeout time.Duration
Middleware []kiotaHttp.Middleware
BaseURL string
Token string

// Token should be left blank if GitHub App auth or an unauthenticated client is desired.
Token string

// GitHubAppPemFilePath should be left blank if token auth or an unauthenticated client is desired.
GitHubAppPemFilePath string
// GitHubAppID should be left blank if token auth or an unauthenticated client is desired.
GitHubAppID int64
// GitHubAppInstallationID should be left blank if token auth or an unauthenticated client is desired.
GitHubAppInstallationID int64
}

// GetDefaultClientOptions returns a new instance of ClientOptions with default values.
Expand Down Expand Up @@ -121,3 +153,11 @@ func WithAPIVersion(version string) ClientOptionFunc {
c.APIVersion = version
}
}

func WithGitHubAppAuthentication(GitHubAppPemFilePath string, GitHubAppID int64, GitHubAppInstallationID int64) ClientOptionFunc {
return func(c *ClientOptions) {
c.GitHubAppPemFilePath = GitHubAppPemFilePath
c.GitHubAppID = GitHubAppID
c.GitHubAppInstallationID = GitHubAppInstallationID
}
}
Loading