devise_token_auth icon indicating copy to clipboard operation
devise_token_auth copied to clipboard

Support for "refresh_token"

Open epicmonkey opened this issue 10 years ago • 18 comments

I didn't find this in docs, is there refresh_token support?

   refresh_token
         OPTIONAL.  The refresh token used to obtain new access tokens
         using the same end-user access grant as described in
         Section 4.1.4.  The authorization server SHOULD NOT issue a
         refresh token when the access grant type is set to "none".

epicmonkey avatar Sep 15 '15 20:09 epicmonkey

this how I implemented refresh token using client

def refresh
    status = false
    if (uid = params['Uid']).present? &&
      (client = params['Client']).present?
      if (account = Account.find_by_uid(uid)).present?
        if account.tokens[client].present?
          response.headers.merge!(account.create_new_auth_token(client))
          status = true
        end
      end
    end
    if status
      render json: {data: account.token_validation_response}
    else
      head :unauthorized
    end
  end

@booleanbetrayal can we have something similar to this

chirag7jain avatar Sep 17 '15 15:09 chirag7jain

I also very much need refresh_token! I'm trying to implement my google_oauth2 strategy with google-api-client gem, but I need a refresh_token, and it seems like devise_token_auth is getting in the way at the point of mount_devise_token_auth_for ...

I have, for example,

mount_devise_token_auth_for 'User', at: 'auth', :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

And then:

class Users::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController def assign_provider_attributes(user, auth_hash) user.assign_attributes({ name: auth_hash['info']['name'], image: auth_hash['info']['image'], email: auth_hash['info']['email'], refresh_token: auth_hash['credentials']['refresh_token'] }) end end

But this does not save the refresh_token to the DB. Any help?

jcmccormick avatar Nov 28 '15 11:11 jcmccormick

I replaced the above assign_provider_attributes with code from the dummy app for "custom/omniauth_callbacks", but I get nil when trying to print the resource.

def omniauth_success super do |resource| pp resource # this returns nil @omniauth_success_block_called = true end end

def omniauth_success_block_called? @omniauth_success_block_called == true end

And when I try to pp resource in omniauth_success, I get nil in the console. I guess I don't understand the ordering of the processes that are going on. Shouldn't resource equate to something for me to use?

jcmccormick avatar Nov 28 '15 13:11 jcmccormick

I see my mistake after looking through DeviseTokenAuth::OmniauthCallbacksController...

I just needed to be using @resource, and I can access auth_hash['credentials'] as well.

jcmccormick avatar Nov 29 '15 05:11 jcmccormick

Here's a refresh token implementation as a separate controller, in case someone is interested:

controller class:

class TokenRefreshesController < DeviseTokenAuth::ApplicationController
  before_action :set_user_by_token, only: [:refresh_token]

  def refresh_token
    if @resource
      auth_header = @resource.create_new_auth_token(@client_id)
      response.headers.merge!(auth_header)

      render json: {
        data: resource_data(resource_json: @resource.token_validation_response)
      }
    else
      head :unauthorized
    end
  end
end

routes.rb:

get 'refresh_token', to: 'token_refreshes#refresh_token'

wangthony avatar Jun 12 '17 22:06 wangthony

Hey @wangthony

I tried your code for refresh token, but I'm getting unauthorized error.

Is it because I'm trying to refresh the auth token after the access-token has expired?

Also had some problem with routes first, but placing the route definition inside devise_scope :user block did the trick.

The headers I'm sending from my client application: client: 'xxxxx' access-token: 'xxxxxx' uid: 'xxxxx' 'token-type': 'Bearer' 'Accept': 'application/json'

Any help much appreciated, Thanks

alliesground avatar May 19 '18 00:05 alliesground

Is there any improvements?

hcyildirim avatar Aug 14 '18 15:08 hcyildirim

There is no such thing as a refresh token in devise token auth. However there is a secure mechanism that changes the token after every request. https://devise-token-auth.gitbook.io/devise-token-auth/security

