Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ability to refresh auth token #195

Merged
merged 10 commits into from
Sep 25, 2024
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "go.sum|^.secrets.baseline$",
"lines": null
},
"generated_at": "2024-06-06T22:18:14Z",
"generated_at": "2024-09-24T20:48:27Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -242,7 +242,7 @@
"hashed_secret": "6f667d3e9627f5549ffeb1055ff294c34430b837",
"is_secret": false,
"is_verified": false,
"line_number": 171,
"line_number": 193,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
55 changes: 55 additions & 0 deletions examples/cmd/iam_demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"time"

"github.com/softlayer/softlayer-go/services"
"github.com/softlayer/softlayer-go/session"
)

func init() {
rootCmd.AddCommand(iamDemoCmd)
}

var iamDemoCmd = &cobra.Command{
Use: "iam-demo",
Short: "Will make 1 API call per minute and refresh API key when needed.",
RunE: func(cmd *cobra.Command, args []string) error {
return RunIamCmd(cmd, args)
},
}

func RunIamCmd(cmd *cobra.Command, args []string) error {
objectMask := "mask[id,companyName]"

// Sets up the session with authentication headers.
sess := &session.Session{
Endpoint: session.DefaultEndpoint,
IAMToken: "Bearer TOKEN",
IAMRefreshToken: "REFRESH TOKEN",
Debug: true,
}

// creates a reference to the service object (SoftLayer_Account)
service := services.GetAccountService(sess)

// Sets the mask, filter, result limit, and then makes the API call SoftLayer_Account::getHardware()

for {
account, err := service.Mask(objectMask).GetObject()
if err != nil {
fmt.Printf("======= ERROR ======")
return err
}
fmt.Printf("AccountId: %v, CompanyName: %v\n", *account.Id, *account.CompanyName)
fmt.Printf("Refreshing Token for no reason...\n")
sess.RefreshToken()
fmt.Printf("%s\n", sess.IAMToken)
fmt.Printf("Sleeping for 60s.......\n")
time.Sleep(60 * time.Second)
}

return nil
}
18 changes: 1 addition & 17 deletions tests/specialcase_test.go → generator/specialcase_test.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package tests
package generator_test

