From 9b947a6d138077b83df6855eca79722ecccd446f Mon Sep 17 00:00:00 2001 From: Lee Ma Date: Tue, 3 Dec 2019 22:01:07 -0500 Subject: [PATCH] Add devise auth endpoints to authenticate and revoke JWTs, also add environment variable gem (#28) * controllers and jwt setup for devise * finish up devise setup * lint * add dotenv gem and seed user data * linting fixes * remove require auth from questions * change revocation strategy to self * add .env setup to readme * add comments to session controller * linting sessions controller comments * add to readme how to protect a resource --- .env.development.local.template | 4 ++++ .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 5 +++++ README.md | 17 ++++++++++++++- app/controllers/application_controller.rb | 21 +++++++++++++++++++ app/controllers/sessions_controller.rb | 19 +++++++++++++++++ app/models/user.rb | 4 +++- config/application.rb | 2 ++ config/initializers/devise.rb | 13 +++++++++++- config/routes.rb | 11 +++++++++- .../20191126021858_jti_matcher_revocation.rb | 8 +++++++ db/schema.rb | 4 +++- db/seeds.rb | 5 +++++ 14 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .env.development.local.template create mode 100644 app/controllers/sessions_controller.rb create mode 100644 db/migrate/20191126021858_jti_matcher_revocation.rb diff --git a/.env.development.local.template b/.env.development.local.template new file mode 100644 index 0000000..a9ded10 --- /dev/null +++ b/.env.development.local.template @@ -0,0 +1,4 @@ +CORS_ORIGIN= +SEED_USER_EMAIL= +SEED_USER_PASSWORD= +DEVISE_JWT_SECRET_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3018829..99e7654 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ # Ignore master key for decrypting credentials and more. /config/master.key +.env.development.local \ No newline at end of file diff --git a/Gemfile b/Gemfile index 19340f8..678b45a 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: %i[mri mingw x64_mingw] gem 'database_cleaner' + gem 'dotenv-rails' gem 'factory_bot_rails' gem 'rspec-rails', '~> 3.5' end diff --git a/Gemfile.lock b/Gemfile.lock index 6d4e970..b57bf72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,6 +75,10 @@ GEM devise (~> 4.0) warden-jwt_auth (~> 0.3.6) diff-lcs (1.3) + dotenv (2.7.5) + dotenv-rails (2.7.5) + dotenv (= 2.7.5) + railties (>= 3.2, < 6.1) dry-auto_inject (0.6.1) dry-container (>= 0.3.4) dry-configurable (0.9.0) @@ -226,6 +230,7 @@ DEPENDENCIES database_cleaner devise devise-jwt (~> 0.5.9) + dotenv-rails factory_bot_rails listen (>= 3.0.5, < 3.2) pg (~> 1.1) diff --git a/README.md b/README.md index 9197ac8..f1a80e9 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,21 @@ SELECT * FROM questions; \d+ questions ``` +**Environment Variable Setup** +``` +# remove .template from .env.development.local.template and fill in .env.development.local, example values: +CORS_ORIGIN=http://localhost:3000 +SEED_USER_EMAIL=test@test.com +SEED_USER_PASSWORD=password +DEVISE_JWT_SECRET_KEY=super_secret_secret_key +``` + +**Authenticating Routes with Devise** +``` +# This project includes Devise for auth, add this line before the resource to require a user to be logged in: +before_action :authenticate_user! +``` + **Run the dev server** ``` @@ -95,4 +110,4 @@ If you get an error during dependency installation containing `can't find gem bu gem update --system bundle install ``` -This worked with Ruby 2.5.1 with `rbenv` on MacOS. \ No newline at end of file +This worked with Ruby 2.5.1 with `rbenv` on MacOS. diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 13c271f..40e0d43 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,25 @@ # frozen_string_literal: true class ApplicationController < ActionController::API + respond_to :json + def render_resource(resource) + if resource.errors.empty? + render json: resource + else + validation_error(resource) + end + end + + def validation_error(resource) + render json: { + errors: [ + { + status: '400', + title: 'Bad Request', + detail: resource.errors, + code: '100' + } + ] + }, status: :bad_request + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..ecb5b84 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class SessionsController < Devise::SessionsController + respond_to :json + + private + + # this returns a response with the session info in json format + # including the jti, user, and timestamps + def respond_with(resource, _opts = {}) + render json: resource + end + + # upon logout, this is the handler that will handle what to send + # as response, currently just sets header to 200 + def respond_to_on_destroy + head :ok + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 5f45909..add9314 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class User < ApplicationRecord - devise :database_authenticatable, :registerable, :validatable + include Devise::JWT::RevocationStrategies::JTIMatcher + devise :database_authenticatable, :validatable, :rememberable, + :jwt_authenticatable, jwt_revocation_strategy: self end diff --git a/config/application.rb b/config/application.rb index 8618bf2..0b084d3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,6 +21,8 @@ # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) +Dotenv::Railtie.load + module SdcApi class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index eeaedf6..ea614ae 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -40,7 +40,7 @@ # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] + config.authentication_keys = [:email] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -296,4 +296,15 @@ # When set to false, does not sign a user in automatically after their password is # changed. Defaults to true, so a user is signed in automatically after changing a password. # config.sign_in_after_change_password = true + config.jwt do |jwt| + jwt.secret = ENV['DEVISE_JWT_SECRET_KEY'] + jwt.dispatch_requests = [ + ['POST', %r{^/login$}] + ] + jwt.revocation_requests = [ + ['DELETE', %r{^/logout$}] + ] + jwt.expiration_time = 240.hours.to_i + jwt.request_formats = { api_user: [:json] } + end end diff --git a/config/routes.rb b/config/routes.rb index 126e5d1..bce477f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,16 @@ # frozen_string_literal: true Rails.application.routes.draw do - devise_for :users + devise_for :users, + path: '', + path_names: { + sign_in: 'login', + sign_out: 'logout' + }, + controllers: { + sessions: 'sessions' + }, + defaults: { format: :json } get '/questions', to: 'questions#index' get '/flowchart/:id', to: 'flowchart#serialized_flowchart_by_id' diff --git a/db/migrate/20191126021858_jti_matcher_revocation.rb b/db/migrate/20191126021858_jti_matcher_revocation.rb new file mode 100644 index 0000000..80a251f --- /dev/null +++ b/db/migrate/20191126021858_jti_matcher_revocation.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class JtiMatcherRevocation < ActiveRecord::Migration[6.0] + def change + add_column :users, :jti, :string, null: false + add_index :users, :jti, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 502fe81..c37ac97 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_11_12_021705) do +ActiveRecord::Schema.define(version: 2019_11_26_021858) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -53,7 +53,9 @@ t.string "encrypted_password", default: "", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.string "jti", null: false t.index ["email"], name: "index_users_on_email", unique: true + t.index ["jti"], name: "index_users_on_jti", unique: true end add_foreign_key "flowchart_nodes", "flowchart_nodes", column: "child_id" diff --git a/db/seeds.rb b/db/seeds.rb index 8e9b8f9..26c56f5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1 +1,6 @@ # frozen_string_literal: true + +case Rails.env +when 'development' + User.create(email: ENV['SEED_USER_EMAIL'], password: ENV['SEED_USER_PASSWORD'], jti: SecureRandom.uuid) +end