# frozen_string_literal: true module Keycard # Mixin for conveniences in controllers. # # These methods depend on a `notary` method in your controller that returns a # configured {Keycard::Notary} instance. module ControllerMethods # The default session timeout is 24 hours, in seconds. DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 # Check whether the current request is authenticated as coming from a known # person or account. # # @return [Boolean] true if any of the {Notary}'s configured authentication # methods succeeds def logged_in? authentication.authenticated? end # Retrieve the user/account to which the current request is attributed. # # @return [User/Account] the user/account that has been authenticated; nil # if no one is logged in def current_user authentication.account end # Validate the session, resetting it if expired. # # This should be called as a before_action before {#authenticate!} when # working with session-based logins. It preserves a CSRF token, if present, # so login forms and the like will pass forgery protection. def validate_session csrf_token = session[:_csrf_token] elapsed = begin Time.now - Time.at(session[:timestamp] || 0) rescue StandardError session_timeout end reset_session if elapsed >= session_timeout session[:_csrf_token] = csrf_token session[:timestamp] = Time.now.to_i if session.key?(:timestamp) end # Require that some authentication method successfully identifies a user/account, # raising an exception if there is a failure for active credentials or no # applicable credentials are presented. # # @raise [AuthenticationFailed] if credentials for an attempted # authentication method are incorrect # @raise [AuthenticationRequired] if all authentication methods are skipped # and authentication could not be attempted # @return nil def authenticate! raise AuthenticationFailed if authentication.failed? raise AuthenticationRequired unless authentication.authenticated? end # Attempt to authenticate, optionally with user-supplied credentials, and # establish a session. # # @param credentials [Hash|kwargs] user-supplied credentials that will be # passed to each authentication method # @return [Boolean] whether the login attempt was successful def login(**credentials) authentication(credentials).authenticated?.tap do |success| setup_session if success end end # Log an account in without checking any credentials, starting a session. # # @param account [User|Account] the user/account object to consider current; # must have an #id property. def auto_login(account) request.env["keycard.authentication"] = notary.waive(account) setup_session end # Clear authentication status and terminate any open session. def logout request.env["keycard.authentication"] = notary.reject reset_session end private def authentication(**credentials) request.env["keycard.authentication"] ||= notary.authenticate(request, session, credentials) end # The session timeout, in seconds. Sessions will be cleared before any # further authentication unless there is a timestamp younger than this many # seconds old. The default is 24 hours. # # @return [Integer] session timeout, in seconds def session_timeout DEFAULT_SESSION_TIMEOUT end def setup_session return_url = session[:return_to] reset_session session[:return_to] = return_url session[:user_id] = current_user.id session[:timestamp] = Time.now.to_i end end end