golang / oauth2

Go OAuth2
https://golang.org/x/oauth2
BSD 3-Clause "New" or "Revised" License
5.24k stars 975 forks source link

Cache token / transport confusion #84

Open billmccord opened 9 years ago

billmccord commented 9 years ago

Hi, I'm having a really hard time figuring out how to accomplish what I want to do. Essentially, I would like to cache the access and refresh tokens so that I don't have to ask the user to authenticate every time I want to create a new client. It seems like the Transport may have been added for this purpose, but an example would make things much clearer. Thanks for the awesome library and any help you can provide!

billmccord commented 9 years ago

After digging into the code a bit more, it seems like perhaps when I'm finished using an OAuth2 Client and want to cache the token away for later reuse I could: 1) Get the Transport from the http.Client 2) Cast it to an oauth2.Transport 3) Get the TokenSource from the oauth2.Transport 4) Get the latest Token from the TokenSource 5) Save the Token Is this a recommended approach or is there an easier way to do this by injecting some kind of callback function that gets notified when a Token is refreshed?

jfcote87 commented 9 years ago

You could also just cache the Token from created from the Config.Exchange() func so that you only cache Token once because the refresh token never expires.

billmccord commented 9 years ago

Every time I refresh tokens on the OAuth library I'm using I get a new refresh token. Is that not standard? On Jan 31, 2015 1:48 AM, "Jim Cote" notifications@github.com wrote:

You could also just cache the Token from created from the Config.Exchange() func so that you only cache Token once because the refresh token never expires.

ā€” Reply to this email directly or view it on GitHub https://github.com/golang/oauth2/issues/84#issuecomment-72240828.

jfcote87 commented 9 years ago

Token.RefreshToken stays the same. Token.AccessToken changes.

See if the following gist explains it any better.

https://gist.github.com/jfcote87/89eca3032cd5f9705ba3

billmccord commented 9 years ago

Jim, thanks for posting the gist. That was helpful, but unless I'm misunderstanding something I think there are still cases where the token could be lost. Consider the following and let me know if this is a possible scenario: 1) I retrieve the token from my datastore and, while it is valid when I check it, it only has a few milliseconds left before expiration. 2) I create the new Client with the token. 3) As I'm using the Client, the token expires. 4) The RoundTrip method on the Client's Transport attempt to get the Token. 5) The reuseTokenSource determines that the token is invalid. 6) Now, the tokenRefresher's Token method is invoked. 7) retrieveToken is called and fetches a new token. 8) The Token is completely replaced (new AccessToken, RefreshToken, and Expiry.)

In this scenario, I would have no way of knowing the token was modified unless I followed my process above of recaching the token or, at the very least, verifying that the token didn't change while I was using the Client. This is because the Client automatically refreshes the Token for you as a convenience and, unfortunately, doesn't tell you that it did it via a callback.

Does that make sense or am I still missing something?

jfcote87 commented 9 years ago

The RefreshToken does not change. so you don't have to worry about the background refresh. The tokenRefresher struct stores the RefreshToken in the oldToken field and this field is not updated again.
https://github.com/golang/oauth2/blob/master/oauth2.go#L221-L227

*note that the RefreshToken field of the returned Token pointer is updated by the passed refresh token. The tokenRefresher is not updated by the returned Token. https://github.com/golang/oauth2/blob/master/oauth2.go#L345-L347

billmccord commented 9 years ago

Jim, unfortunately, this does not seem to be the case. I built a test based on the tests included with oauth2 here: https://gist.github.com/billmccord/4247b0c4d2a6b5a4d09f

You can see from my gist that if the token expires after being used to create an oauth2.Client, then another request will be made in the background to obtain a new token (new access_token and new refresh_token) before proceeding with the request.

Unfortunately, at this point the refresh_token is lost because the ORIGINAL_REFRESH_TOKEN is no longer valid after the NEW_REFRESH_TOKEN is generated according to the OAuth2 spec.

Therefore, it is possible that the token that I stored per your suggestion will become invalid and the only way I can see to ensure that I have the right tokens is to always get the Token again from the Client after use and ensure that a) it hasn't changed from what I have stored or b) update what I have stored with the changed Token I got from the Client.

