Get refresh token and resend original request

I am using access- and refresh-token, and would like to send a request to get new tokens and then afterward send the original request, upon 401 error. I previous used cookies and a sliding session timeout, but switched to using tokens because of a switch in a package I use backend, and would like to let the user be logged in as long as they are active, but have them logged out quickly if they aren’t active.

I am using Elm 0.19 and the code is structured based on Elm-spa 1. I need a solution that lets me handle this one place, and not have to add it to evry submodule I have. From previous posts I have found that are related to this topic, the options seems to be

  1. Using tasks. This would require me to rewrite alot of the code to use Http.task instead of Http.require, and figure out how to write it in the Main.elm

  2. Check if the access token is close to expire, and if so, first ask for new tokens before sending the request. I think this might be simpler to implement, but I am not sure exactly how to do it since I also have to get the time and forward it etc.

Since I think using tokens is pretty normal, and using refresh tokens too, I feel there should be an existing solution that most people use, and that it shouldn’t be a unique problem. Given how much time I have spent on it the last week, I am starting to wonder if there is something important I have missed.

Any help will be greatly appreciated!

1 Like

Hi,

The way I do it, is to keep the refresh cycle separate from the actual requests. If a token is close to expiry, a refresh request is triggered on a timer. If a request is being made for some service, it just picks up whatever the current JWT token is at that time, regardless of whether it is just about to be refreshed or not. This should be a ok, because refreshing a JWT token gives a new token, but does not usually invalidate the current one - there can be a short period of time where >1 token is valid. The reason for this is that JWT tokens rely on digital signatures to prove them valid, allowing for scalable services that don’t constantly need to verify tokens against a central authority.

So I set a safe interval of either 30 seconds before expiry, or half the remaining expiry time, whichever is further in the future. The half of the remaining expiry time bit is just to deal with situations where a token might have a very short life, and 30 seconds is already beyond its expiry - mostly used for when I am testing and don’t want to wait around.

Refresh task can be found here:

If a service call were to fail with a 401 Forbidden because the token had expired - I would reset the auth state to logged out, and the application would use that as a trigger to take the user back to a login view. I would not bother to automatically re-try the request. The reason for this is that I expect my refresh strategy to work well enough to keep the token up to date, and so I expect this kind of thing to not happen very often, so no need to try and deal with it in a seamless way.

Like you, I started out thinking about how to make a more general mechanism to capture all HTTP requests and be able to re-play them, but I think this would be quite hard to do and to get playing nicely with the authentication logic, so tried approaching it in a different way.

4 Likes

At work I created a component that does just that. I call it a component because it incapsulates it’s own state and it’s own update function. Beside that it exposed an api to issue a request to be called from the update functions, and it exposes a Model to be embedded in every module that needs to send requests. The main logic is in the inner update, where it tries the request, look for a specific error (token expired), gets a new token and then issue the original request with the new token.

I would be happy to share the code, but it’s closed source, and it’s really tied to our implementation.

Anyway, I think that the “component” approach is the way to go.

I considered the approach suggested by @rupert but I didn’t want to put that configuration (the token expiration time) in the frontend.

The token expiry time comes on the JWT token and is chosen by the authentication back-end. The 30 seconds before bit, I did hard-code in this module, but could also be made a config option.

A change has to be made since the existing API doesn’t support this.

To minimise the problem, your new function can follow Http.request signature exactly but use Http.task internally. Then it’s a simpler search-and-replace to update your codebase?

Full disclosure, I’m not 100% happy with my solution to this, but I’d like to share my approach.

In my app, I use tasks for this kind of thing. (I find the distinction between Task and Cmd to be inconvenient and unhelpful, but Task has andThen and Cmd doesn’t, so Task it is.) My approach is:

  • Calls to my token-enabled backend don’t produce Task directly, but produce AuthRequiredEndpoint, which is basically an alias for Token -> Task.
  • I have a utility function AuthRequiredEndpoint -> Task which checks if we have a token and if the token is still valid. (I also use a 30-second margin of safety.) If so, use that; otherwise, request a new token (which produces a Task) and then andThen it with the actual request.
  • Getting a new token produces a token in addition to whatever the request was going to produce. I have a TokenUpdated Token Msg message constructor to handle this. Everyone agrees that having functions in messages is bad but I feel like having messages in messages is OK. To handle this message, we just update the “last retrieved token” in the model and then call update again with the underlying message.

