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 18 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
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,48 @@ An "alpha" version of a generated Go SDK from [GitHub's OpenAPI spec](https://gi

## How do I use it?

See example client instantiations and requests in [example_test.go](pkg/example_test.go).
See example client instantiations and requests in [example_test.go](pkg/example_test.go) or in the [cmd/ directory](cmd/).

⚠️ **Note**: This SDK is not yet stable. Breaking changes may occur at any time.

### Authentication

Currently, this SDK supports both [Personal Access Tokens (classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic) and [fine-grained Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens).
This SDK supports [Personal Access Tokens (classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic), [fine-grained Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens), and [GitHub Apps](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) authentication.

Future work is planned for the SDK to support [GitHub Apps](https://docs.github.com/en/apps/overview) authentication as well.
In order to use either type of Personal Access token, you can use the `WithTokenAuthentication("YOUR_TOKEN_HERE")` functional option when constructing a client, like so:

```go
client, err := pkg.NewApiClient(
pkg.WithTokenAuthentication(os.Getenv("GITHUB_TOKEN")),
)
if err != nil {
log.Fatalf("error creating client: %v", err)
}
```

In order to authenticate as a GitHub App, you can use the `WithGitHubAppAuthentication` functional option:

```go
client, err := pkg.NewApiClient(
pkg.WithGitHubAppAuthentication("/path/to/your/pem/file.pem", "your-client-ID", yourInstallationIDInt),
)
if err != nil {
log.Fatalf("error creating client: %v", err)
}
```

To see more detailed examples, view [the cmd/ directory in this repo](cmd/).

⚠️ **Note**: There are [three types](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) of GitHub App authentication:
1. [As the App itself (meta endpoints)](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28)
1. [As an App installation](https://docs.github.com/en/rest/authentication/endpoints-available-for-github-app-installation-access-tokens?apiVersion=2022-11-28)
1. On behalf of a user

Authenticating on behalf of a user is not supported in an SDK, as it requires a UI authentication flow with redirects. This SDK supports authenticating as the App itself and as an App installation.

Note that the SDK **does not yet** support authenticating as the App itself and as an App installation using the same client transparently to the user. Authenticating as the App itself requires [creating a JSON Web Token (JWT)](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app) and using that as token authentication. For helpers to create and sign a JWT in Go, you may use the [golang-jwt/jwt](https://github.com/golang-jwt/jwt) library.

Authenticating as an App installation can be done using the `WithGitHubAppAuthentication` functional option. Future work is planned to make the App meta endpoints vs. App installation endpoints auth schemes transparent to the user and only require one client setup.

## Why a generated SDK?

Expand Down
49 changes: 49 additions & 0 deletions cmd/app-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"context"
"log"
"os"
"strconv"
"time"

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

func main() {
installationID, err := strconv.ParseInt(os.Getenv("INSTALLATION_ID"), 10, 64)
if err != nil {
log.Fatalf("error parsing installation ID from string to int64: %v", err)
}

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.WithGitHubAppAuthentication(os.Getenv("PATH_TO_PEM_FILE"), os.Getenv("CLIENT_ID"), installationID),
)

// 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())
}
}
}
7 changes: 3 additions & 4 deletions cmd/example/main.go → cmd/token-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func main() {
pkg.WithUserAgent("my-user-agent"),
pkg.WithRequestTimeout(5*time.Second),
pkg.WithBaseUrl("https://api.github.com"),
pkg.WithAuthorizationToken(os.Getenv("GITHUB_TOKEN")),
pkg.WithTokenAuthentication(os.Getenv("GITHUB_TOKEN")),
)

// equally valid:
Expand All @@ -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)
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ module github.com/octokit/go-sdk
go 1.21.5

require (
github.com/kfcampbell/ghinstallation v0.0.6
github.com/microsoft/kiota-abstractions-go v1.6.0
github.com/microsoft/kiota-http-go v1.4.1
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/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/golang-jwt/jwt/v5 v5.2.1 // 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
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ 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/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kfcampbell/ghinstallation v0.0.6 h1:L4QkjRqNosJ6Kyetymq7FswY1wUxMQO+fyYXJAWl0WY=
github.com/kfcampbell/ghinstallation v0.0.6/go.mod h1:UXWfCKaLwF+AiyCo8gxE5oA0VMQsAmCdRXgTyyRdUnA=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand Down Expand Up @@ -39,6 +43,8 @@ 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=
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())
}
}
Loading
Loading