jfcote87 commented 9 years ago

I think I see where we might be talking past each other and that an actual bug exists. I tested code using Google's client apis and their implementation of oauth2. According to Google's oauth documentation , refresh tokens do not expire but must be revoked. In my tests, a new refresh token is only generated during a code exchange (i.e. Config.Exchange() ). In a token refresh call (tokenRefresher.Token() ) the refresh_token field is omitted/left blank. This is allowed in the oauth2 spec as sending a new refresh_token during a refresh operation is optional (oauth2 doc section 1.5), not required.

You have found a bug. If a new refresh_token is generated during a refresh, the token source refresh token is not updated. See test code for an example.

Back to your original question of how to cache a Token each time it is refreshed. A hook would need to be added to Config and ReuseTokenSource().

billmccord commented 9 years ago

Thanks for validating this. The OAuth provider I'm calling uses a library that always generates a new refresh_token and there isn't an option to not do this yet. Since it is optional, I agree, that a hook would be necessary to get the updated Token in cases where it is renewed. Since that hook doesn't exist yet, I suppose that my workaround of getting the Token from the client when I'm finished using it is probably the only option?

e.g. // --- Obtain token from storage. --- // --- Create and use client with stored token. --- // Get latest token from client. newToken, err := client.Transport.(*oauth2.Transport).Source.Token() // --- Store revised token (if changed.) ---

It isn't pretty, but it seems like a reasonable workaround for now.

rakyll commented 9 years ago

Does the provider return a different refresh token each time you ask for a new access token?

billmccord commented 9 years ago

Yes. You can see here that issue_refresh_token is hard-coded to true for the refresh_token grant_type: https://github.com/FriendsOfSymfony/oauth2-php/blob/b0e57e17c84175a51af01cef7bbb2961261c84ad/lib/OAuth2.php#L840-L842

rakyll commented 9 years ago

Their API is not complaint with the OAuth 2.0 spec, and we have no intention to support provider-specific non-spec features.

billmccord commented 9 years ago

Two points: 1) I'm not clear on how it isn't compliant with the OAuth 2.0 spec when the spec specifically says in section 1.5 (emphasis, mine): " (H) The authorization server authenticates the client and validates the refresh token, and if valid issues a new access token (and optionally, a new refresh token)." 2) Haven't you already? https://github.com/golang/oauth2/blob/master/oauth2.go#L387-L395

jfcote87 commented 9 years ago

https://tools.ietf.org/html/rfc6749#section-6 Section 6. Refreshing an Access Token (last paragraph, emphasis in original)

The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token. The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client. If a new refresh token is issued, the refresh token scope MUST be identical to that of the refresh token included by the client in the request.

https://tools.ietf.org/html/rfc6749#section-10.4 Section 10.4. Refresh Tokens 4th paragraph

For example, the authorization server could employ refresh token rotation in which a new refresh token is issued with every access token refresh response. The previous refresh token is invalidated but retained by the authorization server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the authorization server of the breach.

billmccord commented 9 years ago

Jim, thanks, that seems to validate that the bug lies with this project because it doesn't provide a mechanism for obtaining the new refresh_token in situations where the refresh_token is replaced during the Refreshing an Access Token operation, correct?

If so, I can file a close this out and create a more specific issue OR we can just use this to track.

jfcote87 commented 9 years ago

Bill, I have a CL ready that fixes the refresh token problem and will upload later today.

@rakyll are you ok with calling this a bug? Should we start a new issue to discuss caching difficulties?

adg commented 9 years ago

@jfcote87 It looks like a bug to me. I think we should start a new issue to discuss caching strategies.

mgenov commented 9 years ago

@adg is there open issue about caching strategies?

adg commented 9 years ago

Not that I know of.

On 4 May 2015 at 04:53, Miroslav Genov notifications@github.com wrote:

@adg https://github.com/adg is there open issue about caching strategies?

ā€” Reply to this email directly or view it on GitHub https://github.com/golang/oauth2/issues/84#issuecomment-98522728.

