Files
fizzy/app/controllers/concerns/authentication.rb
T
Mike Dalessio 3399e45130 Introduce an "Identity" model to ease login
- New untenanted Identity and Membership models
- New `identity_token` cookie with path "/" holds state across tenants

We're not sure whether the untenanted database will be sqlite or
MySQL, and so I've been careful to minimize

- database reads, placing them behind etags and caching
- database writes, only writing when a new Session is created (login)

Note that we track two things in the identity_token cookie: a signed
id, and the updated_at for the underlying Identity object. This allows
us to effectively cache on the Identity without having to hit the
database, by using an Identity::Mock object that is compatible with
etag and cache methods.

The new integration test shows the desired user-facing behavior, which
is to make it easy to login without a tenanted URL and to jump between
tenants.

- the untenanted "login_help" page shows all linked memberships
- the jump menu shows all linked memberships (except the current)

Also introduced a utility script to populate existing employee
Identities, grouping accounts by email address.
2025-10-10 10:12:25 -04:00

110 lines
3.1 KiB
Ruby

module Authentication
extend ActiveSupport::Concern
included do
# Checking for tenant must happen first so we redirect before trying to access the db.
before_action :require_tenant
before_action :require_authentication
helper_method :authenticated?
etag { Current.session.id if authenticated? }
include LoginHelper
end
class_methods do
def require_unauthenticated_access(**options)
allow_unauthenticated_access **options
before_action :redirect_authenticated_user, **options
end
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
before_action :resume_session, **options
end
def require_untenanted_access(**options)
skip_before_action :require_tenant, **options
skip_before_action :require_authentication, **options
before_action :redirect_tenanted_request, **options
end
end
private
def authenticated?
Current.session.present?
end
def require_tenant
unless ApplicationRecord.current_tenant.present?
set_current_identity_token
render "sessions/login_menu"
end
end
def require_authentication
resume_session || request_authentication
end
def resume_session
if session = find_session_by_cookie
set_current_session session
end
end
def find_session_by_cookie
Session.find_signed(cookies.signed[:session_token])
end
def request_authentication(untenanted: false)
if ApplicationRecord.current_tenant.present?
session[:return_to_after_authenticating] = request.url
end
redirect_to_login_url
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def redirect_authenticated_user
redirect_to root_url if authenticated?
end
def redirect_tenanted_request
redirect_to root_url if ApplicationRecord.current_tenant
end
def start_new_session_for(user)
link_identity(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
set_current_session session
end
end
def link_identity(user)
token_value = cookies.signed[:identity_token]
token_identity = Identity.find_signed(token_value["id"]) if token_value.present?
identity = user.set_identity(token_identity)
cookies.signed.permanent[:identity_token] = { value: { "id" => identity.signed_id, "updated_at" => identity.updated_at }, httponly: true, same_site: :lax }
end
def set_current_identity_token
Current.identity_token = Identity::Mock.new(**cookies.signed[:identity_token])
end
def set_current_session(session)
logger.struct " Authorized User##{session.user.id}", authentication: { user: { id: session.user.id } }
Current.session = session
set_current_identity_token
cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax, path: Account.sole.slug }
end
def terminate_session
Current.session.destroy
cookies.delete(:session_token)
end
end