yep, I understand. Storing them separately IS the problem. When you do that you introduce the race condition. Storing them together avoids that.
I think there are probably specific scenarios that you want to allow, specific sequences of the use of a token.
- lookup token based on …a fixed key “upstream-accesstoken” . Maybe you have multiple well-known fixed keys associated to different upstream systems.
- in case of cache hit. token is valid/not expired. - ok proceed, use the token. Nothing further to do.
- in case of cache miss: token is expired. lookup the refresh token with a fixed key “upstream-refreshtoken”.
- in case of cache hit for refresh token. Use the refresh token to get a new access token. and then, cache the access token using the fixed key (upstream-accesstoken). If there is a new refresh token, cache the NEW refresh token using the appropriate fixed key. (At some point you will need a new refresh token, and often when getting a new access token you ALSO get a new refresh token. That’s ideal.) Usually there is a need to use the old (expired) access token with the refresh token in the refresh_token flow; that means you must cache both the access token and the refresh token together.
- refresh token cache miss - you need to go through the original grant flow. In all cases when you get a token, you must cache the refresh+access token PAIR together in the same cache entry when you cache the refresh token.
But there’s something odd - a system-to-system call is usually client_credentials or similar, in which case there is no refresh token flow. When the access token expires you go through another client_credentials flow. I don’t understand why there is a refresh token anyway. It’s Apigee to some upstream system. so why the refresh token? Refresh tokens are used for human flows - like auth code, or Password flow, to eliminate the involvement of the human keying in a password again. Refresh token is out of place for a system-to-system scenario, which THIS IS. So what’s going on? Why are you trying to solve this anyway?
Let’s think about the race: With no concurrency, (a single transaction), you will have this ordering:
- Tx1 → writecache “accesstoken” with accesstoken1
- Tx1 → writecache “refreshtoken” with refreshtoken1,accesstoken1
This is the simple case. There is no race. End state of cache: key(accesstoken) => value(accesstoken1). key(refreshtoken)=>value(refreshtoken1,accesstoken1)
What if there are two transactions and the updates to the cache get interleaved?
- Tx1 → writecache “accesstoken” with accesstoken1
- Tx2 → writecache “accesstoken” with accesstoken2
- Tx2 → writecache “refreshtoken” with refreshtoken2,accesstoken2
- Tx1 → writecache “refreshtoken” with refreshtoken1,accesstoken1
End state of cache: key(accesstoken) => value(accesstoken2). key(refreshtoken)=>value(refreshtoken1,accesstoken1)
At some later point, the accesstoken will expiry. Cache will get evicted. Logic will be: cache miss, so retrieve “refreshtoken”. Get (refreshtoken1,accesstoken1). That is a matched pair. You can use that for refresh flow.
If there are 5 transactions getting tokens (a race) , then it should not matter. Last write to the cache wins. The last tx to complete will write its refresh+access token to the cache with the key “upstream-refreshtoken” . And that will always be a consistent pair.
As I said, caching the items separately is what gets you into trouble. If you cache them together you avoid the problem.
Maybe there is some other constraint I am not considering - like you can only have one active access token at a time, or only one active refresh token at a time. Maybe the upstream invalidates all existing access tokens when it issues a new access token. IF that’s the case then you cannot solve that with Apigee alone. You need an external service that can serialize all access and make sure there is only ONE TRUE access token at any one time.
good luck