johnl commented 8 years ago

For reference, there used to be support for token caching but it was removed for some reason: https://github.com/golang/oauth2/commit/93ad3f4a9ef21ece608bdf66c177b7573de9fcf7

abourget commented 8 years ago

So is this fixed, then ?

rakyll commented 8 years ago

@johnl, the trivial cache implementation is removed because we were not able to come up with a generic and useful cacher interface. I think a wrapper cacher RoundTripper is the perfect solution although it is not documented anywhere.

gebv commented 8 years ago

If the token has been restored, i can access a resource. But token is not auto-refreshed. Why?


sourceToken := oauth2.ReuseTokenSource(nil, &fileTokenSource{accessToken, refreshToken, expiry})

client := &http.Client{
    Transport: &oauth2.Transport{
        Base:   ContextTransport(oauth2.NoContext), // from internal/transport.go
        Source: oauth2.ReuseTokenSource(nil, sourceToken),
    },
}

client.Get("...")
// access the resource
// long time
// token expired
client.Get("...")
// and not auto-refreshed
gebv commented 8 years ago

When your mind is fresh... no problems Working code

sourceToken := oauth2.ReuseTokenSource(nil, &fileTokenSource{accessToken, refreshToken, expiry})
t, _ := sourceToken.Token()
client = conf.Client(oauth2.NoContext, t)
client.Get("...")
// token is auto-updated
lewgun commented 8 years ago

@rakyll I think a wrapper cacher RoundTripper is the perfect solution although it is not documented anywhere. is it here now ?

arvenil commented 8 years ago

Is there currently any workaround for this problem?

arvenil commented 8 years ago

Ok, I see bug with refreshing token got fixed: https://github.com/golang/oauth2/commit/cc2494a288f7645968af9c7293faf02e6371a377 right?

However, original question remains - what's currently proper way to save and restore token? Is there any kind of event when token get's changed? Any example how to achieve that?

arvenil commented 8 years ago

Below pseudo code seems to work. Is this a way to go?

    config := &oauth2.Config{
        ClientID: "App",
        ClientSecret: "It's not a secret",
        Endpoint: oauth2.Endpoint{
            TokenURL: apiUrl + "/token",
        },
    }
    restoredToken := &oauth2.Token{
        AccessToken: savedToken.AccessToken,
        RefreshToken: savedToken.RefreshToken,
        Expiry: savedToken.Expiry,
        TokenType: savedToken.TokenType,
    }
    tokenSource := config.TokenSource(oauth2.NoContext, restoredToken)
    client := oauth2.NewClient(oauth2.NoContext, tokenSource)
    savedToken, err = tokenSource.Token()
    if err != nil {
        return err
    }

    r, err  := client.Get("...")
    if err != nil {
        return err
    }
rakyll commented 8 years ago

@arvenil, a wrapper RoundTripper would look like what's below:

type cacherTransport struct {
    Base *oauth2.Transport
}

func (c *cacherTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
    tok, err := c.Base.Source.Token()
    if err != nil {
        return nil, err
    }
    resp, err = c.Base.RoundTrip(req)
    if err != nil {
        return nil, err
    }
    newTok, err := c.Base.Source.Token()
    if err != nil {
        // TODO: handle error
    }
    if tok.AccessToken != newTok.AccessToken {
        // TODO: cache the refreshed token
    }
    return resp, nil
}

func Example_tokenCache() {
    conf := &oauth2.Config{
        ClientID:     "YOUR_CLIENT_ID",
        ClientSecret: "YOUR_CLIENT_SECRET",
        Scopes:       []string{"SCOPE1", "SCOPE2"},
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://provider.com/o/oauth2/auth",
            TokenURL: "https://provider.com/o/oauth2/token",
        },
    }

    var tok *oauth2.Token
    ts := conf.TokenSource(oauth2.NoContext, tok)
    tr := &oauth2.Transport{Source: ts}

    client := &http.Client{
        Transport: &cacherTransport{Base: tr},
    }
    client.Get("...")
}
arvenil commented 8 years ago

Thank you very much @rakyll Before your example I've tried different approach and ended up with wrapper around TokenSource and that seems to work too.