Like I said above, I’m not completely in love with this solution. My biggest concern is around when getting a token fails. When this happens, we don’t have a failed response to offer to the underlying request. Some part of our application made a request and is expecting a response… do we just leave it waiting forever? Do we gin up a fictitious failure somehow? I’m not really sure. Also, the AuthRequiredEndpoint type makes defining functions to make requests a little clumsier than it has to be. Still, it works for now and I haven’t quite figured out anything better.

So, if I understand you correctly, a refresh token is asked for “for ever”, regardless of if the user is active or not? Or is the refresh token only asked for when a request is to be made, and it is e.g. less than 30 seconds until the token expire?

I would like the users to be logged out if not active for e.g. 30 minutes, which might be hard with your way?

When you log in you get an access token which might be valid for say 5 mins, and a refresh token which might be valid for say 30 mins. When the access token is 30 seconds away from running out, I use the refresh token to request another one. You may also get a new refresh token at that point too.

I have not implemented a feature to auto log-out if no activity in 30 minutes, but I think that could be done. Would need a timer that can be reset, and every update message that counts as user activity would reset it. When this timer runs out, use Auth.unauthed to clear the auth state on the client side - refresh token would still be valid but it got discarded so the client has no way to know it.

I am not sure if I understand completely what you mean, but even with using search-and-replace, it would probably give extra work to correct all the places that it for some reason didn’t work flawlessly.

I ended up using your method, where I refresh the token 30 seconds before it is about to expire, and then handle in the back-end if there is more than x minutes since the last request where the access token was used, and if so return unauthorized.

Thank you!

I think @rupert approach, which you adopted, is really most appropriate :bowing_man:


In any case, I’d missed out that Expect opaque parameter means I can’t

hence I can’t simply search Http.request and replace with httpStringRequest

 getCount : Cmd Msg
 getCount =
-    Http.request
+    httpStringRequest
         { method = "GET"
         , headers = []
         , url = "/api/count"
         , body = Http.emptyBody
-        , expect = Http.expectString OnCounted
         , timeout = Nothing
         , tracker = Nothing
+        , expect = Http.expectString
+        , tagger = OnCounted
         }

How do you handle this situation:

  1. Access token is refreshed. Let’s say it expires in 5 minutes, so a tokenExpiryTask task is scheduled in 4.5 minutes.
  2. Immediately after, the user suspends their computer for 20 minutes.
  3. User starts up their computer again and immediately begins interacting with the page.

The tokenExpiryTask task won’t run for another ~4.5 minutes since Process.sleep uses setTimeout under the covers which picks up after it left off, i.e., it doesn’t correct for time lapses. So in this case, every request will be unauthorized since an expired access token is being used which expired 15 minutes ago.

So the request is made with an expired token. It will come back as an HTTP 401.

My Auth API exposes some functions to help with this. unauthed clears any token from the model, and set the authentication state to LoggedOut. refresh make it attempt a refresh if it has a refresh token, and go into either the LoggedIn or LoggedOut states.

I generally just call unauthed to put the user back to the log in screen, and then they can manually retry the operation.

Calling refresh might be better, it should have the same effect as unauthed if there is no valid refresh token available - but I don’t think I coded it that way, it probably just tries whatever token it has regardless of whether it is expired, or does nothing if there is no token. Sounds like something I could impove.

Did not realise this. Perhaps a better way might be to set a timer for a short interval, maybe 1 second, and repeatedly check the expiry on that pulse.

Yeah, that’s what I do (check every second) as I find it the simplest approach. It does seem a bit wasteful though and I wonder about the impact on low powered devices. All these solutions have tradeoffs though.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.