In order to do that, you need to set change_headers_on_each_request to true. See https://devise-token-auth.gitbook.io/devise-token-auth/config/initialization#about-token-management

On your frontend, you will have to make sure that you replace your current token with the new one (if more recent). If you are using ember-simple-auth, let me know if you need assistance.

stephanebruckert avatar Aug 15 '18 10:08 stephanebruckert

@stephanebruckert Thank you but i need refresh token. I can't force my mobile users to logout and login again. It has to be persistent session. Users can close the application without waiting for a response from server. And if the app closes, I can not save token on the phone. What am I supposed to do in this situation?

hcyildirim avatar Aug 15 '18 10:08 hcyildirim

@ccoeder can you explain this into more details please I don't understand.

if the app closes, I can not save token on the phone

I don't see why you can't save a token on the phone? Also wouldn't it be the same issue with a refresh token?

stephanebruckert avatar Aug 15 '18 10:08 stephanebruckert

@stephanebruckert sorry for my bad english. Im using axios interceptors on react-native. see interceptors

instance.interceptors.response.use(
  async (response) => {
    if (response.headers['access-token']) {
      await onSignIn(response.headers);
    }

    return response;
  },
  error => Promise.reject(error),
);

If headers has access-token im saving it. But there is an issue with this solution. If the user closes the application before the interceptor is running, the code here does not work. The token on the server side has changed. But the client sends a request with the old token.

hcyildirim avatar Aug 15 '18 10:08 hcyildirim

Ha I see, basically send a request and immediately close the app before the response comes? Indeed that's an interesting case.

Did you try it or are you just assuming that it is going to cause an issue? We have been using this mechanism for months now in our backend but never had this issue on our iOS app.

I will try to reproduce it on my side and will let you know.

stephanebruckert avatar Aug 15 '18 13:08 stephanebruckert

Yes, some of my customers told me that they can't see anything in their screens. So i investigated and found they are getting 401 from server.

Thanks for answers by the way.

hcyildirim avatar Aug 15 '18 13:08 hcyildirim

@hcyildirim Did you ever find a solution to this issue? I tried to set-up my own solution just using JWT and refresh tokens, which worked quite well, but I found it difficult to integrate social login which led me back to devise_token_auth

jesster2k10 avatar Apr 09 '20 14:04 jesster2k10

@MaicolBen would the core team approve of this as an added feature to the gem? This would basically allow developers to choose between 2 different implementation styles:

  1. change_headers_on_each_request = true: the token refreshes on every request. This has know performance issues (#922) and flow issues (new tokens not returned on some errors), but it's quite secure!
  2. change_headers_on_each_request = false: this is less secure, but we can set the token_lifetime to a reasonable value to make it so. And with the addition of a "/auth/refresh_token" endpoint, the token can be refreshed before it expires.

iMacTia avatar May 25 '20 08:05 iMacTia

Yes, that doesn't seem like a braking change. Unfortunately, we need someone to submit a pull request for this feature, but I'm happy to review the code and approve it

MaicolBen avatar Dec 04 '20 15:12 MaicolBen

Here is an updated version considering refresh token only if the last token was provided in headers

  def refresh_token
    status = false
    if (uid = request.headers['uid']).present? && (client = request.headers['client']).present?
      if (user = User.find_by(uid: uid)).present?
        if user.tokens[client].present? && DeviseTokenAuth::Concerns::User.tokens_match?(user.tokens[client]['last_token'], request.headers['access-token'])
          headers = user.create_new_auth_token(client)

          response.set_header('access-token', headers['access-token'])
          response.set_header('expiry', headers['expiry'])

          status = true
        end
      end
    end
    if status
      render json: {data: user.token_validation_response}
    else
      head :unauthorized
    end
  end

heberuriegas avatar Jun 16 '21 16:06 heberuriegas

@heberuriegas Thanks for your snippet! I guess we should revoke last_token once a user sign out, to ensure that the token of signed out user would not be used on refreshing access token (if revoking a token is difficult, create a new one, which we do not send to client, for that purpose).

mickamy avatar May 15 '23 05:05 mickamy