I wonder if there is any advantage or disadvantage of any of those solutions - in any case if someone wants to experiment here goes the code:)

type TokenSaver interface {
    Save(*oauth2.Token) error
}

func newCachedTokenSource(src oauth2.TokenSource, ts TokenSaver) oauth2.TokenSource {
    return &cachedTokenSource{
        pts: src,
        ts:  ts,
    }
}

type cachedTokenSource struct {
    pts oauth2.TokenSource // called when t is expired.
    ts  TokenSaver
}

func (s *cachedTokenSource) Token() (*oauth2.Token, error) {
    t, err := s.pts.Token()
    if err != nil {
        return nil, err
    }
    if err := s.ts.Save(t); err != nil {
        return nil, err
    }
    return t, nil
}

// example stolen from @rakyll ;)
func Example_tokenCache() {
    conf := &oauth2.Config{
        ClientID:     "YOUR_CLIENT_ID",
        ClientSecret: "YOUR_CLIENT_SECRET",
        Scopes:       []string{"SCOPE1", "SCOPE2"},
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://provider.com/o/oauth2/auth",
            TokenURL: "https://provider.com/o/oauth2/token",
        },
    }

    var tok *oauth2.Token // nil or restored token
    ts := conf.TokenSource(oauth2.NoContext, tok)
    tokenSaver := newTokenSaver() // here goes actuall implementation of TokenSaver interface
    cts := newCachedTokenSource(ts, tokenSaver)
    client := oauth2.NewClient(oauth2.NoContext, cachedTokenSource)
    client.Get("...")
}
rakyll commented 8 years ago

@arvenil

It looks like it is a lot more readable to implement a race-free cachedTokenSource implementation, even though yours has a race condition. I shared the RoundTripper wrapper since it is mentioned by @lewgun, but it clearly needs to have the token guarded by a lock as well.

liquidmetal commented 8 years ago

Can someone please write a self-contained example of how to make use of ReuseTokenSource?

rakyll commented 8 years ago

@liquidmetal, is your question related to the caching example above? If it is not, could you please ask it on golang-nuts mailing list or other user question facing mediums where you can retrieve a faster response?

stapelberg commented 8 years ago

the trivial cache implementation is removed because we were not able to come up with a generic and useful cacher interface.

@rakyll, could you please elaborate on the specific issue(s) with the Cache interface (providing a Read and Write method) that was removed in https://github.com/golang/oauth2/commit/93ad3f4a9ef21ece608bdf66c177b7573de9fcf7? Which use-case(s) did it not cover?

Iā€™m honestly curious and interested in coming up with a solution that would work ā€” I think we could make the oauth2 library a tad easier to use, and token lifecycle management is the biggest pain point in how I use the library :).

Thanks!

hypnoglow commented 7 years ago

A simple way to cache refreshed token (for very simple use cases):

tokenSource := conf.TokenSource(oauth2.NoContext, token)
newToken, err := tokenSource.Token()
if err != nil {
    log.Fatalln(err)
}

if newToken.AccessToken != token.AccessToken {
    SaveToken(newToken)
    log.Println("Saved new token:", newToken.AccessToken)
}

client := oauth2.NewClient(oauth2.NoContext, tokenSource)
resp, err := client.Get(...)
jboverfelt commented 7 years ago

@rakyll I implemented your fix above in the thread where you define a custom transport in order to cache new tokens, and I think I'm missing something. Your first call to c.Base.Source.Token() will check if the current token is valid, and if it is expired, it will refresh it, returning the new token. It appears that unless the token expires in between the aforementioned first call to Token() and the call to c.Base.RoundTrip(req), the condition later will always be false. That is, in almost every case, the two calls to c.Base.Source.Token() will return the same result. Let me know if I'm off base here. My application had another point where I could save the refreshed token, so I was able to save it off without the use of a new Transport implementation.

bgentry commented 7 years ago

I might be missing something, but it seems like this would be sufficient:

// TokenNotifyFunc is a function that accepts an oauth2 Token upon refresh, and
// returns an error if it should not be used.
type TokenNotifyFunc func(*oauth2.Token) error

