diff options
Diffstat (limited to 'internal/oauth/token.go')
| -rw-r--r-- | internal/oauth/token.go | 94 |
1 files changed, 88 insertions, 6 deletions
diff --git a/internal/oauth/token.go b/internal/oauth/token.go index 31c2c74..855677c 100644 --- a/internal/oauth/token.go +++ b/internal/oauth/token.go @@ -1,6 +1,12 @@ package oauth -import "time" +import ( + "fmt" + "sync" + "time" + + "github.com/go-errors/errors" +) // TokenResponse defines the OAuth response from the server that includes the tokens. type TokenResponse struct { @@ -17,8 +23,8 @@ type TokenResponse struct { Expires int64 `json:"expires_in"` } -// Token is a structure that contains our access and refresh tokens and a timestamp when they expire. -type Token struct { +// token is a structure that contains our access and refresh tokens and a timestamp when they expire. +type token struct { // Access is the access token returned by the server access string @@ -27,10 +33,86 @@ type Token struct { // ExpiredTimestamp is the Expires field but converted to a Go timestamp expiredTimestamp time.Time + + // Refresher is the function that refreshes the token + Refresher func(string) (*TokenResponse, time.Time, error) +} + +// tokenLock is a wrapper around token that protects it with a lock +type tokenLock struct { + // Protects t + mu sync.Mutex + + // The token fields protected by the lock + t *token +} + +// Access gets the OAuth access token used for contacting the server API +// It returns the access token as a string, possibly obtained fresh using the refresher +// If the token cannot be obtained, an error is returned and the token is an empty string. +func (l *tokenLock) Access() (string, error) { + l.mu.Lock() + defer l.mu.Unlock() + + // The tokens are not expired yet + // So they should be valid, re-login not neede + if !l.expired() { + return l.t.access, nil + } + + // Check if refresh is even possible by doing a simple check if the refresh token is empty + // This is not needed but reduces API calls to the server + if l.t.refresh == "" { + return "", errors.Wrap(&TokensInvalidError{Cause: "no refresh token is present"}, 0) + } + + // Otherwise refresh and then later return the access token if we are successful + tr, s, err := l.t.Refresher(l.t.refresh) + if err != nil { + // We have failed to ensure the tokens due to refresh not working + return "", errors.Wrap( + &TokensInvalidError{Cause: fmt.Sprintf("tokens failed refresh with error: %v", err)}, 0) + } + if tr == nil { + return "", errors.New("No token response after refreshing") + } + l.updateInternal(*tr, s) + return l.t.access, nil +} + +// Clear completely clears the token structure +// This is useful for forcing re-authorization +func (l *tokenLock) Clear() { + l.mu.Lock() + l.t = &token{} + l.mu.Unlock() +} + +// updateInternal updates the structure using the response without locking +func (l *tokenLock) updateInternal(r TokenResponse, s time.Time) { + l.t.access = r.Access + l.t.refresh = r.Refresh + l.t.expiredTimestamp = s.Add(time.Second * time.Duration(r.Expires)) +} + +// Update updates the structure usign the response and locks +func (l *tokenLock) Update(r TokenResponse, s time.Time) { + l.mu.Lock() + l.updateInternal(r, s) + l.mu.Unlock() +} + +// SetExpired overrides the timestamp to the current time +// This marks the tokens as expired +func (l *tokenLock) SetExpired() { + l.mu.Lock() + l.t.expiredTimestamp = time.Now() + l.mu.Unlock() } -// Expired checks if the access token is expired. -func (tokens *Token) Expired() bool { +// expired checks if the access token is expired. +// This is only called internally and thus does not lock +func (l *tokenLock) expired() bool { now := time.Now() - return !now.Before(tokens.expiredTimestamp) + return !now.Before(l.t.expiredTimestamp) } |
