require 'devise/strategies/authenticatable' require 'tiddle/model_name' require 'tiddle/token_issuer' module Devise module Strategies class TokenAuthenticatable < Authenticatable def authenticate! env["devise.skip_trackable"] = true resource = mapping.to.find_for_authentication(authentication_keys_from_headers) return fail(:invalid_token) unless resource token = Tiddle::TokenIssuer.build.find_token(resource, token_from_headers) if token && unexpired?(token) touch_token(token) return success!(resource) end fail(:invalid_token) end def valid? authentication_keys_from_headers.present? && token_from_headers.present? end def store? false end # Avoid CSRF clean up for token authentication as it might trigger session creation in API # environments even if CSRF prevention is not being used. # Devise provides a `clean_up_csrf_token_on_authentication` option but it's not always viable # in applications with multiple user models and authentication strategies. def clean_up_csrf? false end private def authentication_keys_from_headers authentication_keys.map do |key| { key => env["HTTP_X_#{model_name}_#{key.upcase}"] } end.reduce(:merge) end def token_from_headers env["HTTP_X_#{model_name}_TOKEN"] end def model_name Tiddle::ModelName.new.with_underscores(mapping.to) end def authentication_keys mapping.to.authentication_keys end def touch_token(token) return unless token.last_used_at < touch_token_interval(token).ago token.update_attribute(:last_used_at, Time.current) end def unexpired?(token) return true if expiration_disabled?(token) Time.current <= token.last_used_at + token.expires_in end def touch_token_interval(token) return 1.hour if expiration_disabled?(token) || token.expires_in >= 24.hours return 5.minutes if token.expires_in >= 1.hour 1.minute end def expiration_disabled?(token) !token.respond_to?(:expires_in) || token.expires_in.blank? || token.expires_in.zero? end end end end Warden::Strategies.add(:token_authenticatable, Devise::Strategies::TokenAuthenticatable)