lynndylanhurley / devise_token_auth

Token based authentication for Rails JSON APIs. Designed to work with jToker and ng-token-auth.
Do What The F*ck You Want To Public License
3.55k stars 1.13k forks source link

Support for "refresh_token" #377

Open epicmonkey opened 9 years ago

epicmonkey commented 9 years ago

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".
chirag7jain commented 9 years ago

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

jcmccormick commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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.

wangthony commented 7 years ago

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'
alliesground commented 6 years ago

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

hcyildirim commented 6 years ago

Is there any improvements?

stephanebruckert commented 6 years ago

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.

hcyildirim commented 6 years ago

@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?

stephanebruckert commented 6 years ago

@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?

hcyildirim commented 6 years ago

@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.

stephanebruckert commented 6 years ago

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.

hcyildirim commented 6 years ago

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.

jesster2k10 commented 4 years ago

@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

iMacTia commented 4 years ago

@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.
MaicolBen commented 3 years ago

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

heberuriegas commented 3 years ago

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
mickamy commented 1 year ago

@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).