Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passkeys support: Rename Devise::Strategies::Authenticatable => PasswordAuthenticatable #5531

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ pkg
log
test/tmp/*
gemfiles/*.lock
.DS_Store
1 change: 1 addition & 0 deletions lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Mailers

module Strategies
autoload :Base, 'devise/strategies/base'
autoload :PasswordAuthenticatable, 'devise/strategies/password_authenticatable'
autoload :Authenticatable, 'devise/strategies/authenticatable'
end

Expand Down
2 changes: 1 addition & 1 deletion lib/devise/models/database_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'devise/strategies/database_authenticatable'
require 'devise/strategies/database_password_authenticatable'

module Devise
module Models
Expand Down
2 changes: 1 addition & 1 deletion lib/devise/models/rememberable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'devise/strategies/rememberable'
require 'devise/strategies/password_rememberable'
require 'devise/hooks/rememberable'
require 'devise/hooks/forgetable'

Expand Down
179 changes: 8 additions & 171 deletions lib/devise/strategies/authenticatable.rb
Original file line number Diff line number Diff line change
@@ -1,178 +1,15 @@
# frozen_string_literal: true

require 'devise/strategies/base'
require 'devise/strategies/password_authenticatable'

module Devise
module Strategies
# This strategy should be used as basis for authentication strategies. It retrieves
# parameters both from params or from http authorization headers. See database_authenticatable
# for an example.
class Authenticatable < Base
attr_accessor :authentication_hash, :authentication_type, :password

def store?
super && !mapping.to.skip_session_storage.include?(authentication_type)
end

def valid?
valid_for_params_auth? || valid_for_http_auth?
end

# Override and set to false for things like OmniAuth that technically
# run through Authentication (user_set) very often, which would normally
# reset CSRF data in the session
def clean_up_csrf?
true
end

private

# Receives a resource and check if it is valid by calling valid_for_authentication?
# A block that will be triggered while validating can be optionally
# given as parameter. Check Devise::Models::Authenticatable.valid_for_authentication?
# for more information.
#
# In case the resource can't be validated, it will fail with the given
# unauthenticated_message.
def validate(resource, &block)
result = resource && resource.valid_for_authentication?(&block)

if result
true
else
if resource
fail!(resource.unauthenticated_message)
end
false
end
end

# Get values from params and set in the resource.
def remember_me(resource)
resource.remember_me = remember_me? if resource.respond_to?(:remember_me=)
end

# Should this resource be marked to be remembered?
def remember_me?
valid_params? && Devise::TRUE_VALUES.include?(params_auth_hash[:remember_me])
end

# Check if this is a valid strategy for http authentication by:
#
# * Validating if the model allows http authentication;
# * If any of the authorization headers were sent;
# * If all authentication keys are present;
#
def valid_for_http_auth?
http_authenticatable? && request.authorization && with_authentication_hash(:http_auth, http_auth_hash)
end

# Check if this is a valid strategy for params authentication by:
#
# * Validating if the model allows params authentication;
# * If the request hits the sessions controller through POST;
# * If the params[scope] returns a hash with credentials;
# * If all authentication keys are present;
#
def valid_for_params_auth?
params_authenticatable? && valid_params_request? &&
valid_params? && with_authentication_hash(:params_auth, params_auth_hash)
end

# Check if the model accepts this strategy as http authenticatable.
def http_authenticatable?
mapping.to.http_authenticatable?(authenticatable_name)
end

# Check if the model accepts this strategy as params authenticatable.
def params_authenticatable?
mapping.to.params_authenticatable?(authenticatable_name)
end

# Extract the appropriate subhash for authentication from params.
def params_auth_hash
params[scope]
end

# Extract a hash with attributes:values from the http params.
def http_auth_hash
keys = [http_authentication_key, :password]
Hash[*keys.zip(decode_credentials).flatten]
end

# By default, a request is valid if the controller set the proper env variable.
def valid_params_request?
!!env["devise.allow_params_authentication"]
end

# If the request is valid, finally check if params_auth_hash returns a hash.
def valid_params?
params_auth_hash.is_a?(Hash)
end

# Note: unlike `Model.valid_password?`, this method does not actually
# ensure that the password in the params matches the password stored in
# the database. It only checks if the password is *present*. Do not rely
# on this method for validating that a given password is correct.
def valid_password?
password.present?
end

# Helper to decode credentials from HTTP.
def decode_credentials
return [] unless request.authorization && request.authorization =~ /^Basic (.*)/mi
Base64.decode64($1).split(/:/, 2)
end

# Sets the authentication hash and the password from params_auth_hash or http_auth_hash.
def with_authentication_hash(auth_type, auth_values)
self.authentication_hash, self.authentication_type = {}, auth_type
self.password = auth_values[:password]

parse_authentication_key_values(auth_values, authentication_keys) &&
parse_authentication_key_values(request_values, request_keys)
end

def authentication_keys
@authentication_keys ||= mapping.to.authentication_keys
end

def http_authentication_key
@http_authentication_key ||= mapping.to.http_authentication_key || case authentication_keys
when Array then authentication_keys.first
when Hash then authentication_keys.keys.first
end
end

def request_keys
@request_keys ||= mapping.to.request_keys
end

def request_values
keys = request_keys.respond_to?(:keys) ? request_keys.keys : request_keys
values = keys.map { |k| self.request.send(k) }
Hash[keys.zip(values)]
end

def parse_authentication_key_values(hash, keys)
keys.each do |key, enforce|
value = hash[key].presence
if value
self.authentication_hash[key] = value
else
return false unless enforce == false
end
end
true
end

# Holds the authenticatable name for this class. Devise::Strategies::DatabaseAuthenticatable
# becomes simply :database.
def authenticatable_name
@authenticatable_name ||=
ActiveSupport::Inflector.underscore(self.class.name.split("::").last).
sub("_authenticatable", "").to_sym
end
class Authenticatable < PasswordAuthenticatable
ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
[Devise] `Devise::Strategies::Authenticatable` is deprecated and will be
removed in the next major version.
Use `Devise::Strategies::PasswordAuthenticatable` instead.
DEPRECATION
end
end
end
end
32 changes: 8 additions & 24 deletions lib/devise/strategies/database_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
# frozen_string_literal: true

require 'devise/strategies/authenticatable'
require 'devise/strategies/database_password_authenticatable'

module Devise
module Strategies
# Default strategy for signing in a user, based on their email and password in the database.
class DatabaseAuthenticatable < Authenticatable
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
hashed = false

if validate(resource){ hashed = true; resource.valid_password?(password) }
remember_me(resource)
resource.after_database_authentication
success!(resource)
end

# In paranoid mode, hash the password even when a resource doesn't exist for the given authentication key.
# This is necessary to prevent enumeration attacks - e.g. the request is faster when a resource doesn't
# exist in the database if the password hashing algorithm is not called.
mapping.to.new.password = password if !hashed && Devise.paranoid
unless resource
Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database)
end
end
class DatabaseAuthenticatable < DatabasePasswordAuthenticatable
ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
[Devise] `Devise::Strategies::DatabaseAuthenticatable` is deprecated and will be
removed in the next major version.
Use `Devise::Strategies::DatabasePasswordAuthenticatable` instead.
DEPRECATION
end
end
end

Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
end
31 changes: 31 additions & 0 deletions lib/devise/strategies/database_password_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require 'devise/strategies/password_authenticatable'

module Devise
module Strategies
# Default strategy for signing in a user, based on their email and password in the database.
class DatabasePasswordAuthenticatable < PasswordAuthenticatable
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
hashed = false

if validate(resource){ hashed = true; resource.valid_password?(password) }
remember_me(resource)
resource.after_database_authentication
success!(resource)
end

# In paranoid mode, hash the password even when a resource doesn't exist for the given authentication key.
# This is necessary to prevent enumeration attacks - e.g. the request is faster when a resource doesn't
# exist in the database if the password hashing algorithm is not called.
mapping.to.new.password = password if !hashed && Devise.paranoid
unless resource
Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database)
end
end
end
end
end

Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabasePasswordAuthenticatable)
Loading