Skip to content

Commit

Permalink
get code from pull/8
Browse files Browse the repository at this point in the history
  • Loading branch information
cs0511 committed Jul 31, 2024
1 parent 812711e commit 016b464
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 13 deletions.
41 changes: 38 additions & 3 deletions app/controllers/devise_token_auth/concerns/set_user_by_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,41 @@ def set_user_by_token(mapping = nil)
end
end

def set_user_by_refresh_token
# determine target authentication class
rc = User

uid_name = DeviseTokenAuth.headers_names[:'uid']
refresh_token_name = DeviseTokenAuth.headers_names[:'refresh-token']
client_name = DeviseTokenAuth.headers_names[:'client']

# parse header for values necessary for authentication
uid = request.headers[uid_name] || params[uid_name]
@token = DeviseTokenAuth::TokenFactory.new unless @token
@token.refresh_token ||= request.headers[refresh_token_name] || params[refresh_token_name]
@token.client ||= request.headers[client_name] || params[client_name]

# client_id isn't required, set to 'default' if absent
@token.client ||= 'default'

# mitigate timing attacks by finding by uid instead of auth token
user = uid && rc.dta_find_by(uid: uid)

if user && user.valid_refresh_token?(@token.refresh_token, @token.client)
# sign_in with bypass: true will be deprecated in the next version of Devise
if self.respond_to? :bypass_sign_in
bypass_sign_in(user, scope: :user)
else
sign_in(:user, user, store: false, bypass: true)
end
return @resource = user
else
# zero all values previously set values
@token.client = nil
return @resource = nil
end
end

def update_auth_header
# cannot save object if model has invalid params
return unless @resource && @token.client
Expand All @@ -111,11 +146,11 @@ def update_auth_header
# cleared by sign out in the meantime
return if @resource.reload.tokens[@token.client].nil?

auth_header = @resource.build_auth_headers(@token.token, @token.client)
auth_header = @resource.build_auth_headers(@token.token, @token.client, @token.refresh_token)

# update the response header
response.headers.merge!(auth_header)

# set a server cookie if configured
if DeviseTokenAuth.cookie_enabled
set_cookie(auth_header)
Expand Down Expand Up @@ -181,7 +216,7 @@ def auth_header_from_batch_request
# extend expiration of batch buffer to account for the duration of
# this request
if @is_batch_request
auth_header = @resource.extend_batch_buffer(@token.token, @token.client)
auth_header = @resource.extend_batch_buffer(@token.token, @token.client, @token.refresh_token)

# Do not return token for batch requests to avoid invalidated
# tokens returned to the client in case of race conditions.
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/devise_token_auth/token_validations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module DeviseTokenAuth
class TokenValidationsController < DeviseTokenAuth::ApplicationController
skip_before_action :assert_is_devise_resource!, only: [:validate_token]
before_action :set_user_by_token, only: [:validate_token]
before_action :set_user_by_refresh_token, :only => [:refresh_token]

def validate_token
# @resource will have been set by set_user_by_token concern
Expand All @@ -15,6 +16,16 @@ def validate_token
end
end

def refresh_token
if @resource
@auth_header = @resource.create_new_auth_token(@token.client)

render_validate_token_success
else
render_validate_token_error
end
end

protected

def render_validate_token_success
Expand Down
43 changes: 35 additions & 8 deletions app/models/devise_token_auth/concerns/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ def create_token(client: nil, lifespan: nil, cost: nil, **token_extras)

tokens[token.client] = {
token: token.token_hash,
expiry: token.expiry
expiry: token.expiry,
refresh_token: token.refresh_token_hash
}.merge!(token_extras)

clean_old_tokens
Expand All @@ -112,6 +113,14 @@ def valid_token?(token, client = 'default')
false
end

def valid_refresh_token?(refresh_token, client = 'default')
return false unless tokens[client]
return true if refresh_token_is_current?(refresh_token, client)

# return false if none of the above conditions are met
return false
end

# this must be done from the controller so that additional params
# can be passed on from the client
def send_confirmation_notification?; false; end
Expand Down Expand Up @@ -140,6 +149,23 @@ def token_is_current?(token, client)
)
end

def refresh_token_is_current?(refresh_token, client)
# ghetto HashWithIndifferentAccess
expiry = tokens[client]['expiry'] || tokens[client][:expiry]
refresh_token_hash = tokens[client]['refresh_token'] || tokens[client][:refresh_token]

