From b9c6e912fdc91ec6c8236c61f262bca47424760b Mon Sep 17 00:00:00 2001 From: Bryan Terce Date: Mon, 24 Aug 2020 21:27:27 -0700 Subject: [PATCH 1/6] Install gems for testing and password validaiton --- Gemfile | 7 ++++++- Gemfile.lock | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 4bd47a3..3fc83b9 100644 --- a/Gemfile +++ b/Gemfile @@ -15,12 +15,13 @@ gem 'puma', '~> 4.1' gem 'grape' gem 'grape_on_rails_routes' +gem 'grape-route-helpers' gem 'grape-swagger' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password -# gem 'bcrypt', '~> 3.1.7' +gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' @@ -43,5 +44,9 @@ group :development do gem 'spring-watcher-listen', '~> 2.0.0' end +group :test do + gem 'factory_bot_rails' +end + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/Gemfile.lock b/Gemfile.lock index bfac3d9..3fb3228 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,6 +57,7 @@ GEM tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) ast (2.4.1) + bcrypt (3.1.15) bootsnap (1.4.8) msgpack (~> 1.0) builder (3.2.4) @@ -86,6 +87,11 @@ GEM dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) erubi (1.9.0) + factory_bot (6.1.0) + activesupport (>= 5.0.0) + factory_bot_rails (6.1.0) + factory_bot (~> 6.1.0) + railties (>= 5.0.0) ffi (1.13.1) globalid (0.4.2) activesupport (>= 4.2.0) @@ -96,6 +102,10 @@ GEM mustermann-grape (~> 1.0.0) rack (>= 1.3.0) rack-accept + grape-route-helpers (2.1.0) + activesupport + grape (>= 0.16.0) + rake grape-swagger (1.2.1) grape (~> 1.3) grape_on_rails_routes (0.3.2) @@ -213,9 +223,12 @@ PLATFORMS ruby DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap (>= 1.4.2) byebug + factory_bot_rails grape + grape-route-helpers grape-swagger grape_on_rails_routes listen (~> 3.2) From 302ad2c1c2226a2da2dc5f9ed6b556f69ab3236d Mon Sep 17 00:00:00 2001 From: Bryan Terce Date: Mon, 24 Aug 2020 21:29:43 -0700 Subject: [PATCH 2/6] Disable fixtures --- config/application.rb | 4 ++++ test/fixtures/.keep | 0 test/fixtures/files/.keep | 0 test/test_helper.rb | 3 --- 4 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 test/fixtures/.keep delete mode 100644 test/fixtures/files/.keep diff --git a/config/application.rb b/config/application.rb index 601e6f2..0d526f7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,5 +33,9 @@ class Application < Rails::Application # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true + + config.generators do |g| + g.test_framework :test_unit, fixture: false + end end end diff --git a/test/fixtures/.keep b/test/fixtures/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/test_helper.rb b/test/test_helper.rb index 1b7300e..73a23b4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,9 +7,6 @@ class TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all - # Add more helper methods to be used by all tests here... end end From e73b49f7488c7edbcf2cc187146254213e1c83d1 Mon Sep 17 00:00:00 2001 From: Bryan Terce Date: Mon, 24 Aug 2020 21:34:04 -0700 Subject: [PATCH 3/6] Create user model --- app/api/api_routes.rb | 5 ++++ app/models/user.rb | 10 ++++++++ db/migrate/20200824035326_create_users.rb | 11 ++++++++ db/schema.rb | 11 +++++++- db/seeds.rb | 12 ++++----- test/factories/user.rb | 9 +++++++ test/models/user_test.rb | 31 +++++++++++++++++++++++ test/test_helper.rb | 2 ++ 8 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20200824035326_create_users.rb create mode 100644 test/factories/user.rb create mode 100644 test/models/user_test.rb diff --git a/app/api/api_routes.rb b/app/api/api_routes.rb index e07fa6b..c027a7f 100644 --- a/app/api/api_routes.rb +++ b/app/api/api_routes.rb @@ -1,3 +1,8 @@ class APIRoutes < Grape::API + use ActionDispatch::Session::CookieStore, key: '_sbhacks_session' format :json + + helpers APIHelpers + + mount API::Auth end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..24262df --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,10 @@ +class User < ApplicationRecord + has_secure_password + + validates :name, presence: true + validates :email, format: { + with: URI::MailTo::EMAIL_REGEXP, + message: 'is not a valid email address' + } + validates :password, length: { in: 8..72 } +end diff --git a/db/migrate/20200824035326_create_users.rb b/db/migrate/20200824035326_create_users.rb new file mode 100644 index 0000000..5987b7c --- /dev/null +++ b/db/migrate/20200824035326_create_users.rb @@ -0,0 +1,11 @@ +class CreateUsers < ActiveRecord::Migration[6.0] + def change + create_table :users do |t| + t.string :name, nil: false + t.string :email, index: { unique: true }, nil: false + t.string :password_digest, nil: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b10373b..147138a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,18 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 2020_08_24_035326) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email" + t.string "password_digest" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["email"], name: "index_users_on_email", unique: true + end + end diff --git a/db/seeds.rb b/db/seeds.rb index 1beea2a..4468e03 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,5 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). -# -# Examples: -# -# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) +User.create!( + name: 'SBHacks Admin', + email: 'development@sbhacks.com', + password: 'password' +) diff --git a/test/factories/user.rb b/test/factories/user.rb new file mode 100644 index 0000000..7528474 --- /dev/null +++ b/test/factories/user.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :user do + name { 'Akshay Heda' } + sequence :email do |n| + "akshay.#{n}@example.com" + end + password { 'ilovesbhacks' } + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..fd81ae9 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,31 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + test 'factory is valid' do + assert_predicate build(:user), :valid? + end + + test 'requires a name' do + user = build(:user) + user.name = nil + assert_not_predicate user, :valid? + + user.name = '' + assert_not_predicate user, :valid? + end + + test 'requires a valid email address' do + user = build(:user) + user.email = 'not an email' + assert_not_predicate user, :valid? + end + + test 'requires a password' do + user = build(:user) + user.password = nil + assert_not_predicate user, :valid? + + user.password = 'short' + assert_not_predicate user, :valid? + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 73a23b4..8bf52e4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,6 +4,8 @@ module ActiveSupport class TestCase + include GrapeRouteHelpers::NamedRouteMatcher + include FactoryBot::Syntax::Methods # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) From 041b5ba905bf4e4b3cb1cd2a9c0ede145daa4a8f Mon Sep 17 00:00:00 2001 From: Bryan Terce Date: Mon, 24 Aug 2020 21:36:16 -0700 Subject: [PATCH 4/6] Create authentication endpoint --- .gitignore | 2 ++ app/api/api/auth.rb | 23 +++++++++++++++++++++++ app/api/api_helpers.rb | 7 +++++++ app/lib/auth/session_auth_middleware.rb | 17 +++++++++++++++++ config/credentials/development.yml.enc | 1 + config/credentials/production.yml.enc | 1 + config/initializers/cookies.rb | 3 +++ config/initializers/grape.rb | 5 +++++ config/initializers/session_store.rb | 3 +++ test/api/auth_test.rb | 17 +++++++++++++++++ test/test_helper.rb | 2 ++ test/test_helpers/sign_in_helper.rb | 9 +++++++++ 12 files changed, 90 insertions(+) create mode 100644 app/api/api/auth.rb create mode 100644 app/api/api_helpers.rb create mode 100644 app/lib/auth/session_auth_middleware.rb create mode 100644 config/credentials/development.yml.enc create mode 100644 config/credentials/production.yml.enc create mode 100644 config/initializers/cookies.rb create mode 100644 config/initializers/grape.rb create mode 100644 config/initializers/session_store.rb create mode 100644 test/api/auth_test.rb create mode 100644 test/test_helpers/sign_in_helper.rb diff --git a/.gitignore b/.gitignore index 8a1b113..b965990 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ # Ignore master key for decrypting credentials and more. /config/master.key +/config/credentials/development.key +/config/credentials/production.key diff --git a/app/api/api/auth.rb b/app/api/api/auth.rb new file mode 100644 index 0000000..6967e5b --- /dev/null +++ b/app/api/api/auth.rb @@ -0,0 +1,23 @@ +module API + class Auth < Grape::API + namespace :session do + desc 'Acquires a session cookie from a username and password' + params do + requires :username, type: String + requires :password, type: String + end + post do + session.destroy + + user = User.find_by(email: params[:username]) + + error! :not_found, 404 unless user.try(:authenticate, params[:password]) + + session[:user_id] = user.id + + status :ok + {} + end + end + end +end diff --git a/app/api/api_helpers.rb b/app/api/api_helpers.rb new file mode 100644 index 0000000..44131cf --- /dev/null +++ b/app/api/api_helpers.rb @@ -0,0 +1,7 @@ +module APIHelpers + extend Grape::API::Helpers + + def session + env['rack.session'] + end +end diff --git a/app/lib/auth/session_auth_middleware.rb b/app/lib/auth/session_auth_middleware.rb new file mode 100644 index 0000000..34d4ff4 --- /dev/null +++ b/app/lib/auth/session_auth_middleware.rb @@ -0,0 +1,17 @@ +module Auth + class SessionAuthMiddleware < Rack::Auth::AbstractHandler + def call(env) + session = env['rack.session'] + + return @app.call(env) if session[:user_id] + + unauthorized + end + + private + + def challenge + 'Session-Cookie' + end + end +end diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc new file mode 100644 index 0000000..3958a5f --- /dev/null +++ b/config/credentials/development.yml.enc @@ -0,0 +1 @@ +hAbLzw==--Izatsmxe7g0ccMzf--NlgK4PY2R7zLYMwSPx4Yeg== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc new file mode 100644 index 0000000..a2785b1 --- /dev/null +++ b/config/credentials/production.yml.enc @@ -0,0 +1 @@ +DClZHkmJZpADs4292n3axT+PHxN5YgAXk7CUqJEiQlW9+BfPJjeCH6IgLVI13ERsPqjE5HLwPwkewSbjw2pzwTDoZ6IbLbJFCCE5TSMnDvfOo8mjzaFoqMyFSouUN5BRinqJlbm7tLIuWauoH5UjE2LmWN6n7hr8iO6dchDzyWyhDJ+sH2xqaiDrlIgTeJGbxhSEOg4dHw==--I5PVRTkkRL16keFX--RS9Oq7j9pwZalWyU/Q26lw== \ No newline at end of file diff --git a/config/initializers/cookies.rb b/config/initializers/cookies.rb new file mode 100644 index 0000000..9aa8b20 --- /dev/null +++ b/config/initializers/cookies.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.middleware.use ActionDispatch::Cookies diff --git a/config/initializers/grape.rb b/config/initializers/grape.rb new file mode 100644 index 0000000..a5c09bb --- /dev/null +++ b/config/initializers/grape.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +Rails.configuration.to_prepare do + Grape::Middleware::Auth::Strategies.add(:from_session, Auth::SessionAuthMiddleware) +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100644 index 0000000..63d593a --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.middleware.use ActionDispatch::Session::CookieStore, key: '_sbhacks_session' diff --git a/test/api/auth_test.rb b/test/api/auth_test.rb new file mode 100644 index 0000000..a6174a7 --- /dev/null +++ b/test/api/auth_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' + +class AuthTest < ActionDispatch::IntegrationTest + test 'signing in sets the session id' do + user = sign_in_as(:user) + + assert_equal user.id, @request.session[:user_id] + end + + test 'signing in with invalid credentials clears the session' do + sign_in_as(:user) + + post session_path, params: { username: 'nonexistent user', password: '' } + + assert_nil @request.session[:user_id] + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8bf52e4..105832a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,11 +1,13 @@ ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' require 'rails/test_help' +require 'test_helpers/sign_in_helper' module ActiveSupport class TestCase include GrapeRouteHelpers::NamedRouteMatcher include FactoryBot::Syntax::Methods + include SignInHelper # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) diff --git a/test/test_helpers/sign_in_helper.rb b/test/test_helpers/sign_in_helper.rb new file mode 100644 index 0000000..675e7db --- /dev/null +++ b/test/test_helpers/sign_in_helper.rb @@ -0,0 +1,9 @@ +module SignInHelper + def sign_in_as(user) + user = create(user) if user.is_a? Symbol + + post session_path, params: { username: user.email, password: user.password } + + user + end +end From 0af05b6e98dd8f0b494604bd63c668c5e00e5185 Mon Sep 17 00:00:00 2001 From: Bryan Terce Date: Mon, 24 Aug 2020 21:36:27 -0700 Subject: [PATCH 5/6] Ignore deprecation warnings in tests --- config/environments/test.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index 0cb2424..8969127 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -3,6 +3,10 @@ # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! +# Tell Ruby to shut up about deprecation warnings during tests +# https://fuzzyblog.io/blog/rails/2020/01/28/turning-off-ruby-deprecation-warnings-when-running-tests.html +$VERBOSE = nil + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. From 493df17dec5417b5ad235a7f870714d55b8c330c Mon Sep 17 00:00:00 2001 From: Bryan Terce Date: Tue, 25 Aug 2020 12:07:05 -0700 Subject: [PATCH 6/6] Refactor auth middleware --- app/api/api/auth.rb | 3 ++- app/api/api_routes.rb | 4 ++-- app/lib/auth/session_auth_middleware.rb | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/api/api/auth.rb b/app/api/api/auth.rb index 6967e5b..781fdbc 100644 --- a/app/api/api/auth.rb +++ b/app/api/api/auth.rb @@ -6,7 +6,7 @@ class Auth < Grape::API requires :username, type: String requires :password, type: String end - post do + post auth: false do session.destroy user = User.find_by(email: params[:username]) @@ -14,6 +14,7 @@ class Auth < Grape::API error! :not_found, 404 unless user.try(:authenticate, params[:password]) session[:user_id] = user.id + cookies[:_csrf_token] = session[:csrf_token] = SecureRandom.base64(32) status :ok {} diff --git a/app/api/api_routes.rb b/app/api/api_routes.rb index c027a7f..fdd1444 100644 --- a/app/api/api_routes.rb +++ b/app/api/api_routes.rb @@ -1,8 +1,8 @@ class APIRoutes < Grape::API use ActionDispatch::Session::CookieStore, key: '_sbhacks_session' - format :json - helpers APIHelpers + format :json + auth :from_session mount API::Auth end diff --git a/app/lib/auth/session_auth_middleware.rb b/app/lib/auth/session_auth_middleware.rb index 34d4ff4..5684d1d 100644 --- a/app/lib/auth/session_auth_middleware.rb +++ b/app/lib/auth/session_auth_middleware.rb @@ -1,17 +1,23 @@ module Auth - class SessionAuthMiddleware < Rack::Auth::AbstractHandler + class SessionAuthMiddleware < Grape::Middleware::Auth::Base def call(env) - session = env['rack.session'] + self.env = env - return @app.call(env) if session[:user_id] + if context.route.options[:auth] != false + throw(:error, status: 401, message: 'Unauthorized') unless context.session[:user_id] + throw(:error, status: 403, message: 'Forbidden') unless verified_request? + end - unauthorized + @app.call(env) end private - def challenge - 'Session-Cookie' + def verified_request? + return true if Rails.env.development? && env['HTTP_X_IGNORE_CSRF'] + + method = context.route.options[:method] + method == 'GET' || method == 'HEAD' || context.session[:csrf_token] == env['HTTP_X_CSRF_TOKEN'] end end end