Skip to content

Commit

Permalink
Merge pull request #199 from softlayer/refreshv2
Browse files Browse the repository at this point in the history
Added an observer for IAMToken refreshes
  • Loading branch information
allmightyspiff authored Oct 2, 2024
2 parents dc4dd3f + d06b8ed commit 7cfa57e
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 10 deletions.
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-09-27T22:05:21Z",
"generated_at": "2024-10-01T21:07:46Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -242,7 +242,7 @@
"hashed_secret": "6f667d3e9627f5549ffeb1055ff294c34430b837",
"is_secret": false,
"is_verified": false,
"line_number": 197,
"line_number": 201,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,60 @@ func main() {
}
```

### IAM authentication

This library supports [IBM's IAM Authentication](https://cloud.ibm.com/docs/account?topic=account-iamoverview) (used by the `ibmcloud` cli for example). You will want to set the `IAMToken` and `IAMRefreshToken` properties on the session to make use of it.


```go
token := "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
refreshToken := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb..."
sess := &session.Session{
Endpoint: "https://api.softlayer.com/rest/v3.1",
Debug: true,
Timeout: 90,
IAMToken: token,
IAMRefreshToken: refreshToken
}
```

You can bypass automatic IAM refresh by either not setting the `IAMRefreshToken` property, or by manually configuring the `TransportHandler`

```go
token := "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
handler := &session.RestTransport{}
sess := &session.Session{
Endpoint: "https://api.softlayer.com/rest/v3.1",
Debug: true,
Timeout: 90,
IAMToken: token,
TransportHandler handler,
}
```

If you want to be able to record the new tokens in a config file (or elsewhere), you can configure an `IAMUpdaters` Observer which will take in as arguments the new tokens, allowing you to save them somewhere for reuse.

```go
type MyIamUpdater struct {
debug bool
}
// This function is where you can configure the logic to save these new tokens somewhere
func (iamupdater *MyIamUpdater) Update(token string, refresh string) {
fmt.Printf("[DEBUG] New Token: %s\n", token)
fmt.Printf("[DEBUG] New Refresh Token: %s\n", refresh)
}
updater := &MyIamUpdater{debug: false}
token := "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
refreshToken := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb..."
sess := &session.Session{
Endpoint: "https://api.softlayer.com/rest/v3.1",
IAMToken: token,
IAMRefreshToken refreshToken,
}
sess.AddIAMUpdater(updater)
```
You can add multiple Updaters as well, they will be called in the order they are added. `MyIamUpdater.Update(token, refresh)` in this example will only be called when the token is actually refreshed.

## Running Examples

The [Examples](https://github.com/softlayer/softlayer-go/tree/master/examples) directory has a few rough examples scripts that can help you get started developing with this library.
Expand Down
23 changes: 23 additions & 0 deletions session/iamupdater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package session

//counterfeiter:generate . IAMUpdater
type IAMUpdater interface {
Update(token string, refresh string)
}

type LogIamUpdater struct {
debug bool
}

func NewLogIamUpdater(debug bool) *LogIamUpdater {
return &LogIamUpdater{
debug: debug,
}
}

func (iamupdater *LogIamUpdater) Update(token string, refresh string) {
if iamupdater.debug {
Logger.Printf("[DEBUG] New Token: %s\n", token)
Logger.Printf("[DEBUG] New Refresh Token: %s\n", refresh)
}
}
6 changes: 0 additions & 6 deletions session/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,6 @@ func (r *RestTransport) DoRequest(sess *Session, service string, method string,

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
if _, ok := err.(sl.Error); ok {
Expand Down
23 changes: 21 additions & 2 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ type Session struct {
//IAMRefreshToken is the IAM refresh token secret that required to refresh IAM Token
IAMRefreshToken string

// A list objects that implement the IAMUpdater interface.
// When a IAMToken is refreshed, these are notified with the new token and new refresh token.
IAMUpdaters []IAMUpdater

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

Expand Down Expand Up @@ -289,6 +293,11 @@ func (r *Session) DoRequest(service string, method string, args []interface{}, o
}

err := r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
//Check if this is a refreshable exception and try 1 more time
if err != nil && r.IAMRefreshToken != "" && NeedsRefresh(err) {
r.RefreshToken()
err = r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
}
r.LastCall = CallToString(service, method, args, options)
if err != nil {
return err
Expand Down Expand Up @@ -345,8 +354,6 @@ func (r *Session) ResetUserAgent() {

// 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")
Expand Down Expand Up @@ -393,6 +400,10 @@ func (r *Session) RefreshToken() error {

r.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
r.IAMRefreshToken = token.RefreshToken
// Mostly these are needed if we want to save these new tokens to a config file.
for _, updater := range r.IAMUpdaters {
updater.Update(r.IAMToken, r.IAMRefreshToken)
}
return nil
}

Expand All @@ -401,6 +412,12 @@ func (r *Session) String() string {
return r.LastCall
}

// Adds a new IAMUpdater instance to the session
// Useful if you want to update a config file with the new Tokens
func (r *Session) AddIAMUpdater(updater IAMUpdater) {
r.IAMUpdaters = append(r.IAMUpdaters, updater)
}

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

// Detects if the SL API returned a specific exception indicating the IAMToken is expired.
func NeedsRefresh(err error) bool {
if slError, ok := err.(sl.Error); ok {
if slError.StatusCode == 500 && slError.Exception == "SoftLayer_Exception_Account_Authentication_AccessTokenValidation" {
Expand All @@ -475,6 +493,7 @@ func getDefaultUserAgent() string {
return fmt.Sprintf("softlayer-go/%s %s ", sl.Version.String(), envAgent)
}

// Formats an API call into a readable string
func CallToString(service string, method string, args []interface{}, options *sl.Options) string {
if options == nil {
options = new(sl.Options)
Expand Down
46 changes: 46 additions & 0 deletions session/session_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package session

import (
"bytes"
"fmt"
"github.com/jarcoal/httpmock"
"github.com/softlayer/softlayer-go/datatypes"
"github.com/softlayer/softlayer-go/sl"
"log"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -178,6 +180,50 @@ func TestRefreshToken(t *testing.T) {
httpmock.Reset()
}

// Tests refreshing a IAM token logging output and calling IAMUpdaters
func TestRefreshTokenWithLog(t *testing.T) {
// setup session and mock environment
logBuf := bytes.Buffer{}
Logger = log.New(&logBuf, "", log.LstdFlags)
s = New()
s.Endpoint = restEndpoint
s.IAMToken = "Bearer TestToken"
s.IAMRefreshToken = "TestTokenRefresh"
//s.Debug = true
updater := NewLogIamUpdater(true)
s.AddIAMUpdater(updater)
httpmock.Activate()
defer httpmock.DeactivateAndReset()
fmt.Printf("TestRefreshTokenWithLog [Happy Path]: ")

// Happy Path
httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT,
httpmock.NewStringResponder(200, `{"access_token": "NewToken123", "refresh_token":"NewRefreshToken123", "token_type":"Bearer"}`),
)
err := s.RefreshToken()
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)
}
logOutput := strings.Split(logBuf.String(), "\n")
if len(logOutput) < 2 {
t.Errorf("Not enough log output detected.")
}
if !strings.HasSuffix(logOutput[0], "[DEBUG] New Token: Bearer NewToken123") {
t.Errorf("%s is incorrect log output", logOutput[0])
}
if !strings.HasSuffix(logOutput[1], "[DEBUG] New Refresh Token: NewRefreshToken123") {
t.Errorf("%s is incorrect log output", logOutput[1])
}
httpmock.Reset()
}

func TestString(t *testing.T) {
// setup session and mock environment
s = New()
Expand Down

0 comments on commit 7cfa57e

Please sign in to comment.