From 97f89dd9a1e8dd268993c03151bac7e8e5db00f3 Mon Sep 17 00:00:00 2001 From: Phil Adams Date: Mon, 29 Nov 2021 15:21:35 -0600 Subject: [PATCH] feat(IamAuthenticator): support refresh token flow in IamAuthenticator (#146) This commit updates the IamAuthenticator so that it supports the "refresh token" auth flow. In this scenario, the user constructs an IamAuthenticator with a refresh token (and client id/secret) instead of an apikey. The authenticator will use the "POST /identity/token" operation with grant_type "refresh_token" to obtain an IAM access token. In this commit, I also added a new "builder" for the IamAuthenticator as well, and deprecated the legacy ctor function although it is still supported. --- .secrets.baseline | 70 ++- Authentication.md | 34 +- v5/core/authenticator_factory_test.go | 14 + v5/core/constants.go | 1 + v5/core/iam_authenticator.go | 207 ++++++-- v5/core/iam_authenticator_test.go | 685 +++++++++++++++++++------- v5/resources/my-credentials.env | 7 + 7 files changed, 763 insertions(+), 255 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 3504f92..60ad3dd 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -86,11 +86,19 @@ "type": "Secret Keyword", "verified_result": null }, + { + "hashed_secret": "e0d246cf37df7d1a561ed649d108dd14f36f28bf", + "is_secret": false, + "is_verified": false, + "line_number": 241, + "type": "Secret Keyword", + "verified_result": null + }, { "hashed_secret": "98635b2eaa2379f28cd6d72a38299f286b81b459", "is_secret": false, "is_verified": false, - "line_number": 522, + "line_number": 546, "type": "Secret Keyword", "verified_result": null }, @@ -98,7 +106,7 @@ "hashed_secret": "47fcf185ee7e15fe05cae31fbe9e4ebe4a06a40d", "is_secret": false, "is_verified": false, - "line_number": 571, + "line_number": 595, "type": "Secret Keyword", "verified_result": null } @@ -312,7 +320,7 @@ "hashed_secret": "8318df9ecda039deac9868adf1944a29a95c7114", "is_secret": false, "is_verified": false, - "line_number": 47, + "line_number": 48, "type": "Secret Keyword", "verified_result": null } @@ -476,7 +484,7 @@ "hashed_secret": "7a5d27bcb7a1e98b6e1bfca4df223ed578a47283", "is_secret": false, "is_verified": false, - "line_number": 89, + "line_number": 97, "type": "Secret Keyword", "verified_result": null }, @@ -484,65 +492,73 @@ "hashed_secret": "c2df5d3d760ff42f33fb38e2534d4c1b7ddde3ab", "is_secret": false, "is_verified": false, - "line_number": 89, + "line_number": 97, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f75b33f87ffeacb3a4f793a09693e672e07449ff", + "hashed_secret": "8b142a91cfb6e617618ad437cedf74a6745f8926", "is_secret": false, "is_verified": false, - "line_number": 96, + "line_number": 133, + "type": "Secret Keyword", + "verified_result": null + } + ], + "v5/core/iam_authenticator_test.go": [ + { + "hashed_secret": "fd08cd887ed1de2f2d3e175117ff607ca65187ae", + "is_secret": false, + "is_verified": false, + "line_number": 34, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7ea6be9eecb6605329a1b1870c2fd2af9b896991", + "hashed_secret": "1f7e33de15e22de9d2eaf502df284ed25ca40018", "is_secret": false, "is_verified": false, - "line_number": 99, + "line_number": 37, "type": "Secret Keyword", "verified_result": null - } - ], - "v5/core/iam_authenticator_test.go": [ + }, { "hashed_secret": "c8f0df25bade89c1873f5f01b85bcfb921443ac6", "is_secret": false, "is_verified": false, - "line_number": 33, + "line_number": 41, "type": "JSON Web Token", "verified_result": null }, { - "hashed_secret": "42de4dc186286dbdc2381b3e09a054f96e1995bc", + "hashed_secret": "1f5e25be9b575e9f5d39c82dfd1d9f4d73f1975c", "is_secret": false, "is_verified": false, - "line_number": 589, + "line_number": 194, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1f5e25be9b575e9f5d39c82dfd1d9f4d73f1975c", + "hashed_secret": "ffc168ba60490856fec503b911fab745e277370b", "is_secret": false, "is_verified": false, - "line_number": 734, + "line_number": 209, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "333f0f8814d63e7268f80e1e65e7549137d2350c", + "hashed_secret": "84de897bbaa1dac9c7e13b27ab2afc2a233a5e4e", "is_secret": false, "is_verified": false, - "line_number": 748, + "line_number": 230, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "84ba4ce8a59ed2d6e90726d57cdc4a927d3672b2", + "hashed_secret": "7480f0b7140317bd82ade3c7a9526408304d5a7f", "is_secret": false, "is_verified": false, - "line_number": 751, + "line_number": 504, "type": "Secret Keyword", "verified_result": null }, @@ -550,7 +566,7 @@ "hashed_secret": "32e8612d8ca77c7ea8374aa7918db8e5df9252ed", "is_secret": false, "is_verified": false, - "line_number": 770, + "line_number": 1047, "type": "Secret Keyword", "verified_result": null } @@ -692,11 +708,19 @@ "type": "Secret Keyword", "verified_result": null }, + { + "hashed_secret": "00cafd126182e8a9e7c01bb2f0dfd00496be724f", + "is_secret": false, + "is_verified": false, + "line_number": 85, + "type": "Secret Keyword", + "verified_result": null + }, { "hashed_secret": "9e2659aa7e2b335ec6bdcf180f3b6f41f5191af5", "is_secret": false, "is_verified": false, - "line_number": 83, + "line_number": 90, "type": "Secret Keyword", "verified_result": null } diff --git a/Authentication.md b/Authentication.md index 168de5d..66ec203 100644 --- a/Authentication.md +++ b/Authentication.md @@ -181,9 +181,9 @@ token itself in terms of initial acquisition and refreshing as needed. ## Identity and Access Management Authentication (IAM) -The `IamAuthenticator` will accept a user-supplied api key and will perform +The `IamAuthenticator` will accept a user-supplied apikey or refresh token and will perform the necessary interactions with the IAM token service to obtain a suitable -bearer token for the specified api key. The authenticator will also obtain +bearer token for the specified apikey or refresh token. The authenticator will also obtain a new bearer token when the current token expires. The bearer token is then added to each outbound request in the `Authorization` header in the form: @@ -193,7 +193,13 @@ form: ### Properties -- ApiKey: (required) the IAM api key +- ApiKey: (optional) the IAM apikey to be used to obtain an IAM access token. +One of ApiKey or RefreshToken must be specified. + +- RefreshToken: (optional) a refresh token to be used to obtain an IAM access token. +One of ApiKey or RefreshToken must be specified. If RefreshToken is specified, then +the ClientId and ClientSecret properties must also be specified, using the same values that were +used to obtain the refresh token value. - URL: (optional) The base endpoint URL of the IAM token service. The default value of this property is the "prod" IAM token service endpoint @@ -225,6 +231,21 @@ made to the IAM token service. - Client: (Optional) The `http.Client` object used to invoke token servive requests. If not specified by the user, a suitable default Client will be constructed. +### Usage Notes +- The IamAuthenticator is used to obtain an access token (a bearer token) from the IAM token service. + +- When constructing an IamAuthenticator instance, you must specify exactly one of ApiKey or RefreshToken. + +- If you specify the ApiKey property, the authenticator will use the +IAM token service's `POST /identity/token` operation +with grant_type `urn:ibm:params:oauth:grant-type:apikey` to exchange the apikey value for an access token. + +- If you specify the RefreshToken property, the authenticator will use the +IAM token service's `POST /identity/token` operation +with grant_type `refresh_token` to exchange the refresh token value for an access token. +In this scenario, you must also specify the ClientId and ClientSecret properties, using the same values +that were used when initially obtaining the refresh token value from the IAM token service. + ### Programming example ```go import { @@ -233,8 +254,11 @@ import { } ... // Create the authenticator. -authenticator := &core.IamAuthenticator{ - ApiKey: "myapikey", +authenticator, err := core.NewIamAuthenticatorBuilder(). + SetApiKey("myapikey"). + Build() +if err != nil { + panic(err) } // Create the service options struct. diff --git a/v5/core/authenticator_factory_test.go b/v5/core/authenticator_factory_test.go index 2dfb557..68d9aed 100644 --- a/v5/core/authenticator_factory_test.go +++ b/v5/core/authenticator_factory_test.go @@ -108,6 +108,20 @@ func TestGetAuthenticatorFromEnvironment1(t *testing.T) { assert.Equal(t, "iam-profile1-id", vpcAuth.IAMProfileID) assert.Empty(t, vpcAuth.URL) + // IAM Authenticator using refresh token. + authenticator, err = GetAuthenticatorFromEnvironment("service9") + assert.Nil(t, err) + assert.NotNil(t, authenticator) + assert.Equal(t, AUTHTYPE_IAM, authenticator.AuthenticationType()) + iamAuth, ok := authenticator.(*IamAuthenticator) + assert.True(t, ok) + assert.NotNil(t, iamAuth) + assert.Empty(t, iamAuth.ApiKey) + assert.Equal(t, "refresh-token", iamAuth.RefreshToken) + assert.Equal(t, "user1", iamAuth.ClientId) + assert.Equal(t, "secret1", iamAuth.ClientSecret) + assert.Equal(t, "https://iam.refresh-token.com", iamAuth.URL) + os.Unsetenv("IBM_CREDENTIALS_FILE") } diff --git a/v5/core/constants.go b/v5/core/constants.go index 923d09a..b02274e 100644 --- a/v5/core/constants.go +++ b/v5/core/constants.go @@ -43,6 +43,7 @@ const ( PROPNAME_AUTH_URL = "AUTH_URL" PROPNAME_AUTH_DISABLE_SSL = "AUTH_DISABLE_SSL" PROPNAME_APIKEY = "APIKEY" + PROPNAME_REFRESH_TOKEN = "REFRESH_TOKEN" // #nosec G101 PROPNAME_CLIENT_ID = "CLIENT_ID" PROPNAME_CLIENT_SECRET = "CLIENT_SECRET" PROPNAME_SCOPE = "SCOPE" diff --git a/v5/core/iam_authenticator.go b/v5/core/iam_authenticator.go index b76d199..3bde6e5 100644 --- a/v5/core/iam_authenticator.go +++ b/v5/core/iam_authenticator.go @@ -35,25 +35,28 @@ import ( // type IamAuthenticator struct { - // The apikey used to fetch the bearer token from the IAM token server - // [required]. + // The apikey used to fetch the bearer token from the IAM token server. + // You must specify either ApiKey or RefreshToken. ApiKey string + // The refresh token used to fetch the bearer token from the IAM token server. + // You must specify either ApiKey or RefreshToken. + // If this property is specified, then you also must supply appropriate values + // for the ClientId and ClientSecret properties (i.e. they must be the same + // values that were used to obtain the refresh token). + RefreshToken string + // The URL representing the IAM token server's endpoint; If not specified, // a suitable default value will be used [optional]. URL string // The ClientId and ClientSecret fields are used to form a "basic auth" - // Authorization header for interactions with the IAM token server - - // If neither field is specified, then no Authorization header will be sent - // with token server requests [optional]. These fields are optional, but must - // be specified together. - ClientId string + // Authorization header for interactions with the IAM token server. // If neither field is specified, then no Authorization header will be sent // with token server requests [optional]. These fields are optional, but must // be specified together. + ClientId string ClientSecret string // A flag that indicates whether verification of the server's SSL certificate @@ -85,29 +88,98 @@ var iamNeedsRefreshMutex sync.Mutex const ( // The default (prod) IAM token server base endpoint address. - defaultIamTokenServerEndpoint = "https://iam.cloud.ibm.com" // #nosec G101 - iamGrantTypeApiKey = "urn:ibm:params:oauth:grant-type:apikey" // #nosec G101 + defaultIamTokenServerEndpoint = "https://iam.cloud.ibm.com" // #nosec G101 + iamAuthOperationPathGetToken = "/identity/token" + iamAuthGrantTypeApiKey = "urn:ibm:params:oauth:grant-type:apikey" // #nosec G101 + iamAuthGrantTypeRefreshToken = "refresh_token" // #nosec G101 ) -// NewIamAuthenticator constructs a new IamAuthenticator instance. -func NewIamAuthenticator(apikey string, url string, clientId string, clientSecret string, - disableSSLVerification bool, headers map[string]string) (*IamAuthenticator, error) { - authenticator := &IamAuthenticator{ - ApiKey: apikey, - URL: url, - ClientId: clientId, - ClientSecret: clientSecret, - DisableSSLVerification: disableSSLVerification, - Headers: headers, - } +// IamAuthenticatorBuilder is used to construct an IamAuthenticator instance. +type IamAuthenticatorBuilder struct { + IamAuthenticator +} + +// NewIamAuthenticatorBuilder returns a new builder struct that +// can be used to construct an IamAuthenticator instance. +func NewIamAuthenticatorBuilder() *IamAuthenticatorBuilder { + return &IamAuthenticatorBuilder{} +} + +// SetApiKey sets the ApiKey field in the builder. +func (builder *IamAuthenticatorBuilder) SetApiKey(s string) *IamAuthenticatorBuilder { + builder.IamAuthenticator.ApiKey = s + return builder +} + +// SetRefreshToken sets the RefreshToken field in the builder. +func (builder *IamAuthenticatorBuilder) SetRefreshToken(s string) *IamAuthenticatorBuilder { + builder.IamAuthenticator.RefreshToken = s + return builder +} + +// SetURL sets the URL field in the builder. +func (builder *IamAuthenticatorBuilder) SetURL(s string) *IamAuthenticatorBuilder { + builder.IamAuthenticator.URL = s + return builder +} + +// SetClientIDSecret sets the ClientId and ClientSecret fields in the builder. +func (builder *IamAuthenticatorBuilder) SetClientIDSecret(clientID, clientSecret string) *IamAuthenticatorBuilder { + builder.IamAuthenticator.ClientId = clientID + builder.IamAuthenticator.ClientSecret = clientSecret + return builder +} + +// SetDisableSSLVerification sets the DisableSSLVerification field in the builder. +func (builder *IamAuthenticatorBuilder) SetDisableSSLVerification(b bool) *IamAuthenticatorBuilder { + builder.IamAuthenticator.DisableSSLVerification = b + return builder +} + +// SetScope sets the Scope field in the builder. +func (builder *IamAuthenticatorBuilder) SetScope(s string) *IamAuthenticatorBuilder { + builder.IamAuthenticator.Scope = s + return builder +} + +// SetHeaders sets the Headers field in the builder. +func (builder *IamAuthenticatorBuilder) SetHeaders(headers map[string]string) *IamAuthenticatorBuilder { + builder.IamAuthenticator.Headers = headers + return builder +} + +// SetClient sets the Client field in the builder. +func (builder *IamAuthenticatorBuilder) SetClient(client *http.Client) *IamAuthenticatorBuilder { + builder.IamAuthenticator.Client = client + return builder +} + +// Build() returns a validated instance of the IamAuthenticator with the config that was set in the builder. +func (builder *IamAuthenticatorBuilder) Build() (*IamAuthenticator, error) { // Make sure the config is valid. - err := authenticator.Validate() + err := builder.IamAuthenticator.Validate() if err != nil { return nil, err } - return authenticator, nil + return &builder.IamAuthenticator, nil +} + +// NewIamAuthenticator constructs a new IamAuthenticator instance. +// Deprecated - use the IamAuthenticatorBuilder instead. +func NewIamAuthenticator(apiKey string, url string, clientId string, clientSecret string, + disableSSLVerification bool, headers map[string]string) (*IamAuthenticator, error) { + + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(apiKey). + SetURL(url). + SetClientIDSecret(clientId, clientSecret). + SetDisableSSLVerification(disableSSLVerification). + SetHeaders(headers). + Build() + + return authenticator, err } // newIamAuthenticatorFromMap constructs a new IamAuthenticator instance from a map. @@ -121,12 +193,15 @@ func newIamAuthenticatorFromMap(properties map[string]string) (authenticator *Ia disableSSL = false } - authenticator, err = NewIamAuthenticator(properties[PROPNAME_APIKEY], properties[PROPNAME_AUTH_URL], - properties[PROPNAME_CLIENT_ID], properties[PROPNAME_CLIENT_SECRET], - disableSSL, nil) - if authenticator != nil { - authenticator.Scope = properties[PROPNAME_SCOPE] - } + authenticator, err = NewIamAuthenticatorBuilder(). + SetApiKey(properties[PROPNAME_APIKEY]). + SetRefreshToken(properties[PROPNAME_REFRESH_TOKEN]). + SetURL(properties[PROPNAME_AUTH_URL]). + SetClientIDSecret(properties[PROPNAME_CLIENT_ID], properties[PROPNAME_CLIENT_SECRET]). + SetDisableSSLVerification(disableSSL). + SetScope(properties[PROPNAME_SCOPE]). + Build() + return } @@ -165,23 +240,40 @@ func (authenticator *IamAuthenticator) setTokenData(tokenData *iamTokenData) { defer authenticator.tokenDataMutex.Unlock() authenticator.tokenData = tokenData + + // Next, we should save the just-returned refresh token back to the main + // authenticator struct. + // This is done so that if we were originally configured with + // a refresh token, then we'll be sure to use a "fresh" + // refresh token next time we invoke the "get token" operation. + // This was recommended by the IAM team to avoid problems in the future + // if the token service is changed to invalidate an existing refresh token + // when a new one is generated and returned in the response. + if tokenData != nil { + authenticator.RefreshToken = tokenData.RefreshToken + } } // Validate the authenticator's configuration. // -// Ensures the ApiKey is valid, and the ClientId and ClientSecret pair are -// mutually inclusive. +// Ensures that the ApiKey and RefreshToken properties are mutually exclusive, +// and that the ClientId and ClientSecret properties are mutually inclusive. func (this *IamAuthenticator) Validate() error { - if this.ApiKey == "" { - return fmt.Errorf(ERRORMSG_PROP_MISSING, "ApiKey") + + // The user should specify exactly one of ApiKey or RefreshToken. + if this.ApiKey == "" && this.RefreshToken == "" || + this.ApiKey != "" && this.RefreshToken != "" { + return fmt.Errorf(ERRORMSG_EXCLUSIVE_PROPS_ERROR, "ApiKey", "RefreshToken") } - if HasBadFirstOrLastChar(this.ApiKey) { + if this.ApiKey != "" && HasBadFirstOrLastChar(this.ApiKey) { return fmt.Errorf(ERRORMSG_PROP_INVALID, "ApiKey") } - // Validate ClientId and ClientSecret. They must both be specified togther or neither should be specified. - if this.ClientId == "" && this.ClientSecret == "" { + // Validate ClientId and ClientSecret. + // If RefreshToken is not specified, then both or neither should be specified. + // If RefreshToken is specified, then both must be specified. + if this.ClientId == "" && this.ClientSecret == "" && this.RefreshToken == "" { // Do nothing as this is the valid scenario } else { // Since it is NOT the case that both properties are empty, make sure BOTH are specified. @@ -255,7 +347,6 @@ func (authenticator *IamAuthenticator) invokeRequestTokenData() error { // RequestToken fetches a new access token from the token server. func (authenticator *IamAuthenticator) RequestToken() (*IamTokenServerResponse, error) { - var operationPath = "/identity/token" // Use the default IAM URL if one was not specified by the user. url := authenticator.URL @@ -263,20 +354,31 @@ func (authenticator *IamAuthenticator) RequestToken() (*IamTokenServerResponse, url = defaultIamTokenServerEndpoint } else { // Canonicalize the URL by removing the operation path if it was specified by the user. - url = strings.TrimSuffix(url, operationPath) + url = strings.TrimSuffix(url, iamAuthOperationPathGetToken) } builder := NewRequestBuilder(POST) - _, err := builder.ResolveRequestURL(url, operationPath, nil) + _, err := builder.ResolveRequestURL(url, iamAuthOperationPathGetToken, nil) if err != nil { return nil, err } - builder.AddHeader(CONTENT_TYPE, "application/x-www-form-urlencoded"). - AddHeader(Accept, APPLICATION_JSON). - AddFormData("grant_type", "", "", iamGrantTypeApiKey). - AddFormData("apikey", "", "", authenticator.ApiKey). - AddFormData("response_type", "", "", "cloud_iam") + builder.AddHeader(CONTENT_TYPE, "application/x-www-form-urlencoded") + builder.AddHeader(Accept, APPLICATION_JSON) + builder.AddFormData("response_type", "", "", "cloud_iam") + + if authenticator.ApiKey != "" { + // If ApiKey was configured, then use grant_type "apikey" to obtain an access token. + builder.AddFormData("grant_type", "", "", iamAuthGrantTypeApiKey) + builder.AddFormData("apikey", "", "", authenticator.ApiKey) + } else if authenticator.RefreshToken != "" { + // Otherwise, if RefreshToken was configured then use grant_type "refresh_token". + builder.AddFormData("grant_type", "", "", iamAuthGrantTypeRefreshToken) + builder.AddFormData("refresh_token", "", "", authenticator.RefreshToken) + } else { + // We shouldn't ever get here due to prior validations, but just in case, let's log an error. + return nil, fmt.Errorf(ERRORMSG_EXCLUSIVE_PROPS_ERROR, "ApiKey", "RefreshToken") + } // Add any optional parameters to the request. if authenticator.Scope != "" { @@ -295,6 +397,8 @@ func (authenticator *IamAuthenticator) RequestToken() (*IamTokenServerResponse, // If client id and secret were configured by the user, then set them on the request // as a basic auth header. + // Our previous validation step would have made sure that both values are specified + // if the RefreshToken property was specified. if authenticator.ClientId != "" && authenticator.ClientSecret != "" { req.SetBasicAuth(authenticator.ClientId, authenticator.ClientSecret) } @@ -378,9 +482,10 @@ type IamTokenServerResponse struct { // iamTokenData : This struct represents the cached information related to a fetched access token. type iamTokenData struct { - AccessToken string - RefreshTime int64 - Expiration int64 + AccessToken string + RefreshToken string + RefreshTime int64 + Expiration int64 } // newIamTokenData: constructs a new IamTokenData instance from the specified IamTokenServerResponse instance. @@ -395,9 +500,10 @@ func newIamTokenData(tokenResponse *IamTokenServerResponse) (*iamTokenData, erro refreshTime := expireTime - int64(float64(timeToLive)*0.2) tokenData := &iamTokenData{ - AccessToken: tokenResponse.AccessToken, - Expiration: expireTime, - RefreshTime: refreshTime, + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + Expiration: expireTime, + RefreshTime: refreshTime, } return tokenData, nil @@ -425,5 +531,4 @@ func (this *iamTokenData) needsRefresh() bool { } return false - } diff --git a/v5/core/iam_authenticator_test.go b/v5/core/iam_authenticator_test.go index 918fa70..87062f0 100644 --- a/v5/core/iam_authenticator_test.go +++ b/v5/core/iam_authenticator_test.go @@ -2,7 +2,7 @@ package core -// (C) Copyright IBM Corp. 2019. +// (C) Copyright IBM Corp. 2019, 2021. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "strings" "testing" "time" @@ -28,13 +30,221 @@ import ( var ( // To enable debug logging during test execution, set this to "LevelDebug" - iamAuthTestLogLevel LogLevel = LevelError - - AccessToken1 string = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI" - AccessToken2 string = "3yJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI" - RefreshToken string = "Xj7Gle500MachEOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI" + iamAuthTestLogLevel LogLevel = LevelError + iamAuthMockApiKey = "mock-apikey" + iamAuthMockRefreshToken = "mock-refresh-token" + iamAuthMockClientID = "bx" + iamAuthMockClientSecret = "bx" + iamAuthMockURL = "https://mock.iam.com" + iamAuthMockScope = "scope1,scope2" + + iamAuthTestAccessToken1 string = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI" + iamAuthTestAccessToken2 string = "3yJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI" + iamAuthTestRefreshToken string = "Xj7Gle500MachEOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI" ) +// +// Tests involving the Builder +// +func TestIamAuthBuilderErrors(t *testing.T) { + var err error + var auth *IamAuthenticator + + // Error: no apikey or refresh token + auth, err = NewIamAuthenticatorBuilder(). + SetApiKey(""). + SetRefreshToken(""). + Build() + assert.NotNil(t, err) + assert.Nil(t, auth) + t.Logf("Expected error: %s", err.Error()) + + // Error: specify both apikey and refresh token + auth, err = NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetRefreshToken(iamAuthMockRefreshToken). + Build() + assert.NotNil(t, err) + assert.Nil(t, auth) + t.Logf("Expected error: %s", err.Error()) + + // Error: invalid apikey + auth, err = NewIamAuthenticatorBuilder(). + SetApiKey("{invalid-apikey}"). + Build() + assert.NotNil(t, err) + assert.Nil(t, auth) + t.Logf("Expected error: %s", err.Error()) + + // Error: refresh token without client id + auth, err = NewIamAuthenticatorBuilder(). + SetRefreshToken(iamAuthMockRefreshToken). + Build() + assert.NotNil(t, err) + assert.Nil(t, auth) + t.Logf("Expected error: %s", err.Error()) + + // Error: refresh token without client secret + auth, err = NewIamAuthenticatorBuilder(). + SetRefreshToken(iamAuthMockRefreshToken). + SetClientIDSecret(iamAuthMockClientID, ""). + Build() + assert.NotNil(t, err) + assert.Nil(t, auth) + t.Logf("Expected error: %s", err.Error()) +} + +func TestIamAuthBuilderSuccess(t *testing.T) { + var err error + var auth *IamAuthenticator + var expectedHeaders = map[string]string{ + "header1": "value1", + } + + // Specify apikey. + auth, err = NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + Build() + assert.Nil(t, err) + assert.NotNil(t, auth) + assert.Equal(t, iamAuthMockApiKey, auth.ApiKey) + assert.Empty(t, auth.RefreshToken) + assert.Empty(t, auth.URL) + assert.Empty(t, auth.ClientId) + assert.Empty(t, auth.ClientSecret) + assert.False(t, auth.DisableSSLVerification) + assert.Empty(t, auth.Scope) + assert.Nil(t, auth.Headers) + assert.Equal(t, AUTHTYPE_IAM, auth.AuthenticationType()) + + // Specify refresh token along with client id/secret. + auth, err = NewIamAuthenticatorBuilder(). + SetRefreshToken(iamAuthMockRefreshToken). + SetClientIDSecret(iamAuthMockClientID, iamAuthMockClientSecret). + Build() + assert.Nil(t, err) + assert.NotNil(t, auth) + assert.Empty(t, auth.ApiKey) + assert.Empty(t, auth.URL) + assert.Equal(t, iamAuthMockRefreshToken, auth.RefreshToken) + assert.Equal(t, iamAuthMockClientID, auth.ClientId) + assert.Equal(t, iamAuthMockClientSecret, auth.ClientSecret) + assert.False(t, auth.DisableSSLVerification) + assert.Empty(t, auth.Scope) + assert.Nil(t, auth.Headers) + assert.Equal(t, AUTHTYPE_IAM, auth.AuthenticationType()) + + // Specify apikey with other properties. + auth, err = NewIamAuthenticatorBuilder(). + SetURL(iamAuthMockURL). + SetApiKey(iamAuthMockApiKey). + SetClientIDSecret(iamAuthMockClientID, iamAuthMockClientSecret). + SetDisableSSLVerification(true). + SetScope(iamAuthMockScope). + SetHeaders(expectedHeaders). + Build() + assert.Nil(t, err) + assert.NotNil(t, auth) + assert.Equal(t, iamAuthMockApiKey, auth.ApiKey) + assert.Empty(t, auth.RefreshToken) + assert.Equal(t, iamAuthMockURL, auth.URL) + assert.Equal(t, iamAuthMockClientID, auth.ClientId) + assert.Equal(t, iamAuthMockClientSecret, auth.ClientSecret) + assert.True(t, auth.DisableSSLVerification) + assert.Equal(t, iamAuthMockScope, auth.Scope) + assert.Equal(t, expectedHeaders, auth.Headers) + assert.Equal(t, AUTHTYPE_IAM, auth.AuthenticationType()) + + // Specify refresh token with other properties. + auth, err = NewIamAuthenticatorBuilder(). + SetURL(iamAuthMockURL). + SetRefreshToken(iamAuthMockRefreshToken). + SetClientIDSecret(iamAuthMockClientID, iamAuthMockClientSecret). + SetDisableSSLVerification(true). + SetScope(iamAuthMockScope). + SetHeaders(expectedHeaders). + Build() + assert.Nil(t, err) + assert.NotNil(t, auth) + assert.Empty(t, auth.ApiKey) + assert.Equal(t, iamAuthMockRefreshToken, auth.RefreshToken) + assert.Equal(t, iamAuthMockURL, auth.URL) + assert.Equal(t, iamAuthMockClientID, auth.ClientId) + assert.Equal(t, iamAuthMockClientSecret, auth.ClientSecret) + assert.True(t, auth.DisableSSLVerification) + assert.Equal(t, iamAuthMockScope, auth.Scope) + assert.Equal(t, expectedHeaders, auth.Headers) + assert.Equal(t, AUTHTYPE_IAM, auth.AuthenticationType()) +} + +// +// Tests that construct an authenticator via map properties. +// +func TestNewIamAuthenticatorFromMap(t *testing.T) { + _, err := newIamAuthenticatorFromMap(nil) + assert.NotNil(t, err) + + var props = map[string]string{ + PROPNAME_AUTH_URL: iamAuthMockURL, + } + _, err = newIamAuthenticatorFromMap(props) + assert.NotNil(t, err) + + props = map[string]string{ + PROPNAME_APIKEY: "", + } + _, err = newIamAuthenticatorFromMap(props) + assert.NotNil(t, err) + + props = map[string]string{ + PROPNAME_APIKEY: iamAuthMockApiKey, + } + authenticator, err := newIamAuthenticatorFromMap(props) + assert.Nil(t, err) + assert.NotNil(t, authenticator) + assert.Equal(t, iamAuthMockApiKey, authenticator.ApiKey) + assert.Equal(t, AUTHTYPE_IAM, authenticator.AuthenticationType()) + + props = map[string]string{ + PROPNAME_APIKEY: iamAuthMockApiKey, + PROPNAME_AUTH_DISABLE_SSL: "false", + PROPNAME_CLIENT_ID: iamAuthMockClientID, + PROPNAME_CLIENT_SECRET: iamAuthMockClientSecret, + PROPNAME_SCOPE: iamAuthMockScope, + } + authenticator, err = newIamAuthenticatorFromMap(props) + assert.Nil(t, err) + assert.NotNil(t, authenticator) + assert.Equal(t, iamAuthMockApiKey, authenticator.ApiKey) + assert.Empty(t, authenticator.RefreshToken) + assert.False(t, authenticator.DisableSSLVerification) + assert.Equal(t, iamAuthMockClientID, authenticator.ClientId) + assert.Equal(t, iamAuthMockClientSecret, authenticator.ClientSecret) + assert.Equal(t, iamAuthMockScope, authenticator.Scope) + assert.Equal(t, AUTHTYPE_IAM, authenticator.AuthenticationType()) + + props = map[string]string{ + PROPNAME_REFRESH_TOKEN: iamAuthMockRefreshToken, + PROPNAME_AUTH_DISABLE_SSL: "false", + PROPNAME_CLIENT_ID: iamAuthMockClientID, + PROPNAME_CLIENT_SECRET: iamAuthMockClientSecret, + PROPNAME_SCOPE: iamAuthMockScope, + } + authenticator, err = newIamAuthenticatorFromMap(props) + assert.Nil(t, err) + assert.NotNil(t, authenticator) + assert.Empty(t, authenticator.ApiKey) + assert.Equal(t, iamAuthMockRefreshToken, authenticator.RefreshToken) + assert.False(t, authenticator.DisableSSLVerification) + assert.Equal(t, iamAuthMockClientID, authenticator.ClientId) + assert.Equal(t, iamAuthMockClientSecret, authenticator.ClientSecret) + assert.Equal(t, iamAuthMockScope, authenticator.Scope) + assert.Equal(t, AUTHTYPE_IAM, authenticator.AuthenticationType()) +} + +// +// Tests involving the legacy ctor +// func TestIamConfigErrors(t *testing.T) { var err error @@ -47,11 +257,11 @@ func TestIamConfigErrors(t *testing.T) { assert.NotNil(t, err) // Missing ClientId. - _, err = NewIamAuthenticator("my-apikey", "", "", "bar", false, nil) + _, err = NewIamAuthenticator(iamAuthMockApiKey, "", "", "bar", false, nil) assert.NotNil(t, err) // Missing ClientSecret. - _, err = NewIamAuthenticator("my-apikey", "", "foo", "", false, nil) + _, err = NewIamAuthenticator(iamAuthMockApiKey, "", "foo", "", false, nil) assert.NotNil(t, err) } @@ -64,10 +274,12 @@ func TestIamAuthenticateFail(t *testing.T) { })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.NotNil(t, authenticator) - assert.Equal(t, AUTHTYPE_IAM, authenticator.AuthenticationType()) // Create a new Request object. builder, err := NewRequestBuilder("GET").ConstructHTTPURL("https://localhost/placeholder/url", nil, nil) @@ -78,7 +290,6 @@ func TestIamAuthenticateFail(t *testing.T) { assert.NotNil(t, request) err = authenticator.Authenticate(request) - // Validate the resulting error is a valid assert.NotNil(t, err) authErr, ok := err.(*AuthenticationError) assert.True(t, ok) @@ -109,7 +320,7 @@ func TestIamGetTokenSuccess(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "%s" - }`, AccessToken1, expiration, RefreshToken) + }`, iamAuthTestAccessToken1, expiration, iamAuthTestRefreshToken) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -120,39 +331,115 @@ func TestIamGetTokenSuccess(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "%s" - }`, AccessToken2, expiration, RefreshToken) + }`, iamAuthTestAccessToken2, expiration, iamAuthTestRefreshToken) username, password, ok := r.BasicAuth() assert.True(t, ok) - assert.Equal(t, "mookie", username) - assert.Equal(t, "betts", password) + assert.Equal(t, iamAuthMockClientID, username) + assert.Equal(t, iamAuthMockClientSecret, password) } })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) + assert.NotNil(t, authenticator) assert.Nil(t, authenticator.getTokenData()) // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Force expiration and verify that we got the second access token. authenticator.getTokenData().Expiration = GetCurrentTime() - 3600 - authenticator.ClientId = "mookie" - authenticator.ClientSecret = "betts" + authenticator.ClientId = iamAuthMockClientID + authenticator.ClientSecret = iamAuthMockClientSecret _, err = authenticator.GetToken() assert.Nil(t, err) assert.NotNil(t, authenticator.getTokenData()) - assert.Equal(t, AccessToken2, authenticator.getTokenData().AccessToken) + assert.Equal(t, iamAuthTestAccessToken2, authenticator.getTokenData().AccessToken) - // Test the RequestToken() method to make sure we can get a RefreshToken. + // Test the RequestToken() method to make sure we can get a refresh token. tokenResponse, err := authenticator.RequestToken() assert.Nil(t, err) assert.NotNil(t, tokenResponse) - assert.Equal(t, RefreshToken, tokenResponse.RefreshToken) + assert.Equal(t, iamAuthTestRefreshToken, tokenResponse.RefreshToken) +} + +func TestIamGetTokenSuccessRT(t *testing.T) { + GetLogger().SetLogLevel(iamAuthTestLogLevel) + + var newRefreshToken string = "new-refresh-token" + + firstCall := true + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + assert.Nil(t, err) + assert.Len(t, r.Form["refresh_token"], 1) + assert.Len(t, r.Form["grant_type"], 1) + assert.Equal(t, "refresh_token", r.Form["grant_type"][0]) + assert.Len(t, r.Form["response_type"], 1) + assert.Len(t, r.Form["scope"], 1) + + username, password, ok := r.BasicAuth() + assert.True(t, ok) + assert.Equal(t, iamAuthMockClientID, username) + assert.Equal(t, iamAuthMockClientSecret, password) + + w.WriteHeader(http.StatusOK) + expiration := GetCurrentTime() + 3600 + if firstCall { + fmt.Fprintf(w, `{ + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": %d, + "refresh_token": "%s" + }`, iamAuthTestAccessToken1, expiration, iamAuthTestRefreshToken) + firstCall = false + } else { + fmt.Fprintf(w, `{ + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": %d, + "refresh_token": "%s" + }`, iamAuthTestAccessToken2, expiration, newRefreshToken) + } + })) + defer server.Close() + + authenticator, err := NewIamAuthenticatorBuilder(). + SetRefreshToken(iamAuthMockRefreshToken). + SetClientIDSecret(iamAuthMockClientID, iamAuthMockClientSecret). + SetURL(server.URL). + SetScope(iamAuthMockScope). + Build() + assert.Nil(t, err) + assert.NotNil(t, authenticator) + assert.Nil(t, authenticator.getTokenData()) + assert.Equal(t, iamAuthMockRefreshToken, authenticator.RefreshToken) + + // Force the first fetch and verify we got the first access token. + // From this first fetch, we should also the first refresh token saved to the authenticator. + token, err := authenticator.GetToken() + assert.Nil(t, err) + assert.Equal(t, iamAuthTestAccessToken1, token) + assert.NotNil(t, authenticator.getTokenData()) + assert.Equal(t, iamAuthTestRefreshToken, authenticator.RefreshToken) + + // Force expiration and verify that we got the second access token. + // At this point, we should also have a second refresh token saved in the authenticator. + authenticator.getTokenData().Expiration = GetCurrentTime() - 3600 + _, err = authenticator.GetToken() + assert.Nil(t, err) + assert.NotNil(t, authenticator.getTokenData()) + assert.Equal(t, iamAuthTestAccessToken2, authenticator.getTokenData().AccessToken) + assert.Equal(t, newRefreshToken, authenticator.RefreshToken) } func TestIamGetTokenSuccessWithScope(t *testing.T) { @@ -166,7 +453,7 @@ func TestIamGetTokenSuccessWithScope(t *testing.T) { assert.Len(t, r.Form["grant_type"], 1) assert.Len(t, r.Form["response_type"], 1) assert.Len(t, r.Form["scope"], 1) - assert.Equal(t, "scope1 scope2", r.Form["scope"][0]) + assert.Equal(t, iamAuthMockScope, r.Form["scope"][0]) w.WriteHeader(http.StatusOK) expiration := GetCurrentTime() + 3600 @@ -177,7 +464,7 @@ func TestIamGetTokenSuccessWithScope(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -188,34 +475,37 @@ func TestIamGetTokenSuccessWithScope(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken2, expiration) + }`, iamAuthTestAccessToken2, expiration) username, password, ok := r.BasicAuth() assert.True(t, ok) - assert.Equal(t, "mookie", username) - assert.Equal(t, "betts", password) + assert.Equal(t, iamAuthMockClientID, username) + assert.Equal(t, iamAuthMockClientSecret, password) } })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + SetScope(iamAuthMockScope). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) - authenticator.Scope = "scope1 scope2" // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Force expiration and verify that we got the second access token. authenticator.getTokenData().Expiration = GetCurrentTime() - 3600 - authenticator.ClientId = "mookie" - authenticator.ClientSecret = "betts" + authenticator.ClientId = iamAuthMockClientID + authenticator.ClientSecret = iamAuthMockClientSecret _, err = authenticator.GetToken() assert.Nil(t, err) assert.NotNil(t, authenticator.getTokenData()) - assert.Equal(t, AccessToken2, authenticator.getTokenData().AccessToken) + assert.Equal(t, iamAuthTestAccessToken2, authenticator.getTokenData().AccessToken) } func TestIamGetCachedToken(t *testing.T) { GetLogger().SetLogLevel(iamAuthTestLogLevel) @@ -231,7 +521,7 @@ func TestIamGetCachedToken(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -242,27 +532,30 @@ func TestIamGetCachedToken(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken2, expiration) + }`, iamAuthTestAccessToken2, expiration) _, _, ok := r.BasicAuth() assert.True(t, ok) } })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Subsequent fetch should still return first access token. token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) } @@ -280,7 +573,7 @@ func TestIamBackgroundTokenRefresh(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -291,21 +584,24 @@ func TestIamBackgroundTokenRefresh(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken2, expiration) + }`, iamAuthTestAccessToken2, expiration) _, _, ok := r.BasicAuth() assert.False(t, ok) } })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Now put the test in the "refresh window" where the token is not expired but still needs to be refreshed. @@ -316,14 +612,14 @@ func TestIamBackgroundTokenRefresh(t *testing.T) { // expired. token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Wait for the background thread to finish time.Sleep(5 * time.Second) token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken2, token) + assert.Equal(t, iamAuthTestAccessToken2, token) assert.NotNil(t, authenticator.getTokenData()) } @@ -341,7 +637,7 @@ func TestIamBackgroundTokenRefreshFailure(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -351,14 +647,17 @@ func TestIamBackgroundTokenRefreshFailure(t *testing.T) { })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) // Successfully fetch the first token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Now put the test in the "refresh window" where the token is not expired but still needs to be refreshed. @@ -368,7 +667,7 @@ func TestIamBackgroundTokenRefreshFailure(t *testing.T) { // expired. token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Wait for the background thread to finish time.Sleep(5 * time.Second) @@ -396,7 +695,7 @@ func TestIamBackgroundTokenRefreshIdle(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -407,21 +706,24 @@ func TestIamBackgroundTokenRefreshIdle(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken2, expiration) + }`, iamAuthTestAccessToken2, expiration) _, _, ok := r.BasicAuth() assert.False(t, ok) } })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Now simulate the client being idle for 10 minutes into the refresh time @@ -433,7 +735,7 @@ func TestIamBackgroundTokenRefreshIdle(t *testing.T) { // expired. token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // RefreshTime should have advanced by 1 minute from the current time @@ -445,7 +747,7 @@ func TestIamBackgroundTokenRefreshIdle(t *testing.T) { // a goroutine & refreshed the token. token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) assert.Equal(t, newRefreshTime, authenticator.getTokenData().RefreshTime) @@ -454,7 +756,7 @@ func TestIamBackgroundTokenRefreshIdle(t *testing.T) { time.Sleep(5 * time.Second) token, err = authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken2, token) + assert.Equal(t, iamAuthTestAccessToken2, token) assert.NotNil(t, authenticator.getTokenData()) assert.NotEqual(t, newRefreshTime, authenticator.getTokenData().RefreshTime) } @@ -474,17 +776,18 @@ func TestIamClientIdAndSecret(t *testing.T) { }`, accessToken, expiration) username, password, ok := r.BasicAuth() assert.True(t, ok) - assert.Equal(t, "mookie", username) - assert.Equal(t, "betts", password) + assert.Equal(t, iamAuthMockClientID, username) + assert.Equal(t, iamAuthMockClientSecret, password) })) defer server.Close() - authenticator := &IamAuthenticator{ - ApiKey: "bogus-apikey", - URL: server.URL, - ClientId: "mookie", - ClientSecret: "betts", - } + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + SetClientIDSecret(iamAuthMockClientID, iamAuthMockClientSecret). + Build() + assert.Nil(t, err) + assert.NotNil(t, authenticator) token, err := authenticator.GetToken() assert.Equal(t, accessToken, token) @@ -528,7 +831,11 @@ func TestIamDisableSSL(t *testing.T) { })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", true, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + SetDisableSSLVerification(true). + Build() assert.Nil(t, err) token, err := authenticator.GetToken() @@ -563,12 +870,17 @@ func TestIamUserHeaders(t *testing.T) { })) defer server.Close() - headers := make(map[string]string) - headers["Header1"] = "Value1" - headers["Header2"] = "Value2" - headers["Host"] = "iam.cloud.ibm.com" + var headers = map[string]string{ + "Header1": "Value1", + "Header2": "Value2", + "Host": "iam.cloud.ibm.com", + } - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, headers) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + SetHeaders(headers). + Build() assert.Nil(t, err) token, err := authenticator.GetToken() @@ -585,14 +897,15 @@ func TestIamGetTokenFailure(t *testing.T) { })) defer server.Close() - authenticator := &IamAuthenticator{ - ApiKey: "bogus-apikey", - URL: server.URL, - } + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() + assert.Nil(t, err) var expectedResponse = []byte("Sorry you are forbidden") - _, err := authenticator.GetToken() + _, err = authenticator.GetToken() assert.NotNil(t, err) assert.Equal(t, "Sorry you are forbidden", err.Error()) // We expect an AuthenticationError to be returned, so cast the returned error @@ -623,7 +936,7 @@ func TestIamGetTokenTimeoutError(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -635,19 +948,22 @@ func TestIamGetTokenTimeoutError(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken2, expiration) + }`, iamAuthTestAccessToken2, expiration) } })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) // Force expiration and verify that we got a timeout error @@ -678,7 +994,7 @@ func TestIamGetTokenServerError(t *testing.T) { "expires_in": 3600, "expiration": %d, "refresh_token": "jy4gl91BQ" - }`, AccessToken1, expiration) + }`, iamAuthTestAccessToken1, expiration) firstCall = false _, _, ok := r.BasicAuth() assert.False(t, ok) @@ -689,14 +1005,17 @@ func TestIamGetTokenServerError(t *testing.T) { })) defer server.Close() - authenticator, err := NewIamAuthenticator("bogus-apikey", server.URL, "", "", false, nil) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + SetURL(server.URL). + Build() assert.Nil(t, err) assert.Nil(t, authenticator.getTokenData()) // Force the first fetch and verify we got the first access token. token, err := authenticator.GetToken() assert.Nil(t, err) - assert.Equal(t, AccessToken1, token) + assert.Equal(t, iamAuthTestAccessToken1, token) assert.NotNil(t, authenticator.getTokenData()) var expectedResponse = []byte("Gateway Timeout") @@ -720,111 +1039,125 @@ func TestIamGetTokenServerError(t *testing.T) { assert.Empty(t, token) } -func TestNewIamAuthenticatorFromMap(t *testing.T) { - _, err := newIamAuthenticatorFromMap(nil) - assert.NotNil(t, err) - - var props = map[string]string{ - PROPNAME_AUTH_URL: "iam-url", - } - _, err = newIamAuthenticatorFromMap(props) - assert.NotNil(t, err) - - props = map[string]string{ - PROPNAME_APIKEY: "", - } - _, err = newIamAuthenticatorFromMap(props) - assert.NotNil(t, err) +func TestIamRequestTokenError(t *testing.T) { + GetLogger().SetLogLevel(iamAuthTestLogLevel) - props = map[string]string{ - PROPNAME_APIKEY: "my-apikey", - } - authenticator, err := newIamAuthenticatorFromMap(props) + authenticator, err := NewIamAuthenticatorBuilder(). + SetApiKey(iamAuthMockApiKey). + Build() assert.Nil(t, err) assert.NotNil(t, authenticator) - assert.Equal(t, "my-apikey", authenticator.ApiKey) - props = map[string]string{ - PROPNAME_APIKEY: "my-apikey", - PROPNAME_AUTH_DISABLE_SSL: "huh???", - PROPNAME_CLIENT_ID: "mookie", - PROPNAME_CLIENT_SECRET: "betts", - PROPNAME_SCOPE: "scope1 scope2", - } - authenticator, err = newIamAuthenticatorFromMap(props) - assert.Nil(t, err) - assert.NotNil(t, authenticator) - assert.Equal(t, "my-apikey", authenticator.ApiKey) - assert.False(t, authenticator.DisableSSLVerification) - assert.Equal(t, "mookie", authenticator.ClientId) - assert.Equal(t, "betts", authenticator.ClientSecret) - assert.Equal(t, "scope1 scope2", authenticator.Scope) + // Now forcibly clear the ApiKey field so we can test an error condition. + authenticator.ApiKey = "" + + _, err = authenticator.RequestToken() + assert.NotNil(t, err) + t.Logf("Expected error: %s", err.Error()) } // // In order to test with a live IAM server, create file "iamtest.env" in the project root. // It should look like this: +// IAMTEST1_AUTH_URL= e.g. https://iam.cloud.ibm.com +// IAMTEST1_AUTH_TYPE=iam +// IAMTEST1_APIKEY= // -// IAMTEST1_AUTH_URL= e.g. https://iam.test.cloud.ibm.com -// IAMTEST1_AUTH_TYPE=iam -// IAMTEST1_APIKEY= -// -// Then uncomment the function below, then add "os" and "strings" to the import list, -// then run these commands: -// cd v/core -// go test -v -tags=auth -run=TestIamLiveTokenServer +// Then comment out the "t.Skip()" line below, then run these commands: +// cd v/core +// go test -v -tags=auth -run=TestIamLiveTokenServer -v // +// To trace request/response messages, change "iamAuthTestLogLevel" above to be "LevelDebug". // -// func TestIamLiveTokenServer(t *testing.T) { -// GetLogger().SetLogLevel(iamAuthTestLogLevel) // -// var request *http.Request -// var err error -// var authHeader string -// var tokenServerResponse *IamTokenServerResponse - -// // Get an iam authenticator from the environment. -// os.Setenv("IBM_CREDENTIALS_FILE", "../../iamtest.env") - -// auth, err := GetAuthenticatorFromEnvironment("iamtest1") -// assert.Nil(t, err) -// assert.NotNil(t, auth) - -// iamAuth, ok := auth.(*IamAuthenticator) -// assert.Equal(t, true, ok) - -// tokenServerResponse, err = iamAuth.RequestToken() -// if err != nil { -// authError := err.(*AuthenticationError) -// iamError := authError.Err -// iamResponse := authError.Response -// t.Logf("Unexpected authentication error: %s\n", iamError.Error()) -// t.Logf("Authentication response: %v+\n", iamResponse) - -// } -// assert.Nil(t, err) -// assert.NotNil(t, tokenServerResponse) - -// accessToken := tokenServerResponse.AccessToken -// assert.NotNil(t, accessToken) -// t.Logf("Access token: %s\n", accessToken) - -// refreshToken := tokenServerResponse.RefreshToken -// assert.NotNil(t, refreshToken) -// t.Logf("Refresh token: %s\n", refreshToken) - -// // Create a new Request object. -// builder, err := NewRequestBuilder("GET").ResolveRequestURL("https://localhost/placeholder/url", "", nil) -// assert.Nil(t, err) -// assert.NotNil(t, builder) - -// request, _ = builder.Build() -// assert.NotNil(t, request) -// err = auth.Authenticate(request) -// assert.Nil(t, err) - -// authHeader = request.Header.Get("Authorization") -// assert.NotEmpty(t, authHeader) -// assert.True(t, strings.HasPrefix(authHeader, "Bearer ")) -// t.Logf("Authorization: %s\n", authHeader) -// } +func TestIamLiveTokenServer(t *testing.T) { + t.Skip("Skipping IAM integration test...") + + GetLogger().SetLogLevel(iamAuthTestLogLevel) + + var request *http.Request + var err error + var authHeader string + var tokenServerResponse *IamTokenServerResponse + + // Get an iam authenticator from the environment. + os.Setenv("IBM_CREDENTIALS_FILE", "../../iamtest.env") + + auth, err := GetAuthenticatorFromEnvironment("iamtest1") + assert.Nil(t, err) + assert.NotNil(t, auth) + + iamAuth, ok := auth.(*IamAuthenticator) + assert.Equal(t, true, ok) + + tokenServerResponse, err = iamAuth.RequestToken() + if err != nil { + authError := err.(*AuthenticationError) + iamError := authError.Err + iamResponse := authError.Response + t.Logf("Unexpected authentication error: %s\n", iamError.Error()) + t.Logf("Authentication response: %v+\n", iamResponse) + + } + assert.Nil(t, err) + assert.NotNil(t, tokenServerResponse) + + accessToken := tokenServerResponse.AccessToken + assert.NotEmpty(t, accessToken) + t.Logf("Access token: %s\n", accessToken) + + refreshToken := tokenServerResponse.RefreshToken + assert.NotEmpty(t, refreshToken) + t.Logf("Refresh token: %s\n", refreshToken) + + // Create a new Request object. + builder, err := NewRequestBuilder("GET").ResolveRequestURL("https://localhost/placeholder/url", "", nil) + assert.Nil(t, err) + assert.NotNil(t, builder) + + request, _ = builder.Build() + assert.NotNil(t, request) + err = auth.Authenticate(request) + assert.Nil(t, err) + + authHeader = request.Header.Get("Authorization") + assert.NotEmpty(t, authHeader) + assert.True(t, strings.HasPrefix(authHeader, "Bearer ")) + t.Logf("Authorization: %s\n", authHeader) + + // Now create a new IamAuthenticator using bx:bx so that we can retrieve + // the refresh token value and then do some testing with that. + // We'll use the URL and ApiKey from the original authenticator above. + newAuth, err := NewIamAuthenticatorBuilder(). + SetURL(iamAuth.URL). + SetApiKey(iamAuth.ApiKey). + SetClientIDSecret("bx", "bx"). + Build() + assert.Nil(t, err) + assert.NotNil(t, newAuth) + + tokenServerResponse, err = newAuth.RequestToken() + assert.Nil(t, err) + assert.NotNil(t, tokenServerResponse) + + refreshToken = tokenServerResponse.RefreshToken + assert.NotEmpty(t, refreshToken) + + // Create a new IamAuthenticator configured with the refresh token. + refreshAuth, err := NewIamAuthenticatorBuilder(). + SetURL(newAuth.URL). + SetRefreshToken(refreshToken). + SetClientIDSecret("bx", "bx"). + Build() + assert.Nil(t, err) + assert.NotNil(t, refreshAuth) + assert.Equal(t, refreshToken, refreshAuth.RefreshToken) + + // Trigger the authenticator to invoke the "get token" operation. + // and make sure that we got back an access token and that we + // saved a different refresh token in the authenticator. + accessToken, err = refreshAuth.GetToken() + assert.Nil(t, err) + assert.NotEmpty(t, accessToken) + assert.NotEqual(t, refreshToken, refreshAuth.RefreshToken) +} diff --git a/v5/resources/my-credentials.env b/v5/resources/my-credentials.env index b4305ca..2987da0 100644 --- a/v5/resources/my-credentials.env +++ b/v5/resources/my-credentials.env @@ -78,6 +78,13 @@ SERVICE8B_AUTH_URL=http://vpc.imds.com/api SERVICE8C_AUTH_TYPE=vpc SERVICE8C_IAM_PROFILE_ID=iam-profile1-id +# IAM auth using refresh token +SERVICE9_AUTH_TYPE=iam +SERVICE9_REFRESH_TOKEN=refresh-token +SERVICE9_CLIENT_ID=user1 +SERVICE9_CLIENT_SECRET=secret1 +SERVICE9_AUTH_URL=https://iam.refresh-token.com + # EQUAL service exercises value with = in them EQUAL_SERVICE_URL==https:/my=host.com/my=service/api EQUAL_SERVICE_APIKEY==my=api=key=