Skip to content

Commit

Permalink
Support GitHub Apps Installation authentication (#69)
Browse files Browse the repository at this point in the history
* Initial commit of github apps poc

* Demo of GitHub Apps auth working in go-sdk

* Remove unnecessary auth configuration functions

* Docs comments cleanup

* Differentiate App and token auth examples

* Switch to using fork of ghinstallation

* Bump to latest ghinstallation fork version

* Allow authorizing with clientID

* Refactor naming to default to clientID for App auth

* Bump kfcampbell/ghinstallation version

* Rename WithAuthorizationToken to WithTokenAuthentication

* Add README note about different types of App auth

* Simplify token initialization client

* Another ghinstallation version bump

* Use env vars for App auth example

* Test coverage for Apps auth

* Update pkg/client.go

Co-authored-by: Nick Floyd <139819+nickfloyd@users.noreply.github.com>

* Fix build error from code suggestion

---------

Co-authored-by: Nick Floyd <139819+nickfloyd@users.noreply.github.com>
  • Loading branch information
kfcampbell and nickfloyd committed Jun 3, 2024
1 parent be07f65 commit 3d777e7
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 32 deletions.
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(
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

0 comments on commit 3d777e7

Please sign in to comment.