From 700e26d7df70b4a56958951dcf24fc696c6ca41c Mon Sep 17 00:00:00 2001 From: Matchu Date: Thu, 3 Aug 2023 17:40:52 -0700 Subject: [PATCH] Remove old OpenNeo ID auth code This removes login/logout/session logic for integrating with OpenNeo ID, replacing them with stubs that just redirect to `/?TODO` when you click login, and helpers that act as if you're not logged in. This gives us a clean slate to plug in new Devise logic to integrate with the `openneo_id` database directly! --- Gemfile | 4 - Gemfile.lock | 5 - app/controllers/application_controller.rb | 22 +-- app/controllers/closet_hangers_controller.rb | 3 +- app/controllers/sessions_controller.rb | 38 ----- app/models/user.rb | 15 -- app/views/layouts/application.html.haml | 2 +- config/initializers/devise.rb | 146 ------------------ config/initializers/openneo_auth.rb | 15 -- config/locales/devise.en.yml | 39 ----- config/routes.rb | 8 +- lib/openneo-auth.rb | 64 -------- lib/openneo-auth/session.rb | 87 ----------- lib/openneo-auth/strategy.rb | 28 ---- vendor/cache/openneo-auth-signatory-0.1.0.gem | Bin 6656 -> 0 bytes vendor/cache/ruby-hmac-0.4.0.gem | Bin 7168 -> 0 bytes 16 files changed, 14 insertions(+), 462 deletions(-) delete mode 100644 app/controllers/sessions_controller.rb delete mode 100644 config/initializers/devise.rb delete mode 100644 config/initializers/openneo_auth.rb delete mode 100644 config/locales/devise.en.yml delete mode 100644 lib/openneo-auth.rb delete mode 100644 lib/openneo-auth/session.rb delete mode 100644 lib/openneo-auth/strategy.rb delete mode 100644 vendor/cache/openneo-auth-signatory-0.1.0.gem delete mode 100644 vendor/cache/ruby-hmac-0.4.0.gem diff --git a/Gemfile b/Gemfile index 224d72d4..658d3f5a 100644 --- a/Gemfile +++ b/Gemfile @@ -43,10 +43,6 @@ gem 'sanitize', '~> 6.0', '>= 6.0.2' # unstable version of RocketAMF interprets info registry as a hash instead of an array gem 'RocketAMF', :git => 'https://github.com/rubyamf/rocketamf.git' -# For working with the OpenNeo ID service. -gem 'msgpack', '~> 1.7', '>= 1.7.2' -gem 'openneo-auth-signatory', '~> 0.1.0' - # For preventing too many modeling attempts. gem 'rack-attack', '~> 6.7' diff --git a/Gemfile.lock b/Gemfile.lock index 5b98be4b..08ac44d6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -193,8 +193,6 @@ GEM nokogiri (1.15.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) - openneo-auth-signatory (0.1.0) - ruby-hmac orm_adapter (0.5.0) parallel (1.23.0) public_suffix (5.0.3) @@ -270,7 +268,6 @@ GEM rspec-expectations (~> 2.0.1) rspec-rails (2.0.1) rspec (~> 2.0.0) - ruby-hmac (0.4.0) rvm-capistrano (1.5.6) capistrano (~> 2.15.4) sanitize (6.0.2) @@ -331,10 +328,8 @@ DEPENDENCIES http_accept_language (~> 2.1, >= 2.1.1) letter_opener (~> 1.8, >= 1.8.1) memcache-client (~> 1.8.5) - msgpack (~> 1.7, >= 1.7.2) mysql2 (~> 0.5.5) nokogiri (~> 1.15, >= 1.15.3) - openneo-auth-signatory (~> 0.1.0) parallel (~> 1.23) rack-attack (~> 6.7) rails (= 7.0.6) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 725ab0bc..6af3fe15 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,12 +5,11 @@ class ApplicationController < ActionController::Base protect_from_forgery - helper_method :can_use_image_mode?, :user_is? + helper_method :current_user, :user_signed_in? before_action :set_locale - before_action :login_as_test_user if Rails.env.development? - def authenticate_user! # too lazy to change references to login_path + def authenticate_user! redirect_to(login_path) unless user_signed_in? end @@ -18,8 +17,12 @@ class ApplicationController < ActionController::Base raise AccessDenied unless user_signed_in? && current_user.id == params[:user_id].to_i end - def can_use_image_mode? - true + def current_user + nil # TODO + end + + def user_signed_in? + false # TODO end def infer_locale @@ -59,18 +62,9 @@ class ApplicationController < ActionController::Base def set_locale I18n.locale = infer_locale || I18n.default_locale end - - def user_is?(user) - user_signed_in? && user == current_user - end def valid_locale?(locale) locale && I18n.usable_locales.include?(locale.to_sym) end - - def login_as_test_user - test_user = User.find_by_name('test') - sign_in(:user, test_user, bypass: true) - end end diff --git a/app/controllers/closet_hangers_controller.rb b/app/controllers/closet_hangers_controller.rb index f42546a8..dfae42ab 100644 --- a/app/controllers/closet_hangers_controller.rb +++ b/app/controllers/closet_hangers_controller.rb @@ -29,7 +29,8 @@ class ClosetHangersController < ApplicationController end def index - @public_perspective = params.has_key?(:public) || !user_is?(@user) + is_user = user_signed_in? && current_user == @user + @public_perspective = params.has_key?(:public) || !is_user @perspective_user = current_user unless @public_perspective closet_lists = @user.closet_lists unless @perspective_user == @user diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb deleted file mode 100644 index ae733670..00000000 --- a/app/controllers/sessions_controller.rb +++ /dev/null @@ -1,38 +0,0 @@ -class SessionsController < ApplicationController - rescue_from Openneo::Auth::Session::InvalidSignature, :with => :invalid_signature - rescue_from Openneo::Auth::Session::MissingParam, :with => :missing_param - - before_action :initialize_session, :only => [new] - - skip_before_action :verify_authenticity_token, :only => [:create] - - def new - redirect_to Openneo::Auth.remote_auth_url(params, session) - end - - def create - session = Openneo::Auth::Session.from_params(params) - session.save! - render :text => 'Success' - end - - def destroy - sign_out(:user) - redirect_to (params[:return_to] || root_path) - end - - protected - - def initialize_session - session[:session_initialization_placeholder] = nil - end - - def invalid_signature(exception) - render :text => "Signature did not match. Check secret.", - :status => :unprocessable_entity - end - - def missing_param(exception) - render :text => exception.message, :status => :unprocessable_entity - end -end diff --git a/app/models/user.rb b/app/models/user.rb index e253e731..ac5ecf88 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,7 +1,6 @@ class User < ApplicationRecord include PrettyParam - DefaultAuthServerId = 1 PreviewTopContributorsCount = 3 has_many :closet_hangers @@ -23,8 +22,6 @@ class User < ApplicationRecord scope :top_contributors, -> { order('points DESC').where('points > 0') } - devise :rememberable - def admin? name == 'matchu' # you know that's right. end @@ -159,18 +156,6 @@ class User < ApplicationRecord contact_neopets_connection.try(:neopets_username) end - def self.find_or_create_from_remote_auth_data(user_data) - user = find_or_initialize_by_remote_id_and_auth_server_id( - user_data['id'], - DefaultAuthServerId - ) - if user.new_record? - user.name = user_data['name'] - user.save - end - user - end - def self.points_required_to_pass_top_contributor(offset) user = User.top_contributors.select(:points).limit(1).offset(offset).first user ? user.points : 0 diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f0259483..68a4b9fb 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -47,7 +47,7 @@ = userbar_contributions_summary(current_user) = link_to t('.userbar.items'), user_closet_hangers_path(current_user), :id => 'userbar-items-link' = link_to t('.userbar.outfits'), current_user_outfits_path - = link_to t('.userbar.settings'), Openneo::Auth.remote_settings_url + = link_to t('.userbar.settings'), auth_user_settings_path = link_to t('.userbar.logout'), logout_path_with_return_to - else = link_to login_path_with_return_to, :id => 'userbar-log-in' do diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb deleted file mode 100644 index 75ca3904..00000000 --- a/config/initializers/devise.rb +++ /dev/null @@ -1,146 +0,0 @@ -# Use this hook to configure devise mailer, warden hooks and so forth. The first -# four configuration values can also be set straight in your models. -Devise.setup do |config| - # ==> Mailer Configuration - # Configure the e-mail address which will be shown in DeviseMailer. - config.mailer_sender = "matchu@openneo.net" - - # Configure the class responsible to send e-mails. - # config.mailer = "Devise::Mailer" - - # ==> ORM configuration - # Load and configure the ORM. Supports :active_record (default) and - # :mongoid (bson_ext recommended) by default. Other ORMs may be - # available as additional gems. - require 'devise/orm/active_record' - - # ==> Configuration for any authentication mechanism - # Configure which keys are used when authenticating an user. By default is - # just :email. You can configure it to use [:username, :subdomain], so for - # authenticating an user, both parameters are required. Remember that those - # parameters are used only when authenticating and not when retrieving from - # session. If you need permissions, you should implement that in a before filter. - # config.authentication_keys = [ :email ] - - # Tell if authentication through request.params is enabled. True by default. - # config.params_authenticatable = true - - # Tell if authentication through HTTP Basic Auth is enabled. False by default. - # config.http_authenticatable = false - - # Set this to true to use Basic Auth for AJAX requests. True by default. - # config.http_authenticatable_on_xhr = true - - # The realm used in Http Basic Authentication - # config.http_authentication_realm = "Application" - - # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 10. If - # using other encryptors, it sets how many times you want the password re-encrypted. - # config.stretches = 10 - - # Define which will be the encryption algorithm. Devise also supports encryptors - # from others authentication tools as :clearance_sha1, :authlogic_sha512 (then - # you should set stretches above to 20 for default behavior) and :restful_authentication_sha1 - # (then you should set stretches to 10, and copy REST_AUTH_SITE_KEY to pepper) - # config.encryptor = :bcrypt - - # Setup a pepper to generate the encrypted password. - # config.pepper = "f6a7bb49e6d2348d529bf4c64c09af1491284e90087d282713825f09b8ac0d78be1d3e5fb65b4f95115da90a8b6be60a9d4da68ae60a6174a6c238976b52b848" - - # ==> Configuration for :confirmable - # The time you want to give your user to confirm his account. During this time - # he will be able to access your application without confirming. Default is nil. - # When confirm_within is zero, the user won't be able to sign in without confirming. - # You can use this to let your user access some features of your application - # without confirming the account, but blocking it after a certain period - # (ie 2 days). - # config.confirm_within = 2.days - - # ==> Configuration for :rememberable - # The time the user will be remembered without asking for credentials again. - # config.remember_for = 2.weeks - - # If true, a valid remember token can be re-used between multiple browsers. - # config.remember_across_browsers = true - - # If true, extends the user's remember period when remembered via cookie. - # config.extend_remember_period = false - - # ==> Configuration for :validatable - # Range for password length - # config.password_length = 6..20 - - # Regex to use to validate the email address - # config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i - - # ==> Configuration for :timeoutable - # The time you want to timeout the user session without activity. After this - # time the user will be asked for credentials again. - # config.timeout_in = 10.minutes - - # ==> Configuration for :lockable - # Defines which strategy will be used to lock an account. - # :failed_attempts = Locks an account after a number of failed attempts to sign in. - # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts - - # Defines which strategy will be used to unlock an account. - # :email = Sends an unlock link to the user email - # :time = Re-enables login after a certain amount of time (see :unlock_in below) - # :both = Enables both strategies - # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both - - # Number of authentication tries before locking an account if lock_strategy - # is failed attempts. - # config.maximum_attempts = 20 - - # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour - - # ==> Configuration for :token_authenticatable - # Defines name of the authentication token params key - # config.token_authentication_key = :auth_token - - # ==> Scopes configuration - # Turn scoped views on. Before rendering "sessions/new", it will first check for - # "users/sessions/new". It's turned off by default because it's slower if you - # are using only default views. - # config.scoped_views = true - - # Configure the default scope given to Warden. By default it's the first - # devise role declared in your routes. - # config.default_scope = :user - - # Configure sign_out behavior. - # By default sign_out is scoped (i.e. /users/sign_out affects only :user scope). - # In case of sign_out_all_scopes set to true any logout action will sign out all active scopes. - # config.sign_out_all_scopes = false - - # ==> Navigation configuration - # Lists the formats that should be treated as navigational. Formats like - # :html, should redirect to the sign in page when the user does not have - # access, but formats like :xml or :json, should return 401. - # If you have any extra navigational formats, like :iphone or :mobile, you - # should add them to the navigational formats lists. Default is [:html] - # config.navigational_formats = [:html, :iphone] - - # ==> Warden configuration - # If you want to use other strategies, that are not (yet) supported by Devise, - # you can configure them inside the config.warden block. The example below - # allows you to setup OAuth, using http://github.com/roman/warden_oauth - # - # config.warden do |manager| - # manager.oauth(:twitter) do |twitter| - # twitter.consumer_secret = - # twitter.consumer_key = - # twitter.options :site => 'http://twitter.com' - # end - # manager.default_strategies(:scope => :user).unshift :twitter_oauth - # end - - config.warden do |manager| - manager.default_strategies(:scope => :user).unshift(:openneo_auth_token) - end -end diff --git a/config/initializers/openneo_auth.rb b/config/initializers/openneo_auth.rb deleted file mode 100644 index 30128a23..00000000 --- a/config/initializers/openneo_auth.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'openneo-auth' - -Openneo::Auth.configure do |config| - config.app = ENV.fetch('OPENNEO_AUTH_APP') - config.auth_server = ENV.fetch('OPENNEO_AUTH_SERVER') - config.secret = ENV.fetch('OPENNEO_AUTH_SECRET') - - config.remote_auth_user_finder do |user_data| - User.find_or_create_from_remote_auth_data(user_data) - end - - config.remember_user_finder do |id| - User.find_by_id(id) - end -end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml deleted file mode 100644 index 5e4e4332..00000000 --- a/config/locales/devise.en.yml +++ /dev/null @@ -1,39 +0,0 @@ -en: - errors: - messages: - not_found: "not found" - already_confirmed: "was already confirmed" - not_locked: "was not locked" - - devise: - failure: - unauthenticated: 'You need to sign in or sign up before continuing.' - unconfirmed: 'You have to confirm your account before continuing.' - locked: 'Your account is locked.' - invalid: 'Invalid email or password.' - invalid_token: 'Invalid authentication token.' - timeout: 'Your session expired, please sign in again to continue.' - inactive: 'Your account was not activated yet.' - sessions: - signed_in: 'Signed in successfully.' - signed_out: 'Signed out successfully.' - passwords: - send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' - updated: 'Your password was changed successfully. You are now signed in.' - confirmations: - send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' - confirmed: 'Your account was successfully confirmed. You are now signed in.' - registrations: - signed_up: 'You have signed up successfully. If enabled, a confirmation was sent to your e-mail.' - updated: 'You updated your account successfully.' - destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' - unlocks: - send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' - unlocked: 'Your account was successfully unlocked. You are now signed in.' - mailer: - confirmation_instructions: - subject: 'Confirmation instructions' - reset_password_instructions: - subject: 'Reset password instructions' - unlock_instructions: - subject: 'Unlock Instructions' diff --git a/config/routes.rb b/config/routes.rb index 8584210c..7fae6cbe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,8 +7,6 @@ OpenneoImpressItems::Application.routes.draw do root :to => 'outfits#new' - devise_for :users - # DEPRECATED get '/bodies/:body_id/swf_assets.json' => 'swf_assets#index', :as => :body_swf_assets @@ -49,9 +47,9 @@ OpenneoImpressItems::Application.routes.draw do post '/pets/submit' => 'pets#submit', :method => :post get '/modeling' => 'pets#bulk', :as => :bulk_pets - get '/login' => 'sessions#new', :as => :login - get '/logout' => 'sessions#destroy', :as => :logout - post '/users/authorize' => 'sessions#create' + get '/login', to: redirect('/?TODO'), as: :login + get '/logout', to: redirect('/?TODO'), as: :logout + get '/auth-users/settings', to: redirect('/?TODO'), as: :auth_user_settings post '/locales/choose' => 'locales#choose', :as => :choose_locale diff --git a/lib/openneo-auth.rb b/lib/openneo-auth.rb deleted file mode 100644 index c1ebea7f..00000000 --- a/lib/openneo-auth.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'openneo-auth/session' -require 'openneo-auth/strategy' - -Warden::Strategies.add :openneo_auth_token, Openneo::Auth::Strategies::Token - -module Openneo - module Auth - class Config - attr_accessor :app, :auth_server, :secret - - def find_user_with_remote_auth(data) - raise "Must set a remote user finder for Openneo Auth to find a user" unless @remote_auth_user_finder - @remote_auth_user_finder.call(data) - end - - def find_user_by_remembering(id) - raise "Must set a remember user finder for Openneo Auth to find a user" unless @remember_user_finder - @remember_user_finder.call(id) - end - - def remote_auth_user_finder(&block) - @remote_auth_user_finder = block - end - - def remember_user_finder(&block) - @remember_user_finder = block - end - end - - class << self - def config - @@config ||= Config.new - end - - def configure(&block) - block.call(config) - end - - def remote_auth_url(params, session) - raise "Must set config.app to this app's subdomain" unless config.app - raise "Must set config.auth_server to remote server's hostname" unless config.auth_server - query = { - :app => config.app, - :session_id => session[:session_id], - :path => params[:return_to] || '/', - :from => params[:from] - }.to_query - uri = URI::HTTP.build({ - :host => config.auth_server, - :path => '/', - :query => query - }) - uri.to_s - end - - def remote_settings_url - URI::HTTP.build({ - :host => config.auth_server, - :path => '/users/edit' - }).to_s - end - end - end -end diff --git a/lib/openneo-auth/session.rb b/lib/openneo-auth/session.rb deleted file mode 100644 index 0b8011b8..00000000 --- a/lib/openneo-auth/session.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'active_support/core_ext/hash' -require 'msgpack' -require 'openneo-auth-signatory' - -module Openneo - module Auth - class Session - REMOTE_MSG_KEYS = %w(session_id source user) - TMP_STORAGE_DIR = Rails.root.join('tmp', 'openneo-auth-sessions') - - attr_writer :id - - def save! - content = +MessagePack.pack(@message) - FileUtils.mkdir_p TMP_STORAGE_DIR - File.open(tmp_storage_path, 'w') do |file| - file.write content - end - end - - def destroy! - File.delete(tmp_storage_path) - end - - def load_message! - raise NotFound, "Session #{id} not found" unless File.exists?(tmp_storage_path) - @message = File.open(tmp_storage_path, 'r') do |file| - MessagePack.unpack file.read - end - end - - def params=(params) - unless Auth.config.secret - raise "Must set config.secret to the remote auth server's secret" - end - given_signature = params['signature'] - secret = +Auth.config.secret - signatory = Auth::Signatory.new(secret) - REMOTE_MSG_KEYS.each do |key| - unless params.include?(key) - raise MissingParam, "Missing required param #{key.inspect}" - end - end - @message = params.slice(*REMOTE_MSG_KEYS) - correct_signature = signatory.sign(@message) - unless given_signature == correct_signature - raise InvalidSignature, "Signature (#{given_signature}) " + - "did not match message #{@message.inspect} (#{correct_signature})" - end - end - - def user - Auth.config.find_user_with_remote_auth(@message['user']) - end - - def self.from_params(params) - session = new - session.params = params - session - end - - def self.find(id) - session = new - session.id = id - session.load_message! - session - end - - private - - def id - @id ||= @message[:session_id] - end - - def tmp_storage_path - name = "#{id}.mpac" - File.join TMP_STORAGE_DIR, name - end - - class InvalidSession < ArgumentError;end - class InvalidSignature < InvalidSession;end - class MissingParam < InvalidSession;end - class NotFound < StandardError;end - end - end -end - diff --git a/lib/openneo-auth/strategy.rb b/lib/openneo-auth/strategy.rb deleted file mode 100644 index 688321a7..00000000 --- a/lib/openneo-auth/strategy.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'devise' - -module Openneo - module Auth - module Strategies - class Token < Devise::Strategies::Authenticatable - def valid? - session && session[:session_id] - end - - def authenticate! - begin - auth_session = Session.find session[:session_id] - rescue Session::NotFound => e - pass - else - auth_session.destroy! - success! auth_session.user - end - end - - def remember_me? - true - end - end - end - end -end diff --git a/vendor/cache/openneo-auth-signatory-0.1.0.gem b/vendor/cache/openneo-auth-signatory-0.1.0.gem deleted file mode 100644 index b6ac1f6db6bca7336d1af51875481360058a0d2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6656 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