-
Notifications
You must be signed in to change notification settings - Fork 3
The Common Authentication Base Module, Annotated
Jeff Dickey edited this page Mar 1, 2019
·
1 revision
This is an annotation of the code of a possible CommonAuthn
module, suitable for use by a Web app. It presumes that the Hanami [Controller Action Class|(https://guides.hanamirb.org/actions/overview/) (or CAC) which (indirectly) includes this module:
This module also makes use of a @redirect_url
instance variable, which will be defined if it does not already esist. Finally, it exposes a @current_user
variable from the including CAC.
NOTE that the annotations will be as comments beginning with NOTE:
. Other comments have (mostly) been removed from the in-use module from which this was created.
Without further ado...
# frozen_string_literal: true
require 'crypt_ident'
module Web
# @api private
# NOTE: The above is a [YARD](https://yardoc.org/) annotation which by default
# removes this module from the documentation generated for your app. (You
# *are* documenting your code, aren't you?)
module CommonAuthn
# NOTE: This method is called from the `self.included` method of the various
# modules (e.g., `ProhibitGuest`) which provide the high-level "drop-in"
# functionality you'd normally use in a CAC.
def self.setup_callbacks(other)
other.class_eval do
include CryptIdent
before :authenticate!
expose :current_user
end
end
private
# NOTE: An actual [halt](https://guides.hanamirb.org/actions/control-flow/#halt)
# is pretty abrupt for a Web app, though it's common in JSON-serving
# back-end API implementations. Instead, the usual practice is to simply
# expire the session (if any) and redirect back to the landing page or
# wherever else you've pointed `@redirect_url` to.
def authenticate!
expire_unless_authenticated
# Alternatively...
# halt_unless_authenticated
end
# NOTE: This is the core question of the module: is the User properly
# Authenticated and the session still unexpired?
def authenticated?
return false unless session_current_user_valid?
session_still_valid?
end
# NOTE: If you're feeling properly modular, you may want to break this
# method and the three methods it calls out into its own class, of which
# the current `#expire_session` need be the only public method. Having
# done so here, however, would complicate this annotation.
def expire_session
config = CryptIdent.config
update_expired_session_data(config)
flash_for_expiry(config)
redirect_for_session_expiry
end
# NOTE: One of the two candidate methods called from `#authenticate!` to
# verify that all is still as it should be with the Current User and the
# session-expiry data. (The other is `#halt_unless_authenticated`, below.)
def expire_unless_authenticated
expire_session unless authenticated?
end
# NOTE: Yes, this is a hard-coded, non-i18n message string. You can do
# better, but this gets the point across for the current purpose. Also
# note that we make use of the `:error_key` configuration value.
def flash_for_expiry(config)
error_message = 'Your session has expired. You have been signed out.'
flash[config.error_key] = error_message
end
# NOTE: One of the two candidate methods called from `#authenticate!` to
# verify that all is still as it should be with the Current User and the
# session-expiry data. (See also `#expire_unless_authenticated`, above.)
def halt_unless_authenticated
halt 401 unless authenticated?
end
# NOTE: Called from `#expire_session`, above. Does what it says on the tin.
def redirect_for_session_expiry
@redirect_url ||= routes.root_path
redirect_to @redirect_url
end
# NOTE: Also called from `expire_session`; setting the entries in the
# `session` Hash-like object is what tells us we don't have a Current User
# (if we'd had one previously). Note that `#update_session_expiry` is a
# method defined in the `CryptIdent` API itself; we haven't seen too many
# of those as yet.
def update_expired_session_data(config)
session[:current_user] = config.guest_user
updates = update_session_expiry(session)
session[:expires_at] = updates[:expires_at]
end
def session_current_user_valid?
# NOTE: Retrieving an Entity from session data? If you stored an Entity
# value, what you'll get back is a *Hash* of the Entity's attribute
# values. Probably not what you want, especially if you're going to
# immediately call a method on the purported object (to which `Hash`
# does not respond).
attrs = user_attrs_from_session
valid_current_user?(attrs).tap do |ret|
@current_user = User.new(attrs) if ret
end
end
# NOTE: If we get here, we're an Authenticated Member; has our session
# expired? If we don't have a `session[:expires_at]` value, that means
# this is the first time `#authenticated?` has been called since the
# Member successfully Authenticated. If the expiry timestamp has *not*
# passed, then we update it (restarting the countdown, as it were). Only
# if we had previously set the expiry timestamp *and* it has expired,
# will the method return false.
def session_still_valid?
updates = update_session_expiry(session)
return false if _session_expiry_check(session)
session[:expires_at] = updates[:expires_at] # update previously-set value
true
end
def _session_expiry_check(updates)
session[:expires_at] ||= updates[:expired_at] # initialise if not set
session_expired?(session)
end
# NOTE:We store a *Hash of attributes* in `session[:current_user]` rather
# than the object itself. That is because `Rack::Session` doesn't support
# arbitrary type serialisation/de-serialisation; it apparently predates
# [marshaling](https://ruby-doc.org/core-2.6.1/Marshal.html) in Ruby core.
def user_attrs_from_session
repo = CryptIdent.config.repository
session[:current_user] || repo.guest_user.to_h
end
end # module Web::CommonAuthn
end