lib/kankri/simple_authenticator.rb in kankri-0.1.0 vs lib/kankri/simple_authenticator.rb in kankri-0.1.1
- old
+ new
@@ -4,75 +4,216 @@
# An object that takes in a user hash and authenticates users
#
# This object holds user data in memory, including passwords. It is thus
# not secure for mission-critical applications.
class SimpleAuthenticator
- # Makes hashing functions for users based on SHA256.
+ extend Forwardable
+
+ # Makes hashing functions for users based on SHA256
+ #
+ # @api public
+ # @example Create a set of hashing functions for a given set of usernames
+ # SimpleAuthenticator::sha256_hasher(['alf', 'roy', 'busby'])
+ #
+ # @param usernames [Array] A list of usernames to form the keys of the
+ # hashing table.
+ #
+ # @return [Hash] A hash mapping usernames to functions that will take
+ # passwords and return their hashed equivalent.
def self.sha256_hasher(usernames)
digest_hasher(usernames, Digest::SHA256)
end
- # Makes hashing functions for users based on a Digest implementation.
+ # Makes hashing functions for users based on a Digest implementation
+ #
+ # Each hashing function uses a random salt value, which is stored inside
+ # the function and unique to the username.
+ #
+ # @api public
+ # @example Create a set of hashing functions for a given set of usernames
+ # SimpleAuthenticator::digest_hasher(['joe', 'ron'], Digest::SHA256)
+ #
+ # @param usernames [Array] A list of usernames to form the keys of the
+ # hashing table.
+ # @param hasher [Digest] A Digest to use when hashing the user passwords
+ #
+ # @return [Hash] A hash mapping usernames to functions that will take
+ # passwords and return their hashed equivalent.
def self.digest_hasher(usernames, hasher)
Hash[
usernames.map do |username|
salt = SecureRandom.random_bytes
- [username, ->(password) { hasher.digest(password + salt) } ]
+ [username, ->(password) { hasher.digest(password + salt) }]
end
]
end
+ # Initialises the SimpleAuthenticator
+ #
+ # @api public
+ # @example Initialises the SimpleAuthenticator with a user hash.
+ # SimpleAuthenticator.new(
+ # admin: {
+ # password: 'hunter2',
+ # privileges: {
+ # foo: 'all',
+ # bar: ['abc', 'def', 'ghi'],
+ # baz: []
+ # }
+ # }
+ # )
+ # @example Initialises the SimpleAuthenticator with a custom hasher.
+ # SimpleAuthenticator.new(
+ # { admin: {
+ # password: 'hunter2',
+ # privileges: {
+ # foo: 'all',
+ # bar: ['abc', 'def', 'ghi'],
+ # baz: []
+ # }
+ # }
+ # }, hasher
+ # )
+ #
+ # @param users [String] A hash mapping usernames (which may be Strings or
+ # Symbols) to hashes containing a mapping from :password to the user's
+ # password, and from :privileges to a hash mapping privilege keys to
+ # privilege lists.
+ # @param hash_maker [Object] A callable that takes a list of usernames
+ # and returns a hash mapping the usernames to functions that hash
+ # passwords for those users. If nil, a sensible default hasher will be
+ # used.
def initialize(users, hash_maker = nil)
hash_maker ||= self.class.method(:sha256_hasher)
@users = users
@hashers = hash_maker.call(@users.keys)
@passwords = passwords
@privilege_sets = privilege_sets
end
+ # Attempts to authenticate with the given username and password
+ #
+ # This will fail with an AuthenticationFailure exception if the credentials
+ # are invalid.
+ #
+ # @api public
+ # @example Authenticates with a String username and password.
+ # auth.authenticate('joe_bloggs', 'hunter2')
+ # @example Authenticates with a Symbol username and String password.
+ # auth.authenticate(:joe_bloggs, 'hunter2')
+ #
+ # @param username [Object] The candidate username; this may be either a
+ # String or a Symbol, and will be normalised to a Symbol.
+ # @param username [Object] The candidate username; this may be either a
+ # String or a Symbol, and will be normalised to a String.
+ #
+ # @return [PrivilegeSet] The privilege set for the username
def authenticate(username, password)
auth_fail unless auth_ok?(username.intern, password.to_s)
privileges_for(username.intern)
end
private
- def privileges_for(username)
- @privilege_sets[username]
- end
+ # Returns the privilege set for the given username
+ #
+ # @api private
+ #
+ # @param username [Object] The username whose privilege set is sought.
+ #
+ # @return [PrivilegeSet] The privilege set for the username
+ def_delegator :@privilege_sets, :fetch, :privileges_for
# Creates a hash mapping username symbols to their password strings
+ #
+ # @api private
+ #
+ # @return [Hash] A hash mapping usernames to passwords
def passwords
transform_users do |name, entry|
plaintext = entry.fetch(:password).to_s
@hashers.fetch(name).call(plaintext)
end
end
# Creates a hash mapping username symbols to their privilege sets
+ #
+ # @api private
+ #
+ # @return [Hash] A hash mapping usernames to privilege sets
def privilege_sets
transform_users { |_, entry| PrivilegeSet.new(entry.fetch(:privileges)) }
end
+ # Creates a new Hash by modifying the entries of the user hash
+ #
+ # @api private
+ #
+ # @yieldparam name [Object] The username.
+ # @yieldparam entry [Hash] The user entry.
+ #
+ # @return [Hash] The hash mapping usernames to the yielded values.
def transform_users
Hash[@users.map { |name, entry| [name.intern, (yield name, entry)] }]
end
+ # Fails with an AuthenticationFailure exception
+ #
+ # @api private
+ #
+ # @return [void]
def auth_fail
fail(Kankri::AuthenticationFailure)
end
+ # Checks to see if given authentication credentials are OK
+ #
+ # @api private
+ #
+ # @param username [Object] The username of the authentication attempt.
+ # @param password [Object] The password of the authentication attempt.
+ #
+ # @return [Boolean] True if the authentication attempt fails; false
+ # otherwise.
def auth_ok?(username, password)
username_present?(username) && password_ok?(username, password)
end
+ # Checks to see if the username is in the system
+ #
+ # @api private
+ #
+ # @param username [Object] The username of the authentication attempt.
+ #
+ # @return [Boolean] True if the username exists in the authenticator;
+ # false otherwise.
def username_present?(username)
@hashers.key?(username) && @passwords.key?(username)
end
+ # Checks the password to see if it is correct for this username
+ #
+ # @api private
+ #
+ # @param username [Object] The username of the authentication attempt.
+ # @param password [Object] The password of the authentication attempt.
+ #
+ # @return [Boolean] True if the password is correct; false otherwise.
def password_ok?(username, password)
- hashed_pass = @hashers.fetch(username).call(password)
- PasswordCheck.new(username, hashed_pass, @passwords).ok?
+ hashed_pass = hashed_password(username, password)
+ PasswordCheck.check(username, hashed_pass, @passwords)
+ end
+
+ # Applies the user's hashing function to the candidate password
+ #
+ # @api private
+ #
+ # @param username [Object] The username of the authentication attempt.
+ # @param password [Object] The password of the authentication attempt.
+ #
+ # @return [Object] The hashed password.
+ def hashed_password(username, password)
+ @hashers.fetch(username).call(password)
end
end
end