Skip to content

Commit

Permalink
Implement abstraction for token authentication (#12)
Browse files Browse the repository at this point in the history
* Initial commit of GitHub token provider

* Initial test outline

* Use Authorization: token {token} to auth

* Complete integration tests

* Ensure tests are run in CI build

* Add constant for AuthType

* Test case for when token is provided with request

* Test case error when blank token is given

* Initial pass of API version and user agent header implementation

* Standardize on token rather than bearer auth

* Add empty line at end of CI build

* Introduce TokenProviderOption pattern

* Comment updates

* Apply user options after defaults

* WithAPIVersion handler and default handlers implemented

* Change provider instantiation signature and add new test cases

* NewTokenProvider should not return an error

* Docs comment changes

* Move header constants to their own package
  • Loading branch information
kfcampbell committed Nov 30, 2023
1 parent 300eada commit 8971919
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 20 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ jobs:

- name: Build the SDK
run: go build ./...

- name: Run unit tests
run: go test ./...
49 changes: 49 additions & 0 deletions github/authentication/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package authentication

import (
"fmt"

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

// Request provides a wrapper around Kiota's abs.RequestInformation type
type Request struct {
*abs.RequestInformation
}

// WithAuthorization sets the Authorization header to the given token,
// prepended by the AuthType
func (r *Request) WithAuthorization(token string) {
if r.Headers.ContainsKey(headers.AuthorizationKey) {
r.Headers.Remove(headers.AuthorizationKey)
}
r.Headers.Add(headers.AuthorizationKey, fmt.Sprintf("%v %v", headers.AuthType, token))
}

// WithUserAgent allows the caller to set the User-Agent string for each request
func (r *Request) WithUserAgent(userAgent string) {
if r.Headers.ContainsKey(headers.UserAgentKey) {
r.Headers.Remove(headers.UserAgentKey)
}
r.Headers.Add(headers.UserAgentKey, userAgent)
}

// WithDefaultUserAgent sets the default User-Agent string for each request
func (r *Request) WithDefaultUserAgent() {
r.WithUserAgent(headers.UserAgentValue)
}

// WithAPIVersion sets the API version header for each request
func (r *Request) WithAPIVersion(version string) {
if r.Headers.ContainsKey(headers.APIVersionKey) {
r.Headers.Remove(headers.APIVersionKey)
}
r.Headers.Add(headers.APIVersionKey, version)
}

// WithDefaultAPIVersion sets the API version header to the default (the version used
// to generate the code) for each request
func (r *Request) WithDefaultAPIVersion() {
r.WithAPIVersion(headers.APIVersionValue)
}
85 changes: 85 additions & 0 deletions github/authentication/token_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package authentication

import (
"context"

abs "github.com/microsoft/kiota-abstractions-go"
)

type TokenProvider struct {
options []TokenProviderOption
}

type TokenProviderOption func(*TokenProvider, *Request)

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

// WithDefaultUserAgent sets the User-Agent string sent for requests to the default
// for this SDK.
func WithDefaultUserAgent() TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithDefaultUserAgent()
}
}

// WithUserAgent sets the User-Agent string sent with each request.
func WithUserAgent(userAgent string) TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithUserAgent(userAgent)
}
}

// WithDefaultAPIVersion sets the API version header sent with each request.
func WithDefaultAPIVersion() TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithDefaultAPIVersion()
}
}

// WithAPIVersion sets the API version header sent with each request.
func WithAPIVersion(version string) TokenProviderOption {
return func(t *TokenProvider, r *Request) {
r.WithAPIVersion(version)
}
}

// TODO(kfcampbell): implement new constructor with allowedHosts

// NewTokenProvider creates an instance of TokenProvider with the specified token and options.
func NewTokenProvider(options ...TokenProviderOption) *TokenProvider {
provider := &TokenProvider{
options: options,
}

return provider
}

// defaultHandlers contains our "sensible defaults" for TokenProvider initialization
var defaultHandlers = []TokenProviderOption{WithDefaultUserAgent(), WithDefaultAPIVersion()}

// AuthenticateRequest applies the default options for each request, then the user's options
// (if present in the TokenProvider). User options are guaranteed to be run in the order they
// were input.
func (t *TokenProvider) AuthenticateRequest(context context.Context, request *abs.RequestInformation, additionalAuthenticationContext map[string]interface{}) error {
reqWrapper := &Request{RequestInformation: request}

if reqWrapper.Headers == nil {
reqWrapper.Headers = abs.NewRequestHeaders()
}

for _, option := range defaultHandlers {
option(t, reqWrapper)
}

// apply user options after defaults
for _, option := range t.options {
option(t, reqWrapper)
}

return nil
}
181 changes: 181 additions & 0 deletions github/authentication/token_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package authentication_test

import (
"context"
"fmt"
"log"
"os"
"strings"
"testing"

abstractions "github.com/microsoft/kiota-abstractions-go"
http "github.com/microsoft/kiota-http-go"
"github.com/octokit/go-sdk/github/authentication"
"github.com/octokit/go-sdk/github/headers"
"github.com/octokit/go-sdk/github/octokit"
"github.com/octokit/go-sdk/github/octokit/user"
)

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

reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("there should be no error when calling AuthenticateRequest")
}