// NotifyingTokenSource is an oauth2.TokenSource that calls a function when a
// new token is obtained.
type NotifyingTokenSource struct {
    f   TokenNotifyFunc
    src oauth2.TokenSource
}

// NewNotifyingTokenSource creates a NotifyingTokenSource from an underlying src
// and calls f when a new token is obtained.
func NewNotifyingTokenSource(src oauth2.TokenSource, f TokenNotifyFunc) *NotifyingTokenSource {
    return &NotifyingTokenSource{f: f, src: src}
}

// Token fetches a new token from the underlying source.
func (s *NotifyingTokenSource) Token() (*oauth2.Token, error) {
    t, err := s.src.Token()
    if err != nil {
        return nil, err
    }
    if s.f == nil {
        return t, nil
    }
    return t, s.f(t)
}

Ideally this would be placed directly between the reuseTokenSource and the tokenRefresher that are composed by Config.TokenSource(). But because tokenRefresher is not exposed directly, the simplest hack to achieve this would be:

  f := func(t *oauth2.Token) error {
    _, err := fmt.Printf("got a new token: %#v\n", t)
    return err
  }
  config := &oauth2.Config{}
  realSource := config.TokenSource(context.Background(), savedToken)

  notifyingSrc := NewNotifyingTokenSource(realSource, f)
  client := oauth2.NewClient(ctx.Background(), notifyingSrc)
  // now use the client...

The result has the following layered TokenSources:

So that's an extra layer of mutex due to the dual reuseTokenSource, but not awful. That could be avoided by exposing the tokenRefresher more directly. I do believe this can & should be made easier for such a common task.

Did I get any of that wrong?

bgentry commented 7 years ago

After trying out what I suggested, there was one minor problem with it. My function f would get called to be notified about a new token on the very first use due to the fact that the top-level ReuseTokenSource (built by oauth2.NewClient()) is not provided with an initial token, and would therefore cascade all the way down to the realSource on first use.

To avoid that unnecessary call, I had to add yet another layer of ReuseTokenSource:

    f := func(t *oauth2.Token) error {
        _, err := fmt.Printf("got a new token: %#v\n", t)
        return err
    }
    config := &oauth2.Config{
        Endpoint: oauth2.Endpoint{TokenURL: apiBaseURL + "/sessions"},
    }

    realSource := config.TokenSource(ctx, savedToken)
    notifyingSrc := oauth2src.NewNotifyingTokenSource(realSource, f)
    notifyingWithInitialSrc := oauth2.ReuseTokenSource(savedToken, notifyingSrc)
    oauth2HTTPClient = oauth2.NewClient(ctx, notifyingWithInitialSrc)

At least ReuseTokenSource() is smart enough to avoid redundant mutexes when it's stacked directly on top of another reuseTokenSource šŸ¤·ā€ā™‚ļø

gam-phon commented 7 years ago

I am using @hypnoglow solution. Thanks.

In python, requests-oauthlib. Passing a function, with a token argument, to be run if the token got refreshed. Reference

       :token_updater: Method with one argument, token, to be used to update
                        your token databse on automatic token refresh. If not
                        set a TokenUpdated warning will be raised when a token
                        has been refreshed. This warning will carry the token
                        in its token argument.

Example

j0hnsmith commented 6 years ago

Here's my solution, it's mostly oauth2.ReuseTokenSource with @bgentry's notify func (also used to persist the initial token).

// TokenNotifyFunc is a function that accepts an oauth2 Token upon refresh, and
// returns an error if it should not be used.
type TokenNotifyFunc func(*oauth2.Token) error

// NotifyRefreshTokenSource is essentially `oauth2.ResuseTokenSource` with `TokenNotifyFunc` added.
type NotifyRefreshTokenSource struct {
    new oauth2.TokenSource
    mu  sync.Mutex // guards t
    t   *oauth2.Token
    f   TokenNotifyFunc // called when token refreshed so new refresh token can be persisted
}

func StoreNewToken(t *oauth2.Token) error {
    // persist token
    return nil // or error
}

