Skip to content

Commit

Permalink
[ME-2810] Move Client Device Authorization To Go SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianosela committed Apr 15, 2024
1 parent 2e553eb commit 8c2bc71
Show file tree
Hide file tree
Showing 14 changed files with 470 additions and 77 deletions.
59 changes: 37 additions & 22 deletions client/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,31 @@ import (
"os"
"time"

"github.com/borderzero/border0-go/client/reqedit"
"github.com/golang-jwt/jwt"
)

// APIClient is the client for the Border0 API.
type APIClient struct {
http HTTPRequester
timeout time.Duration
authToken string
baseURL string
retryWaitMin time.Duration // minimum time to wait
retryWaitMax time.Duration // maximum time to wait
retryMax int // maximum number of retries
backoff Backoff
http HTTPRequester
timeout time.Duration
authToken string
baseURL string
portalBaseURL string
retryWaitMin time.Duration // minimum time to wait
retryWaitMax time.Duration // maximum time to wait
retryMax int // maximum number of retries
backoff Backoff
}

// verifies APIClient implements all services
var (
_ AuthenticationService = (*APIClient)(nil)
_ ConnectorService = (*APIClient)(nil)
_ PolicyService = (*APIClient)(nil)
_ SocketService = (*APIClient)(nil)
)

