-
Notifications
You must be signed in to change notification settings - Fork 24
Add token refresh in background #333
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
Merged
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
e905933
Implement refresh token functionality
1be2b7d
Fix error messages
5f32b7d
Add comment
bea8006
Fix comments
81558ce
Document field
e8694d4
Rename variable
d636436
Fix bug
a4f25c4
Add tests
4bfacac
Reorder fields
72aea36
Add changelog
f37c649
Add TestRefreshTokenConcurrency
72c7696
Lint fix
f3188a4
Rename fields and option
7a26487
Remove GoPlayground link
01b7723
Rename methods and structs
82feefb
Rename methods
c1e02a4
Rename test
907c557
Change test settings
801d999
Change test settings
e2f64fc
Override errors from Body.Close() only when main err is nil
4665d53
Fix comments
7646714
Fix comment
89719e3
Improve comments
d04cd3c
Improve comments
18cc152
Improve comments
48be95b
Replace if-else with switch
3a82335
Change test settings
89d8c76
Fix comment
ccc5701
Fix comment
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,126 @@ | ||
| package clients | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/golang-jwt/jwt/v5" | ||
|
|
||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| ) | ||
|
|
||
| var ( | ||
| defaultTimeStartBeforeTokenExpiration = 30 * time.Minute | ||
| defaultTimeBetweenContextCheck = time.Second | ||
| defaultTimeBetweenTries = 5 * time.Minute | ||
| ) | ||
|
|
||
| // Continuously refreshes the token of a key flow, retrying if the token API returns 5xx errrors. Writes to stderr when it terminates. | ||
| // | ||
| // To terminate this routine, close the context in keyFlow.config.BackgroundTokenRefreshContext. | ||
| func continuousRefreshToken(keyflow *KeyFlow) { | ||
| refresher := &continuousTokenRefresher{ | ||
| keyFlow: keyflow, | ||
| timeStartBeforeTokenExpiration: defaultTimeStartBeforeTokenExpiration, | ||
| timeBetweenContextCheck: defaultTimeBetweenContextCheck, | ||
| timeBetweenTries: defaultTimeBetweenTries, | ||
| } | ||
| err := refresher.continuousRefreshToken() | ||
| fmt.Fprintf(os.Stderr, "Token refreshing terminated: %v", err) | ||
| } | ||
|
|
||
| type continuousTokenRefresher struct { | ||
| keyFlow *KeyFlow | ||
| // Token refresh tries start at [Access token expiration timestamp] - [This duration] | ||
| timeStartBeforeTokenExpiration time.Duration | ||
| timeBetweenContextCheck time.Duration | ||
| timeBetweenTries time.Duration | ||
| } | ||
|
|
||
| // Continuously refreshes the token of a key flow, retrying if the token API returns 5xx errrors. Always returns with a non-nil error. | ||
| // | ||
| // To terminate this routine, close the context in refresher.keyFlow.config.BackgroundTokenRefreshContext. | ||
| func (refresher *continuousTokenRefresher) continuousRefreshToken() error { | ||
| expirationTimestamp, err := refresher.getAccessTokenExpirationTimestamp() | ||
| if err != nil { | ||
| return fmt.Errorf("get access token expiration timestamp: %w", err) | ||
| } | ||
| startRefreshTimestamp := expirationTimestamp.Add(-refresher.timeStartBeforeTokenExpiration) | ||
|
|
||
| for { | ||
| err = refresher.waitUntilTimestamp(startRefreshTimestamp) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err := refresher.keyFlow.config.BackgroundTokenRefreshContext.Err() | ||
| if err != nil { | ||
| return fmt.Errorf("check context: %w", err) | ||
| } | ||
|
|
||
| ok, err := refresher.refreshToken() | ||
| if err != nil { | ||
| return fmt.Errorf("refresh tokens: %w", err) | ||
| } | ||
| if !ok { | ||
| startRefreshTimestamp = startRefreshTimestamp.Add(refresher.timeBetweenTries) | ||
joaopalet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| continue | ||
| } | ||
|
|
||
| expirationTimestamp, err := refresher.getAccessTokenExpirationTimestamp() | ||
| if err != nil { | ||
| return fmt.Errorf("get access token expiration timestamp: %w", err) | ||
| } | ||
| startRefreshTimestamp = expirationTimestamp.Add(-refresher.timeStartBeforeTokenExpiration) | ||
| } | ||
| } | ||
|
|
||
| func (refresher *continuousTokenRefresher) getAccessTokenExpirationTimestamp() (*time.Time, error) { | ||
| token := refresher.keyFlow.token.AccessToken | ||
|
|
||
| // We can safely use ParseUnverified because we are not doing authentication of any kind | ||
| // We're just checking the expiration time | ||
| tokenParsed, _, err := jwt.NewParser().ParseUnverified(token, &jwt.RegisteredClaims{}) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("parse token: %w", err) | ||
| } | ||
| expirationTimestampNumeric, err := tokenParsed.Claims.GetExpirationTime() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("get expiration timestamp: %w", err) | ||
| } | ||
| return &expirationTimestampNumeric.Time, nil | ||
| } | ||
|
|
||
| func (refresher *continuousTokenRefresher) waitUntilTimestamp(timestamp time.Time) error { | ||
| for time.Now().Before(timestamp) { | ||
| err := refresher.keyFlow.config.BackgroundTokenRefreshContext.Err() | ||
| if err != nil { | ||
| return fmt.Errorf("check context: %w", err) | ||
| } | ||
| time.Sleep(refresher.timeBetweenContextCheck) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Returns: | ||
| // - (true, nil) if successful. | ||
| // - (false, nil) if not successful but should be retried. | ||
| // - (_, err) if not successful and shouldn't be retried. | ||
| func (refresher *continuousTokenRefresher) refreshToken() (bool, error) { | ||
| err := refresher.keyFlow.createAccessTokenWithRefreshToken() | ||
| if err == nil { | ||
| return true, nil | ||
| } | ||
|
|
||
| // Should be retried if this is an API error with status code 5xx | ||
| oapiErr := &oapierror.GenericOpenAPIError{} | ||
| if !errors.As(err, &oapiErr) { | ||
| return false, err | ||
| } | ||
| if oapiErr.StatusCode < 500 { | ||
| return false, err | ||
| } | ||
| return false, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.