import (
"github.com/softlayer/softlayer-go/datatypes"
Expand Down
14 changes: 8 additions & 6 deletions session/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ func (r *RestTransport) DoRequest(sess *Session, service string, method string,

path := buildPath(service, method, options)

resp, code, err := sendHTTPRequest(
sess,
path,
restMethod,
parameters,
options)
resp, code, err := sendHTTPRequest(sess, path, restMethod, parameters, options)

//Check if this is a refreshable exception
if err != nil && sess.IAMRefreshToken != "" && NeedsRefresh(err) {
sess.RefreshToken()
resp, code, err = sendHTTPRequest(sess, path, restMethod, parameters, options)
}

if err != nil {
//Preserve the original sl error
Expand Down Expand Up @@ -226,6 +227,7 @@ func makeHTTPRequest(
} else {
url = url + session.Endpoint
}
// fmt.Printf("Calling %s/%s", strings.TrimRight(url, "/"), path)
url = fmt.Sprintf("%s/%s", strings.TrimRight(url, "/"), path)
req, err := http.NewRequest(requestType, url, bytes.NewBuffer(requestBody))
if err != nil {
Expand Down
65 changes: 61 additions & 4 deletions session/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ package session

import (
"errors"
"testing"

"fmt"
"net/http"
"reflect"
"testing"

"github.com/jarcoal/httpmock"
"github.com/softlayer/softlayer-go/datatypes"
"github.com/softlayer/softlayer-go/sl"
"github.com/softlayer/softlayer-go/tests"
)

var s *Session
Expand Down Expand Up @@ -208,7 +207,7 @@ var testcases = []testcase{
sl.String("https://example.com"),
},
options: sl.Options{Id: sl.Int(12345)},
responder: tests.NewEchoResponder(200),
responder: httpmock.NewStringResponder(200, `{"parameters":["https://example.com"]}`),
expected: `{"parameters":["https://example.com"]}`,
expectError: nil,
},
Expand Down Expand Up @@ -320,6 +319,64 @@ func TestRest(t *testing.T) {
}
}

// Tests refreshing a IAM token if it is expired.
func TestRestReauth(t *testing.T) {
// setup session and mock environment
s = New()
s.Endpoint = restEndpoint
s.IAMToken = "Bearer TestToken"
s.IAMRefreshToken = "TestTokenRefresh"
//s.Debug = true
httpmock.Activate()
defer httpmock.DeactivateAndReset()
fmt.Printf("Test [Rest Reauthentication]: ")
slOptions := &sl.Options{}
slResults := &datatypes.Account{}

httpmock.RegisterResponder("GET", fmt.Sprintf("%s/SoftLayer_Account.json", restEndpoint),
func(req *http.Request) (*http.Response, error) {
if s.IAMToken == "Bearer TestToken" {
return httpmock.NewStringResponse(
500,
`{"code":"SoftLayer_Exception_Account_Authentication_AccessTokenValidation", "error": "Expired Token"}`,
), nil
} else {
return httpmock.NewStringResponse(
200,
`{"id":1234,"companyName":"test"}`,
), nil
}
})
httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT,
httpmock.NewStringResponder(200, `{"access_token": "NewToken123", "refresh_token":"NewRefreshToken123", "token_type":"Bearer"}`),
)
err := s.DoRequest("SoftLayer_Account", "getObject", nil, slOptions, slResults)
if err != nil {
t.Errorf("Testing Error: %v\n", err.Error())
}

if s.IAMToken != "Bearer NewToken123" {
t.Errorf("(IAMToken) %s != 'Bearer NewToken123', Refresh Failed.", s.IAMToken)
}
if s.IAMRefreshToken != "NewRefreshToken123" {
t.Errorf("(IAMRefreshToken) %s != 'NewRefreshToken123', Refresh Failed.", s.IAMRefreshToken)
}
if httpmock.GetTotalCallCount() != 3 {
t.Errorf("Call Count = %d, expected 3", httpmock.GetTotalCallCount())
}
callInfo := httpmock.GetCallCountInfo()
fmt.Printf("%v\n", callInfo)
iamUrl := "POST https://iam.cloud.ibm.com/identity/token"
slUrl := "GET https://api.softlayer.com/rest/v3/SoftLayer_Account.json"
if callInfo[iamUrl] != 1 {
t.Errorf("%s called %d times, expected 1", iamUrl, callInfo[iamUrl])
}
if callInfo[slUrl] != 2 {
t.Errorf("%s called %d times, expected 1", slUrl, callInfo[slUrl])
}
teardown()
}

func setup(tc testcase) {
httpmock.RegisterResponder(
httpMethod(tc.method, tc.args),
Expand Down
84 changes: 84 additions & 0 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ package session

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"os/user"
"strings"
Expand All @@ -44,6 +48,21 @@ func init() {
// DefaultEndpoint is the default endpoint for API calls, when no override is provided.
const DefaultEndpoint = "https://api.softlayer.com/rest/v3.1"

const IBMCLOUDIAMENDPOINT = "https://iam.cloud.ibm.com/identity/token"

// IAMTokenResponse ...
type IAMTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
}

// IAMErrorMessage -
type IAMErrorMessage struct {
ErrorMessage string `json:"errormessage"`
ErrorCode string `json:"errorcode"`
}

var retryableErrorCodes = []string{"SoftLayer_Exception_WebService_RateLimitExceeded"}

// TransportHandler interface for the protocol-specific handling of API requests.
Expand Down Expand Up @@ -105,6 +124,9 @@ type Session struct {
//IAMToken is the IAM token secret that included IMS account for token-based authentication
IAMToken string

//IAMRefreshToken is the IAM refresh token secret that required to refresh IAM Token
IAMRefreshToken string

// AuthToken is the token secret for token-based authentication
AuthToken string

Expand Down Expand Up @@ -316,6 +338,59 @@ func (r *Session) ResetUserAgent() {
r.userAgent = getDefaultUserAgent()
}

// Refreshes an IAM authenticated session
func (r *Session) RefreshToken() error {

Logger.Println("[DEBUG] Refreshing IAM Token")
client := http.DefaultClient
reqPayload := url.Values{}
reqPayload.Add("grant_type", "refresh_token")
reqPayload.Add("refresh_token", r.IAMRefreshToken)

req, err := http.NewRequest("POST", IBMCLOUDIAMENDPOINT, strings.NewReader(reqPayload.Encode()))

if err != nil {
return err
}
req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("bx:bx")))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "application/json")
var token IAMTokenResponse
var eresp IAMErrorMessage

resp, err := client.Do(req)
if err != nil {
return err
}

defer resp.Body.Close()

responseBody, err := ioutil.ReadAll(resp.Body)

if err != nil {
return err
}

if resp != nil && resp.StatusCode != 200 {
err = json.Unmarshal(responseBody, &eresp)
if err != nil {
return err
}
if eresp.ErrorCode != "" {
return sl.Error{Exception: eresp.ErrorCode, Message: eresp.ErrorMessage}
}
}

err = json.Unmarshal(responseBody, &token)
if err != nil {
return err
}

r.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
r.IAMRefreshToken = token.RefreshToken
return nil
}

func envFallback(keyName string, value *string) {
if *value == "" {
*value = os.Getenv(keyName)
Expand Down Expand Up @@ -372,6 +447,15 @@ func isRetryable(err error) bool {
return isTimeout(err) || hasRetryableCode(err)
}

func NeedsRefresh(err error) bool {
if slError, ok := err.(sl.Error); ok {
if slError.StatusCode == 500 && slError.Exception == "SoftLayer_Exception_Account_Authentication_AccessTokenValidation" {
return true
}
}
return false
}

// Set ENV Variable SL_USERAGENT to append that to the useragent string
func getDefaultUserAgent() string {
envAgent := os.Getenv("SL_USERAGENT")
Expand Down
Loading
Loading