// Requester is the interface for the Border0 API client.
type Requester interface {
TokenClaims() (jwt.MapClaims, error)
Expand All @@ -38,27 +48,32 @@ const (

// default config setting values
const (
defaultTimeout = 10 * time.Second // default timeout for requests
defaultBaseURL = "https://api.border0.com/api/v1" // default base URL for Border0 API
defaultRetryWaitMin = 1 * time.Second // default minimum time to wait between retries
defaultRetryWaitMax = 30 * time.Second // default maximum time to wait between retries
defaultRetryMax = 4 // default maximum number of retries
defaultTimeout = 10 * time.Second // default timeout for requests
defaultBaseURL = "https://api.border0.com/api/v1" // default base URL for Border0 API
defaultPortalBaseURL = "https://portal.border0.com" // default base URL for Border0 (Admin) Portal
defaultRetryWaitMin = 1 * time.Second // default minimum time to wait between retries
defaultRetryWaitMax = 30 * time.Second // default maximum time to wait between retries
defaultRetryMax = 4 // default maximum number of retries
)

// New creates a new Border0 API client.
func New(options ...Option) *APIClient {
api := &APIClient{
timeout: defaultTimeout,
authToken: os.Getenv("BORDER0_AUTH_TOKEN"),
baseURL: os.Getenv("BORDER0_BASE_URL"),
retryWaitMin: defaultRetryWaitMin,
retryWaitMax: defaultRetryWaitMax,
retryMax: defaultRetryMax,
backoff: ExponentialBackoff,
timeout: defaultTimeout,
authToken: os.Getenv("BORDER0_AUTH_TOKEN"),
baseURL: os.Getenv("BORDER0_BASE_URL"),
portalBaseURL: os.Getenv("BORDER0_PORTAL_BASE_URL"),
retryWaitMin: defaultRetryWaitMin,
retryWaitMax: defaultRetryWaitMax,
retryMax: defaultRetryMax,
backoff: ExponentialBackoff,
}
if api.baseURL == "" {
api.baseURL = defaultBaseURL
}
if api.portalBaseURL == "" {
api.portalBaseURL = defaultPortalBaseURL
}
for _, option := range options {
option(api)
}
Expand All @@ -81,7 +96,7 @@ func (api *APIClient) TokenClaims() (jwt.MapClaims, error) {
return parsedJWT.Claims.(jwt.MapClaims), nil
}

func (api *APIClient) request(ctx context.Context, method, path string, input, output any) (code int, err error) {
func (api *APIClient) request(ctx context.Context, method, path string, input, output any, edits ...reqedit.EditRequestFunc) (code int, err error) {

shouldRetry := false
retryMax := api.retryMax
Expand All @@ -92,7 +107,7 @@ func (api *APIClient) request(ctx context.Context, method, path string, input, o
for ; ; retryCount++ {

shouldRetry = false
code, err = api.http.Request(ctx, method, api.baseURL+path, input, output)
code, err = api.http.Request(ctx, method, api.baseURL+path, input, output, edits...)
if err != nil {
if code == http.StatusNotFound {
shouldRetry = true
Expand Down
3 changes: 2 additions & 1 deletion client/api_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/borderzero/border0-go/client/mocks"
"github.com/borderzero/border0-go/client/reqedit"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -198,7 +199,7 @@ func Test_APIClient_request(t *testing.T) {
requester.EXPECT().
Request(ctx, testMethod, testURL, testInput, testOutput).
Return(http.StatusInternalServerError, errUnitTest).
Run(func(_ context.Context, _, _ string, _, _ any) {
Run(func(_ context.Context, _, _ string, _, _ any, _ ...reqedit.EditRequestFunc) {
cancel()
}).
Once()
Expand Down
84 changes: 84 additions & 0 deletions client/auth/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package auth

import (
"fmt"

"github.com/borderzero/border0-go/lib/osutil"
)

// Config represents authentication configuration.
type Config struct {
tokenStorageFilePath string // file to write token to
tokenWritingEnabled bool // whether the border0 client should attempt to write tokens to the token storage file path
browserEnabled bool // whether the border0 client should attempt opening the default browser for completing device authorization flow.

legacyAuth bool // whether to use programmatic authentication
email string // DEPRECATED: programmatic authentication email
password string // DEPRECATED: programmatic authentication password
}

// GetTokenStorageFilePath is the getter for the token storage file path.
func (c *Config) GetTokenStorageFilePath() string { return c.tokenStorageFilePath }

// ShouldWriteTokensToDisk is the getter for the token writing disabled boolean.
func (c *Config) ShouldWriteTokensToDisk() bool { return c.tokenWritingEnabled }

// ShouldTryOpeningBrowser is the getter for the disabled browser boolean.
func (c *Config) ShouldTryOpeningBrowser() bool { return c.browserEnabled }

// ShouldUseLegacyAuthentication is the getter for the legacy (programmatic) authentication boolean.
func (c *Config) ShouldUseLegacyAuthentication() bool { return c.legacyAuth }

// GetEmail is the getter for the email.
func (c *Config) GetEmail() string { return c.email }

// GetEmail is the getter for the password.
func (c *Config) GetPassword() string { return c.password }

// Option represents an authentication option
type Option func(*Config)

// GetConfig returns a configuration object populated with the given options.
func GetConfig(opts ...Option) (*Config, error) {
c := &Config{
tokenStorageFilePath: "", // will only try populating if not set after applying opts
tokenWritingEnabled: true,
browserEnabled: true,
legacyAuth: false,
}
for _, opt := range opts {
opt(c)
}

if c.tokenStorageFilePath == "" {
hd, err := osutil.GetUserHomeDir()
if err != nil {
return nil, fmt.Errorf("no token storage filepath provided and failed to get user home directory: %v", err)
}
c.tokenStorageFilePath = fmt.Sprintf("%s/.border0/token", hd)
}

return c, nil
}

// WithTokenStorageFilePath is the authentication option to set the token storage file path.
func WithTokenStorageFilePath(filePath string) Option {
return func(c *Config) { c.tokenStorageFilePath = filePath }
}

// WithTokenWritingDisabled is the authentication option to toggle whether tokens
// acquired through the device authorization flow should be written to disk.
func WithTokenWriting(enabled bool) Option {
return func(c *Config) { c.tokenWritingEnabled = enabled }
}

// WithOpenBrowser is the authentication option to toggle whether the
// browser should be opened for the device authorization flow URL.
func WithOpenBrowser(enabled bool) Option {
return func(c *Config) { c.browserEnabled = enabled }
}

// WithLegacyCredentials is the authentication option to set legacy credentials.
func WithLegacyCredentials(email, password string) Option {
return func(c *Config) { c.legacyAuth = true; c.email = email; c.password = password }
}
Loading

0 comments on commit 8c2bc71

Please sign in to comment.