if len(reqInfo.Headers.Get(headers.AuthorizationKey)) != 1 {
t.Errorf("there should be exactly one authorization key")
}

receivedToken := reqInfo.Headers.Get(headers.AuthorizationKey)[0]
if !strings.Contains(receivedToken, token) {
t.Errorf("received token doesn't match up with given token")
}
}

// 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))
reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("there should be no error when calling AuthenticateRequest")
}

apiVersions := reqInfo.Headers.Get(headers.APIVersionKey)
if len(apiVersions) != 1 {
t.Errorf("exactly one API version should be present in the request")
}

if apiVersions[0] != headers.APIVersionValue {
t.Errorf("default API version is set incorrectly")
}

userAgents := reqInfo.Headers.Get(headers.UserAgentKey)
if len(userAgents) != 1 {
t.Errorf("exactly one user agent string should be present in the request")
}

if userAgents[0] != headers.UserAgentValue {
t.Errorf("default user agent string is set incorrectly")
}
}

func TestOverwritingDefaultRequestOptions(t *testing.T) {
token := "i'm totally a real token"
apiVersion := "i'm totally a real API version"
userAgent := "i'm totally a real user agent"
provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(token),
authentication.WithAPIVersion(apiVersion),
authentication.WithUserAgent(userAgent))

reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("should be no error when calling authenticated request")
}

apiVersions := reqInfo.Headers.Get(headers.APIVersionKey)
if len(apiVersions) != 1 {
t.Errorf("exactly one API version should be present in the request")
}

if apiVersions[0] != apiVersion {
t.Errorf("default API version is set incorrectly")
}

userAgents := reqInfo.Headers.Get(headers.UserAgentKey)
if len(userAgents) != 1 {
t.Errorf("exactly one user agent string should be present in the request")
}

if userAgents[0] != userAgent {
t.Errorf("default user agent string is set incorrectly")
}

}

func TestAnonymousAuthIsAllowed(t *testing.T) {
provider := authentication.NewTokenProvider()
reqInfo := abstractions.NewRequestInformation()
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("should be no error when calling authenticated request")
}

authorizations := reqInfo.Headers.Get(headers.AuthorizationKey)
if len(authorizations) != 0 {
t.Errorf("no authorization header should be present in the request")
}
}

func TestTokenSetInRequestIsNotOverwritten(t *testing.T) {
providerToken := "dit dit dit / dat dat dat / dit dit dit"
provider := authentication.NewTokenProvider(
authentication.WithAuthorizationToken(providerToken),
)

requestToken := "dit dit dit dit / dit / dit dat dit dit / dit dat dat dit"
requestHeaders := abstractions.NewRequestHeaders()
requestHeaders.Add(headers.AuthType, requestToken)

reqInfo := abstractions.NewRequestInformation()
reqInfo.Headers = requestHeaders
addtlContext := make(map[string]interface{})

err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext)
if err != nil {
t.Errorf("AuthenticateRequest should not error")
}
reqInfoToken := reqInfo.Headers.Get(headers.AuthorizationKey)[0]

if !strings.Contains(reqInfoToken, providerToken) {
t.Errorf("received token doesn't match up with given token")
}
}

// TODO(kfcampbell): make a more permanent decision about how to structure
// and separately run unit vs. integration tests
func TestHappyPathIntegration(t *testing.T) {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
t.Skip("in order to run integration tests, ensure a valid GITHUB_TOKEN exists in the environment")
}

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

adapter, err := http.NewNetHttpRequestAdapter(provider)
if err != nil {
log.Fatalf("Error creating request adapter: %v", err)
}
headers := abstractions.NewRequestHeaders()
_ = headers.TryAdd("Accept", "application/vnd.github.v3+json")

client := octokit.NewApiClient(adapter)
emailsRequestConfig := &user.EmailsRequestBuilderGetRequestConfiguration{
Headers: headers,
}
userEmails, err := client.User().Emails().Get(context.Background(), emailsRequestConfig)
if err != nil {
log.Fatalf("%v\n", err)
}

for _, v := range userEmails {
fmt.Printf("%v\n", *v.GetEmail())
}
}
13 changes: 13 additions & 0 deletions github/headers/header_contents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package headers

const AuthorizationKey = "Authorization"
const AuthType = "bearer"
const UserAgentKey = "User-Agent"

// TODO(kfcampbell): get the version and binary name from build settings rather than hard-coding
const UserAgentValue = "go-sdk@v0.0.0"

const APIVersionKey = "X-GitHub-Api-Version"

// TODO(kfcampbell): get the version from the generated code somehow
const APIVersionValue = "2022-11-28"
13 changes: 7 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module github.com/octokit/go-sdk
go 1.21.4

require (
github.com/microsoft/kiota-abstractions-go v1.4.0
github.com/microsoft/kiota-abstractions-go v1.5.2
github.com/microsoft/kiota-http-go v1.1.1
github.com/microsoft/kiota-serialization-form-go v1.0.0
github.com/microsoft/kiota-serialization-json-go v1.0.4
github.com/microsoft/kiota-serialization-multipart-go v1.0.0
Expand All @@ -13,14 +14,14 @@ require (
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.2.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/std-uritemplate/std-uritemplate/go v0.0.46 // indirect
github.com/std-uritemplate/std-uritemplate/go v0.0.47 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 8971919

Please sign in to comment.