// Token returns the current token if it's still valid, else will
// refresh the current token (using r.Context for HTTP client
// information) and return the new one.
func (s *NotifyRefreshTokenSource) Token() (*oauth2.Token, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.t.Valid() {
        fmt.Println("returning existing token")
        return s.t, nil
    }
    t, err := s.new.Token()
    if err != nil {
        return nil, err
    }
    s.t = t
    return t, s.f(t)
}

Usage

// initial token
tok, err := someOauth2Config.Exchange(ctx, code)
if err != nil {
    // do something
}
err = StoreNewToken(tok) // persist the initial token
if err != nil {
    // do something
}

...
// token from persistent store
tok, err := FetchTokenFromStorage(tokenKey)
nrts := &NotifyRefreshTokenSource{
    new: someOauth2Config.TokenSource(ctx, tok),
    t:   tok,
    f: StoreNewToken
}
client := oauth2.NewClient(ctx, srts)
resp, err := client.Get("...") // StoreNewToken() called whenever a new refresh token is obtained
jfcote87 commented 6 years ago

Perhaps a caching solution should not be part of the library, people will keep coming up with additional cases. Rather than hiding the refresh functionality, create a NewToken func on oauth.Config would allow developers to create their own caching tokensources.

func (c *Config) NewToken(ctx context.Context, tk *Token) (*Token, error) {
    if tk == nil || tk.RefreshToken == "" {
        return nil, errors.New("no refresh token")
    }    
    return retrieveToken(ctx, c, url.Values{
        "grant_type":    {"refresh_token"},
        "refresh_token": {tk.RefreshToken},
    })
}

Add the same functionality to jwt.Config and clientcredentials.Config. @j0hnsmith's example would then look more like the following which could be added to an caching_example_test.go file.

type TokenRefresher interface {
    NewToken(context.Context, *oauth.Token) (*oauth.Token, error) 
}

// NotifyRefreshTokenSource is essentially `oauth2.ResuseTokenSource` with `TokenNotifyFunc` added.
type NotifyRefreshTokenSource struct {
    new TokenRefresher
    mu  sync.Mutex // guards t
    t   *oauth2.Token
    f   func(context.Context, *oauth2.Token)  // called when token refreshed so new refresh token can be persisted
}

func StoreNewToken(ctx context.Context, t *oauth2.Token) error {
    // persist token
    return nil // or error
}

// Token returns the current token if it's still valid, else will
// refresh the current token (using r.Context for HTTP client
// information) and return the new one.
func (s *NotifyRefreshTokenSource) Token() (*oauth2.Token, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.t.Valid() {
        fmt.Println("returning existing token")
        return s.t, nil
    }
    t, err := s.new.Token()
    if err != nil {
        return nil, err
    }
    s.t = t
    return t, s.f(t)
}
Usage

to a caching_example_test.go.

rodrigodiez commented 6 years ago

Would be nice to see this implemented in the library. The use case is common enough as there are many providers that invalidate the refresh token once it is used

ThiefMaster commented 5 years ago

how is this still not part of the library?! :/

ibudiallo commented 4 years ago

For those who are still stuck, here is what I did to resolve the issue.

Before I attempt to make a web request and let the client automatically update the token (which I cannot retrieve), I check the expiry date myself:

ctx := context.Background() // reuse your context
token := fetchedFromDB()
conf := &oauth2.Config{}
if token.Expiry.Before(time.Now()) { // expired so let's update it
   src := conf.TokenSource(ctx, token)
   newToken, err := src.Token() // this actually goes and renews the tokens
   if err != nil {
     panic(err)
   }
   if newToken.AccessToken != token.AccessToken {
     SaveTheToken(newToken) // back to the database with new access and refresh token
     token = newToken
   }
}
client := config.Client(ctx, token)

Then you can use the client to make web requests.

lpar commented 3 years ago

@ibudiallo:

Before I attempt to make a web request and let the client automatically update the token (which I cannot retrieve), I check the expiry date myself:

