-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement abstraction for token authentication (#12)
* 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
1 parent
300eada
commit 8971919
Showing
7 changed files
with
354 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,3 +23,6 @@ jobs: | |
|
||
- name: Build the SDK | ||
run: go build ./... | ||
|
||
- name: Run unit tests | ||
run: go test ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.