From 112f04437673a6cc71532c5d7325b5d91b3a25c3 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Fri, 8 Nov 2024 12:56:15 +0100 Subject: [PATCH] Switch authentication to new rails 8.0 base --- app/controllers/application_controller.rb | 10 +-- app/controllers/concerns/authentication.rb | 71 ++++++++++++++++++++++ app/controllers/sessions_controller.rb | 28 +++++---- app/models/current.rb | 6 ++ app/views/sessions/new.html.haml | 12 ++-- config/application.rb | 1 + 6 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 app/controllers/concerns/authentication.rb create mode 100644 app/models/current.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 02c66d4..c65e796 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + include Authentication before_action :basic_profile_info # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. @@ -8,11 +9,6 @@ class ApplicationController < ActionController::Base protected - # @return [String,nil] The value of the authorization cookie - def authorization_header - cookies["Authorization"] - end - # Gets base information about a profile, things that are needed to show the profile info a of a logged-in user # @return [BasicProfileInfo] if the user is logged in # @return [nil] if the user is not logged in @@ -20,9 +16,9 @@ def basic_profile_info @basic_profile_info ||= retro_meet_client.basic_profile_info rescue RetroMeetClient::UnauthorizedError flash.now[:warn] = t("forced_log_out") - cookies.delete("Authorization") + terminate_session redirect_to :root end - def retro_meet_client = @retro_meet_client ||= RetroMeetClient.new(authorization_header) + def retro_meet_client = @retro_meet_client ||= RetroMeetClient.new(Current.session) end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 0000000..a6f36bf --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# This module can be included in controllers to enable authentication and related methods. +# It will automatically require authentication for all methods on that controller and any children controllers +module Authentication + extend ActiveSupport::Concern + + included do + before_action :require_authentication + helper_method :authenticated? + end + + class_methods do + # Method that allows some method in a controller to be unauthenticated. Should really only be used for root and sign_in/register actions + def allow_unauthenticated_access(**options) + skip_before_action :require_authentication, **options + end + end + + private + + # Check if there's a current session + def authenticated? + resume_session + end + + # Makes sure that if there's no session, it will require auth + def require_authentication + resume_session || request_authentication + end + + # Sets the current session from the cookie + def resume_session + Current.session ||= find_session_by_cookie + end + + # (renatolond, 2024-11-08) should it be returning something more than the authorization cookie? + # + # @return [String] + def find_session_by_cookie + cookies.signed[:authorization] + end + + # Sets the return url and redirects to login page + # @return [void] + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end + + # Will either return the url the user was at before, or the root + # + # @return [String] The url to go after the login + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end + + # Sets the cookie for the new session + # @return [void] + def start_new_session_for(authorization_token) + # TODO: check expiration time + cookies.signed[:authorization] = { value: authorization_token, httponly: true, same_site: :strict } + end + + # Logs out from retro meet core and removes the session cookie + # @return [void] + def terminate_session + retro_meet_client.sign_out + cookies.delete(:authorization) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 594d7a2..2fe1101 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,17 +2,20 @@ class SessionsController < ApplicationController layout "no_columns" + allow_unauthenticated_access only: %i[new create new_account create_account] + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } def new - redirect_to :root unless basic_profile_info.nil? + redirect_to :root if Current.session end def create - authorization_token = RetroMeetClient.login(login: params.dig(:sessions, :email), - password: params.dig(:sessions, :password)) - - cookies[:Authorization] = authorization_token - redirect_to :root + authorization_token = RetroMeetClient.login(login: params[:email], + password: params[:password]) + # user_agent: request.user_agent, + # ip_address: request.remote_ip) + start_new_session_for authorization_token + redirect_to after_authentication_url rescue RetroMeetClient::BadPasswordError flash.now[:error] = t(".bad_password") render "new", status: :unauthorized @@ -21,15 +24,17 @@ def create render "new", status: :unauthorized end - def new_account; end + def new_account + redirect_to :root if Current.session + end def create_account @email = params[:email] if params[:password] == params[:password_confirmation] authorization_token = RetroMeetClient.create_account(login: params[:email], password: params[:password]) - cookies[:Authorization] = authorization_token - redirect_to :root + start_new_session_for authorization_token + redirect_to after_authentication_url else flash.now[:error] = t(".passwords_do_not_match") render "new_account", status: :bad_request @@ -40,8 +45,7 @@ def create_account end def destroy - retro_meet_client.sign_out - cookies.delete(:Authorization) - redirect_to :root + terminate_session + redirect_to new_session_path end end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..39974e4 --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# This class keeps the current session that's used for authentication +class Current < ActiveSupport::CurrentAttributes + attribute :session +end diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 33d4795..e8553e7 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -4,16 +4,12 @@ %h3.title.has-text-grey= t('.sign_in') %p.subtitle.has-text-grey= t('.login_to_proceed') .box - = form_for(:sessions, url: session_path()) do |f| + = form_with url: session_path do |form| .field .control - = f.email_field :email, autofocus: true, autocomplete: 'email', placeholder: 'email@example.com', class: 'input is-large' + = form.email_field :email, autofocus: true, autocomplete: 'email', placeholder: 'email@example.com', class: 'input is-large' .field .control - = f.password_field :password, autocomplete: "current-password", placeholder: 'password', class: 'input is-large' - .field - = f.label :remember_me, class: 'checkbox' do - = f.check_box :remember_me - = t(".remember_me") - = f.submit t(".sign_in"), class: 'button is-block is-info is-large is-fullwidth' + = form.password_field :password, autocomplete: "current-password", placeholder: 'password', class: 'input is-large' + = form.submit t(".sign_in"), class: 'button is-block is-info is-large is-fullwidth' = render "sessions/shared/links" diff --git a/config/application.rb b/config/application.rb index 61ab9da..edbeb90 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,6 +28,7 @@ class Application < Rails::Application # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) + config.active_support.isolation_level = :fiber # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files