-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
352 additions
and
0 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 |
---|---|---|
@@ -0,0 +1,17 @@ | ||
GOCMD=go | ||
TEST?=$$(go list ./... |grep -v 'vendor') | ||
|
||
default: clean build test | ||
|
||
all: default | ||
|
||
test: | ||
${GOCMD} test -v | ||
|
||
build: | ||
${GOCMD} build | ||
|
||
clean: | ||
${GOCMD} clean | ||
|
||
.PHONY: test build clean |
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,44 @@ | ||
Equinix oAuth2 Go client | ||
================ | ||
Go implementation of oAuth2 enabbled HTTP client for interactions with Equinix APIs. | ||
Module implementes Equinix specific client credentials grant type with custom `TokenSource` from standard Go oauth2 module. | ||
|
||
* Contact us : https://developer.equinix.com/contact-us | ||
|
||
Requirements | ||
---------------- | ||
* [Go](https://golang.org/doc/install) 1.14+ (to build provider plugin) | ||
|
||
Usage | ||
---------------- | ||
1. Import | ||
``` | ||
import "github.com/equinix/oauth2-go" | ||
``` | ||
2. Prepare configuration and get http client | ||
``` | ||
authConfig := oauth2.Config{ | ||
ClientID: "myClientId", | ||
ClientSecret: "myClientSecret" | ||
BaseURL: "https://api.equinix.com"} | ||
//*http.Client is returned | ||
hc := authConfig.New(context.Background()) | ||
``` | ||
3. Use client | ||
`*http.Client` created by oAuth2 library will deal with token acquisition, refreshment and population of Authorization headers in subsequent requests. | ||
Below example shows how to use oAuth2 client with [Resty REST client library](https://github.com/go-resty/resty) | ||
``` | ||
rc := resty.NewWithClient(hc) | ||
resp, err := rc.R().Get("https://api.equinix.com/ecx/v3/port/userport") | ||
if err != nil { | ||
fmt.Println("Error:", err) | ||
} else { | ||
fmt.Println("Status Code:", resp.StatusCode()) | ||
fmt.Println("Body:\n", resp) | ||
} | ||
``` |
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,10 @@ | ||
module oauth2-go | ||
|
||
go 1.14 | ||
|
||
require ( | ||
github.com/go-resty/resty/v2 v2.3.0 | ||
github.com/jarcoal/httpmock v1.0.5 | ||
github.com/stretchr/testify v1.6.0 | ||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d | ||
) |
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,32 @@ | ||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= | ||
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= | ||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= | ||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
github.com/jarcoal/httpmock v1.0.5 h1:cHtVEcTxRSX4J0je7mWPfc9BpDpqzXSJ5HbymZmyHck= | ||
github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= | ||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= | ||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | ||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= | ||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= | ||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= | ||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
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,130 @@ | ||
//Package oauth2 provides support for making oAuth2 authorized and authenticated HTTP requests | ||
//for interactions with Equinix APIs, in particular Equinix specific client credencials grant type | ||
package oauth2 | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"strconv" | ||
"time" | ||
|
||
"github.com/go-resty/resty/v2" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
const ( | ||
tokenPath = "/oauth2/v1/token" | ||
defTokenTimeout = 3600 | ||
) | ||
|
||
//Config describes oauth2 client credentials flow | ||
type Config struct { | ||
// ClientID is the application's ID. | ||
ClientID string | ||
// ClientSecret is the application's secret. | ||
ClientSecret string | ||
// BaseURL is the base endpoint of a server that token endpoint | ||
BaseURL string | ||
} | ||
|
||
//Error describes oauth2 err | ||
type Error struct { | ||
Code string | ||
Message string | ||
} | ||
|
||
func (e Error) Error() string { | ||
return fmt.Sprintf("oauth2: error when acquiring token: code: %v, message %v", e.Code, e.Message) | ||
} | ||
|
||
//New creates *http.Client with Equinix oAuth2 tokensource. | ||
//The returned client is not valid beyond the lifetime of the context. | ||
func (c *Config) New(ctx context.Context) *http.Client { | ||
return c.NewWithClient(ctx, nil) | ||
} | ||
|
||
//NewWithClient creates *http.Client with Equinix oAuth2 tokensource and custom *http.Client. | ||
//The returned client is not valid beyond the lifetime of the context. | ||
func (c *Config) NewWithClient(ctx context.Context, hc *http.Client) *http.Client { | ||
return oauth2.NewClient(ctx, c.TokenSource(ctx, hc)) | ||
} | ||
|
||
//TokenSource returns a TokenSource that returns t until t expires, | ||
//automatically refreshing it as necessary using the provided context and the | ||
//client ID and client secret. | ||
func (c *Config) TokenSource(ctx context.Context, hc *http.Client) oauth2.TokenSource { | ||
var restClient *resty.Client | ||
if hc == nil { | ||
restClient = resty.New() | ||
} else { | ||
restClient = resty.NewWithClient(hc) | ||
} | ||
source := &tokenSource{ | ||
ctx, | ||
c, | ||
restClient} | ||
return oauth2.ReuseTokenSource(nil, source) | ||
} | ||
|
||
type tokenSource struct { | ||
ctx context.Context | ||
conf *Config | ||
*resty.Client | ||
} | ||
|
||
type tokenRequest struct { | ||
GrantType string `json:"grant_type"` | ||
ClientID string `json:"client_id"` | ||
ClientSecret string `json:"client_secret"` | ||
} | ||
|
||
type tokenResponse struct { | ||
AccessToken string `json:"access_token"` | ||
TokenType string `json:"token_type"` | ||
TokenTimeout string `json:"token_timeout"` | ||
RefreshToken string `json:"refresh_token"` | ||
} | ||
|
||
type tokenError struct { | ||
ErrorCode string `json:"errorCode"` | ||
ErrorMessage string `json:"errorMessage"` | ||
} | ||
|
||
func (c *tokenSource) Token() (*oauth2.Token, error) { | ||
req := tokenRequest{"client_credentials", c.conf.ClientID, c.conf.ClientSecret} | ||
result := &tokenResponse{} | ||
resp, err := c.R(). | ||
SetContext(c.ctx). | ||
SetHeader("Accept", "application/json"). | ||
SetHeader("Content-Type", "application/json"). | ||
SetHeader("User-agent", "equinix/oauth2-go"). | ||
SetBody(&req). | ||
SetResult(result). | ||
SetError(&tokenError{}). | ||
Post(c.conf.BaseURL + tokenPath) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("oauth2: failed to fetch token: %s", err) | ||
} | ||
if resp.IsError() { | ||
respError := resp.Error().(*tokenError) | ||
return nil, Error{Code: respError.ErrorCode, Message: respError.ErrorMessage} | ||
} | ||
token := oauth2.Token{ | ||
AccessToken: result.AccessToken, | ||
TokenType: "Bearer", | ||
RefreshToken: result.RefreshToken} | ||
|
||
timeout, err := strconv.Atoi(result.TokenTimeout) | ||
if err != nil { | ||
timeout = defTokenTimeout | ||
} | ||
if timeout != 0 { | ||
token.Expiry = time.Now().Add(time.Duration(timeout) * time.Second) | ||
} | ||
if token.AccessToken == "" { | ||
return nil, fmt.Errorf("oauth2: server response missing access_token") | ||
} | ||
return &token, 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,106 @@ | ||
package oauth2 | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"io/ioutil" | ||
"net/http" | ||
"strconv" | ||
"testing" | ||
"time" | ||
|
||
"github.com/jarcoal/httpmock" | ||
"github.com/stretchr/testify/assert" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
const ( | ||
baseURL = "http://localhost:8888" | ||
) | ||
|
||
func TestTokenFetch(t *testing.T) { | ||
//Given | ||
clientID := "testClientID" | ||
clientSecret := "testClientSecret" | ||
respFile, _ := ioutil.ReadFile("./test-fixtures/token_response.json") | ||
resp := tokenResponse{} | ||
req := tokenRequest{} | ||
if err := json.Unmarshal(respFile, &resp); err != nil { | ||
assert.Fail(t, "Can't unmarshal response file") | ||
} | ||
testHc := &http.Client{} | ||
httpmock.ActivateNonDefault(testHc) | ||
defer httpmock.DeactivateAndReset() | ||
httpmock.RegisterResponder("POST", baseURL+tokenPath, | ||
func(r *http.Request) (*http.Response, error) { | ||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||
return httpmock.NewStringResponse(400, ""), nil | ||
} | ||
resp, _ := httpmock.NewJsonResponse(200, resp) | ||
return resp, nil | ||
}, | ||
) | ||
|
||
//when | ||
testConfig := Config{ | ||
ClientID: clientID, | ||
ClientSecret: clientSecret, | ||
BaseURL: baseURL} | ||
token, err := testConfig.TokenSource(context.Background(), testHc).Token() | ||
|
||
//then | ||
assert.Nil(t, err, "TokenSource should not return an error") | ||
assert.NotNil(t, token, "TokenSource should return a token") | ||
verifyTokenRequest(t, req, clientID, clientSecret) | ||
verifyToken(t, *token, resp) | ||
} | ||
|
||
func TestError(t *testing.T) { | ||
respFile, _ := ioutil.ReadFile("./test-fixtures/token_response_err.json") | ||
resp := tokenError{} | ||
if err := json.Unmarshal(respFile, &resp); err != nil { | ||
assert.Fail(t, "Can't unmarshal response file") | ||
} | ||
testHc := &http.Client{} | ||
httpmock.ActivateNonDefault(testHc) | ||
defer httpmock.DeactivateAndReset() | ||
httpmock.RegisterResponder("POST", baseURL+tokenPath, | ||
func(r *http.Request) (*http.Response, error) { | ||
resp, _ := httpmock.NewJsonResponse(500, resp) | ||
return resp, nil | ||
}, | ||
) | ||
|
||
//when | ||
testConfig := Config{ | ||
ClientID: "clientID", | ||
ClientSecret: "clientSecret", | ||
BaseURL: baseURL} | ||
_, err := testConfig.TokenSource(context.Background(), testHc).Token() | ||
|
||
//then | ||
assert.NotNil(t, err, "TokenSource should return an error") | ||
assert.IsType(t, Error{}, err, "Returned error has proper type") | ||
verifyErrorResponse(t, err.(Error), resp) | ||
} | ||
|
||
func verifyToken(t *testing.T, token oauth2.Token, resp tokenResponse) { | ||
assert.Equal(t, resp.AccessToken, token.AccessToken, "AccessToken matches") | ||
assert.Equal(t, resp.RefreshToken, token.RefreshToken, "RefreshToken matches") | ||
assert.Equal(t, resp.TokenType, token.TokenType, "TokenType matches") | ||
|
||
respTimeout, err := strconv.Atoi(resp.TokenTimeout) | ||
assert.Nil(t, err, "Error when converting TokenTimeout from the response to int: %v", err) | ||
assert.WithinDuration(t, time.Now().Add(time.Duration(respTimeout)*time.Second), token.Expiry, time.Duration(1)*time.Second, "Token expiry reflects token_timeout from the response") | ||
} | ||
|
||
func verifyTokenRequest(t *testing.T, req tokenRequest, clientID string, clientSecret string) { | ||
assert.Equal(t, "client_credentials", req.GrantType, "GrantType matches") | ||
assert.Equal(t, clientID, req.ClientID, "ClientID matches") | ||
assert.Equal(t, clientSecret, req.ClientSecret, "ClientSecret matches") | ||
} | ||
|
||
func verifyErrorResponse(t *testing.T, err Error, resp tokenError) { | ||
assert.Equal(t, resp.ErrorCode, err.Code, "Error code matches") | ||
assert.Equal(t, resp.ErrorMessage, err.Message, "Error message matches") | ||
} |
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,6 @@ | ||
{ | ||
"access_token": "qwErtY8zyW1abcdefGHI", | ||
"refresh_token": "aBiLmo8zsMX345MSaHHI", | ||
"token_timeout": "3600", | ||
"token_type": "Bearer" | ||
} |
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,7 @@ | ||
{ | ||
"errorDomain": "apps-fqa", | ||
"errorTitle": "Login Failure", | ||
"errorCode": "S1006", | ||
"developerMessage": "Login Failure, please try again", | ||
"errorMessage": "Login Failure, please try again." | ||
} |