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