While this is admirably simple, I realized it unfortunately has a failure mode:

  1. Your code to get a client is called.
  2. It fetches the cached token from the DB, and notes that the token hasn't expired and can be reused.
  3. Your code passes back a client to the caller, set to re-use the cached token.
  4. The caller makes a sequence of OAuth2 requests.
  5. Between two of those requests, the token expires.
  6. The client fetches a new token, and the server chooses to respond with a new refresh token as well and invalidate the previous one (which it's allowed to do, as per the spec).
  7. Your code never finds out about the new tokens, so they don't get written to cache.
  8. Next time your code is called to get a client, it returns a client that tries to use an invalid access token with an invalid refresh token, and fails.
gouthampc commented 2 years ago

@lpar in the same solution mentioned by @ibudiallo, how about saving the token with a TTL with little less time (say 5 minutes) than the expiry time, this way we're refreshing the token ahead of time.

ctx := context.Background() // reuse your context
token := fetchedFromDB()
conf := &oauth2.Config{}
if (token == nil) { // TTL has passed, so let's fetch the token update 
   src := conf.TokenSource(ctx, token)
   newToken, err := src.Token() // this actually goes and renews the tokens
   if err != nil {
     panic(err)
   }
   if newToken.AccessToken != token.AccessToken {
     ttl := token.Expiry.Sub(time.Now()) - 5 * time.Minute // TTL 5 minutes before actual token expiry.
     SaveTokenWithTTL(newToken, ttl) // back to the database with new access and refresh token
     token = newToken
   }
}
client := config.Client(ctx, token)
blueforesticarus commented 2 years ago

I came here to say, yes, this is very confusing.

It took a whole hour of searching just to figure out that CacheFile used to be supported but was removed. It is strange this isnt a solved issue since I expect it is a common usecase, I'd think that most of the time application developers would want to cache the token. Maybe there is something I don't understand about oauth2, but if so it is not obvious from this thread or this repo.

My understanding is: 1) the token issued in the authentication flow expires, after which it can be used to get a new token 2) this library does the refresh transparently under the hood. When the server say the token expired, the library gets a new one, and completes the request 3) once this happens, ie. the expired token is used to get a new one, the old token becomes invalid. When the new token expires, IT will be used to get a new one. You cannot use the same token for a refresh more than once. 4) Therefore if you write code to save the token, and the library refreshes the token later on, your saved token becomes useless. You need to save the new token.

Most of the solutions here seem to ignore the 4th point, or like @gouthampc above, require extra time based plumbing. The best solutions here seems to be from @j0hnsmith and @bgentry, although those are far from self explanatory.

I think either something needs to be added to the library to facilitate having a CacheFile that updates when the token is refreshed, or at least an example needs to be written for how to do it with existing plumbing.

himby commented 2 years ago

What about utilizing "golang.org/x/sync/singleflight" to prevent unnecessary renewal of access tokens? In my case I'm using a memory based cache to store tokens. keyed with a sessionId. Then I use the sessionId combined with existingToken.Expiry.Unix() as key in the singleflight query. It is important that the requestGroup is a pointer set from a global scope. Seems promising, but not very well tested.

...
if existingToken.Expiry.Before(time.Now().UTC()) {
    src := o.Config.TokenSource(o.ctx, existingToken)
    key := sessionId + strconv.FormatInt(existingToken.Expiry.Unix(), 10)
    res, err, shared := requestGroup.Do(key, func() (interface{}, error) {
        newToken, err := src.Token() // this actually goes and renews the tokens
        if err != nil {
            log.Debug(err)
        }

        if newToken != nil {
            if newToken.AccessToken != existingToken.AccessToken {
            existingToken = newToken
            }
        }
        ..
})
...
ellulpatrick commented 2 years ago

@lpar , @ibudiallo - You could set the Token.Expiry to 0 to disable the "auto-refresh", as per the documentation here: https://pkg.go.dev/golang.org/x/oauth2#Token

This means that then you can manage refreshing manually yourself. It defeats the purpose of auto-refresh functionality though. It really should be simpler than this, to save a refreshed token.

KoduIsGreat commented 2 years ago

I've read this thread, multiple times and am still confused about what, if any of these solutions actually work, or if there is a more documented way to solve this problem.