return true if (
# ensure that expiry and token are set
expiry && refresh_token_hash &&

# ensure that the token has not yet expired
DateTime.strptime(expiry.to_s, '%s') > Time.zone.now &&

# ensure that the token is valid
DeviseTokenAuth::Concerns::User.tokens_match?(refresh_token_hash, refresh_token)
)
end

# check if the hash of received token matches the stored token
def does_token_match?(token_hash, token)
return false if token_hash.nil?
Expand Down Expand Up @@ -176,10 +202,10 @@ def create_new_auth_token(client = nil)
updated_at: now
)

update_auth_headers(token.token, token.client)
update_auth_header(token.token, token.client, token.refresh_token)
end

def build_auth_headers(token, client = 'default')
def build_auth_headers(token, client = 'default', refresh_token = nil)
# client may use expiry to prevent validation request if expired
# must be cast as string or headers will break
expiry = tokens[client]['expiry'] || tokens[client][:expiry]
Expand All @@ -188,7 +214,8 @@ def build_auth_headers(token, client = 'default')
DeviseTokenAuth.headers_names[:"token-type"] => 'Bearer',
DeviseTokenAuth.headers_names[:"client"] => client,
DeviseTokenAuth.headers_names[:"expiry"] => expiry.to_s,
DeviseTokenAuth.headers_names[:"uid"] => uid
DeviseTokenAuth.headers_names[:"uid"] => uid,
DeviseTokenAuth.headers_names[:"refresh-token"] => refresh_token
}
headers.merge(build_bearer_token(headers))
end
Expand All @@ -201,8 +228,8 @@ def build_bearer_token(auth)
{ DeviseTokenAuth.headers_names[:"authorization"] => bearer_token }
end

def update_auth_headers(token, client = 'default')
headers = build_auth_headers(token, client)
def update_auth_headers(token, client = 'default', refresh_token = nil)
headers = build_auth_headers(token, client, refresh_token)
clean_old_tokens
save!

Expand All @@ -216,9 +243,9 @@ def build_auth_url(base_url, args)
DeviseTokenAuth::Url.generate(base_url, args)
end

def extend_batch_buffer(token, client)
def extend_batch_buffer(token, client, refresh_token = nil)
tokens[client]['updated_at'] = Time.zone.now
update_auth_headers(token, client)
update_auth_header(token, client, refresh_token)
end

def confirmed?
Expand Down
1 change: 1 addition & 0 deletions lib/devise_token_auth/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Engine < ::Rails::Engine
self.default_callbacks = true
self.headers_names = { 'authorization': 'Authorization',
'access-token': 'access-token',
'refresh-token': 'refresh-token',
'client': 'client',
'expiry': 'expiry',
'uid': 'uid',
Expand Down
1 change: 1 addition & 0 deletions lib/devise_token_auth/rails/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def mount_devise_token_auth_for(resource, opts)
devise_scope mapping_name.to_sym do
# path to verify token validity
get "#{full_path}/validate_token", controller: token_validations_ctrl.to_s, action: 'validate_token' if !opts[:skip].include?(:token_validations)
post "#{full_path}/refresh_token", controller: token_validations_ctrl.to_s, action: "refresh_token"

# omniauth routes. only define if omniauth is installed and not skipped.
if defined?(::OmniAuth) && !opts[:skip].include?(:omniauth_callbacks)
Expand Down
6 changes: 4 additions & 2 deletions lib/devise_token_auth/token_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ def self.create(client: nil, lifespan: nil, cost: nil)
obj_token = token
obj_token_hash = token_hash(obj_token, cost)
obj_expiry = expiry(lifespan)
obj_refresh_token = token
obj_refresh_token_hash = token_hash(obj_refresh_token, cost)

Token.new(obj_client, obj_token, obj_token_hash, obj_expiry)
Token.new(obj_client, obj_token, obj_token_hash, obj_expiry, obj_refresh_token, obj_refresh_token_hash)
end

# Generates a random URL-safe client.
Expand Down Expand Up @@ -102,7 +104,7 @@ def self.new
Token.new
end

Token = Struct.new(:client, :token, :token_hash, :expiry) do
Token = Struct.new(:client, :token, :token_hash, :expiry, :refresh_token, :refresh_token_hash) do
# Sets all instance variables of the token to nil. It is faster than creating new empty token.
# Example:
# token.clear!
Expand Down

0 comments on commit 016b464

Please sign in to comment.