diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..665df2c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,118 @@ +version: 2.1 + +build_steps: &build_steps + steps: + - checkout + - run: + name: Install dependencies + command: bundle update + - run: + command: |- + echo "Ruby version: " $(ruby -v) + echo "Rails version: " $(rails -v) + name: Show build information + - run: + name: Run tests + command: rake + +ruby-2-4: &ruby-2-4 + docker: + - image: circleci/ruby:2.4 + +ruby-2-5: &ruby-2-5 + docker: + - image: circleci/ruby:2.5 + +ruby-2-6: &ruby-2-6 + docker: + - image: circleci/ruby:2.6 + +rails-4-2: &rails-4-2 + environment: + RAILS_VERSION: "~> 4.2.0" + +rails-5-1: &rails-5-1 + environment: + RAILS_VERSION: "~> 5.1.0" + +rails-5-2: &rails-5-2 + environment: + RAILS_VERSION: "~> 5.2.0" + +rails-6-0: &rails-6-0 + environment: + RAILS_VERSION: "6.0.0.rc1" + +rails-edge: &rails-edge + environment: + RAILS_BRANCH: "master" + +jobs: + "ruby-2-4-rails-4-2": + <<: *ruby-2-4 + <<: *rails-4-2 + <<: *build_steps + "ruby-2-4-rails-5-1": + <<: *ruby-2-4 + <<: *rails-5-1 + <<: *build_steps + "ruby-2-4-rails-5-2": + <<: *ruby-2-4 + <<: *rails-5-2 + <<: *build_steps + "ruby-2-5-rails-4-2": + <<: *ruby-2-5 + <<: *rails-4-2 + <<: *build_steps + "ruby-2-5-rails-5-1": + <<: *ruby-2-5 + <<: *rails-5-1 + <<: *build_steps + "ruby-2-5-rails-5-2": + <<: *ruby-2-5 + <<: *rails-5-2 + <<: *build_steps + "ruby-2-5-rails-6-0": + <<: *ruby-2-5 + <<: *rails-6-0 + <<: *build_steps + "ruby-2-5-rails-edge": + <<: *ruby-2-5 + <<: *rails-edge + <<: *build_steps + "ruby-2-6-rails-4-2": + <<: *ruby-2-6 + <<: *rails-4-2 + <<: *build_steps + "ruby-2-6-rails-5-1": + <<: *ruby-2-6 + <<: *rails-5-1 + <<: *build_steps + "ruby-2-6-rails-5-2": + <<: *ruby-2-6 + <<: *rails-5-2 + <<: *build_steps + "ruby-2-6-rails-6-0": + <<: *ruby-2-6 + <<: *rails-6-0 + <<: *build_steps + "ruby-2-6-rails-edge": + <<: *ruby-2-6 + <<: *rails-edge + <<: *build_steps + +workflows: + version: 2 + build: + jobs: + - "ruby-2-4-rails-4-2" + - "ruby-2-4-rails-5-1" + - "ruby-2-4-rails-5-2" + - "ruby-2-5-rails-4-2" + - "ruby-2-5-rails-5-1" + - "ruby-2-5-rails-5-2" + - "ruby-2-5-rails-6-0" + - "ruby-2-6-rails-4-2" + - "ruby-2-6-rails-5-1" + - "ruby-2-6-rails-5-2" + - "ruby-2-6-rails-6-0" diff --git a/.rubocop.yml b/.rubocop.yml index e0d96c7..32e06fe 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,27 +1,27 @@ +Layout/AccessModifierIndentation: + EnforcedStyle: outdent + +Layout/SpaceInsideHashLiteralBraces: + EnforcedStyle: no_space + Metrics/BlockNesting: Max: 2 -Metrics/ClassLength: - CountComments: false - Max: 120 - -Metrics/PerceivedComplexity: - Max: 8 +Metrics/LineLength: + AllowURI: true + Enabled: false -Metrics/ModuleLength: +Metrics/MethodLength: CountComments: false - Max: 120 + Max: 10 Metrics/ParameterLists: - Max: 3 + Max: 4 CountKeywordArgs: true -Metrics/AbcSize: - Enabled: false - Style/CollectionMethods: PreferredMethods: - collect: 'map' + map: 'collect' reduce: 'inject' find: 'detect' find_all: 'select' @@ -29,41 +29,27 @@ Style/CollectionMethods: Style/Documentation: Enabled: false -Style/DotPosition: - EnforcedStyle: trailing - Style/DoubleNegation: Enabled: false -Style/EachWithObject: - Enabled: false - -Style/Encoding: +Style/ExpandPathArguments: Enabled: false Style/HashSyntax: EnforcedStyle: hash_rockets -Style/Lambda: - Enabled: false - -Style/SingleSpaceBeforeFirstArg: +Style/StderrPuts: Enabled: false -Style/SpaceAroundOperators: - MultiSpaceAllowedForOperators: - - "=" - - "=>" - - "||" - - "||=" - - "&&" - - "&&=" +Style/StringLiterals: + EnforcedStyle: single_quotes -Style/SpaceInsideHashLiteralBraces: - EnforcedStyle: no_space +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma -Style/StringLiterals: - EnforcedStyle: double_quotes +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma -Style/TrivialAccessors: - Enabled: false +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cdc5e9c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +bundler_args: --without development +before_install: + - gem update --system + - gem update bundler +cache: bundler +env: + global: + - JRUBY_OPTS="$JRUBY_OPTS --debug" +language: ruby +rvm: + - jruby-9000 + - 2.3.5 + - 2.4.4 + - 2.5.3 + - jruby-head + - ruby-head +matrix: + allow_failures: + - rvm: jruby-head + - rvm: ruby-head + fast_finish: true +sudo: false diff --git a/Gemfile b/Gemfile index bee8b9d..da6b8da 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,21 @@ -source "https://rubygems.org" +# frozen_string_literal: true -# Specify your gem's dependencies in omniauth-rails.gemspec -gemspec +source 'https://rubygems.org' + +# rubocop:disable Bundler/DuplicatedGem +if ENV['RAILS_VERSION'] + gem 'rails', ENV['RAILS_VERSION'] +elsif ENV['RAILS_BRANCH'] + gem 'rails', git: 'https://github.com/rails/rails.git', branch: ENV['RAILS_BRANCH'] +end +# rubocop:enable Bundler/DuplicatedGem + +gem 'rake' -gem "rake" -gem "rubocop" +group :test do + gem 'coveralls', :require => false + gem 'rspec', '~> 3.5.0' + gem 'rubocop' +end + +gemspec diff --git a/Rakefile b/Rakefile index 4cb030a..5a62f05 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ -require "bundler/gem_tasks" -require "rubocop/rake_task" +# frozen_string_literal: true +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' -RuboCop::RakeTask.new - -task :default => :rubocop +RSpec::Core::RakeTask.new(:spec) +task :default => :spec diff --git a/lib/omniauth-rails.rb b/lib/omniauth-rails.rb new file mode 100644 index 0000000..02b82ca --- /dev/null +++ b/lib/omniauth-rails.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require 'omniauth-rails/version' +require 'omniauth-rails/railtie' diff --git a/lib/omniauth-rails/railtie.rb b/lib/omniauth-rails/railtie.rb new file mode 100644 index 0000000..85691cc --- /dev/null +++ b/lib/omniauth-rails/railtie.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require 'rails' + +module OmniAuth + module Rails + class Railtie < ::Rails::Railtie + initializer 'OmniAuth request_forgery_protection' do + OmniAuth.config.allowed_request_methods = [:post] + + method = OmniAuth.config.respond_to?(:validate_request_phase) ? + :validate_request_phase : + :before_request_phase + + OmniAuth.config.send(method) do |env| + OmniAuth::Rails::RequestForgeryProtection.call(env) + end + end + end + end +end diff --git a/lib/omniauth-rails/request_forgery_protection.rb b/lib/omniauth-rails/request_forgery_protection.rb new file mode 100644 index 0000000..fcbd747 --- /dev/null +++ b/lib/omniauth-rails/request_forgery_protection.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require 'action_controller' + +module OmniAuth + module Rails + module RequestForgeryProtection + class Controller < ActionController::Base + protect_from_forgery :with => :exception, :prepend => true + + rescue_from ActionController::InvalidAuthenticityToken do |e| + # Log warning + raise e + end + + def index + head :ok + end + end + + def self.app + @app ||= Controller.action(:index) + end + + def self.call(env) + app.call(env) + end + + def self.verified?(env) + call(env) + + true + rescue ActionController::InvalidAuthenticityToken + false + end + end + end +end diff --git a/lib/omniauth-rails/version.rb b/lib/omniauth-rails/version.rb index d29f060..ceb8711 100644 --- a/lib/omniauth-rails/version.rb +++ b/lib/omniauth-rails/version.rb @@ -1,3 +1,7 @@ -module OmniAuthRails - VERSION = "1.0.0" +# frozen_string_literal: true + +module OmniAuth + module Rails + VERSION = '1.0.0' + end end diff --git a/omniauth-rails.gemspec b/omniauth-rails.gemspec index eb43f80..a264dd8 100644 --- a/omniauth-rails.gemspec +++ b/omniauth-rails.gemspec @@ -1,23 +1,27 @@ -# coding: utf-8 -lib = File.expand_path("../lib", __FILE__) +# frozen_string_literal: true +lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "omniauth-rails/version" -Gem::Specification.new do |spec| - spec.name = "omniauth-rails" - spec.version = OmniAuthRails::VERSION - spec.authors = ["Erik Michaels-Ober", "Douwe Maan"] - spec.email = ["sferik@gmail.com", "douwe@gitlab.com"] +require 'omniauth-rails/version' - spec.description = "Ruby on Rails extensions to OmniAuth" - spec.summary = spec.description - spec.homepage = "https://github.com/intridea/omniauth-rails" - spec.license = "MIT" +Gem::Specification.new do |gem| + gem.authors = ['Tom Milewski'] + gem.email = ['tmilewski@gmail.com'] + gem.description = 'Official Rails OmniAuth gem.' + gem.summary = gem.description + gem.homepage = 'https://github.com/omniauth/omniauth-rails' + gem.license = 'MIT' - spec.files = `git ls-files -z`.split("\x0") - spec.require_paths = ["lib"] + gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } + gem.files = `git ls-files`.split("\n") + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.name = 'omniauth-rails' + gem.require_paths = %w[lib] + gem.version = OmniAuth::Rails::VERSION - spec.add_dependency "omniauth" - spec.add_dependency "rails" - spec.add_development_dependency "bundler", "~> 1.9" + gem.add_dependency 'omniauth', '~> 1.0' + gem.add_dependency 'rails' + gem.add_development_dependency 'rack-test' + gem.add_development_dependency 'rspec', '~> 3.5' + gem.add_development_dependency 'simplecov' end diff --git a/spec/omniauth-rails/railtie_spec.rb b/spec/omniauth-rails/railtie_spec.rb new file mode 100644 index 0000000..cd0ba28 --- /dev/null +++ b/spec/omniauth-rails/railtie_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe OmniAuth::Rails::Railtie do + describe 'Railtie' do + before do + OmniAuth::Rails::Railtie.initializers.each(&:run) + end + + it 'should only allow POST requests' do + expect(OmniAuth.config.allowed_request_methods).to eq([:post]) + end + + it '`before_request_phase` should call `OmniAuth::Rails::RequestForgeryProtection`' do + env = {} + expect(OmniAuth::Rails::RequestForgeryProtection).to receive(:call).with(env) + OmniAuth.config.before_request_phase.call(env) + end + end + + describe 'Railtie Integration' do + let(:app) { Rails.application } + + let(:authenticity_token) do + get "/token" + last_response.body + end + + it 'should disallow access to :get with 404' do + get '/auth/developer' + expect(last_response.status).to eq(404) + end + + it 'should fail as 422 without a token' do + post '/auth/developer' + expect(last_response.status).to eq(422) + end + + it 'should fail with a bad token' do + post '/auth/developer', authenticity_token: 'foo' + expect(last_response.status).to eq(422) + end + + it 'should succeed with a vaild token' do + post '/auth/developer', authenticity_token: authenticity_token + expect(last_response.status).to eq(200) + end + end +end diff --git a/spec/omniauth-rails/request_forgery_protection_spec.rb b/spec/omniauth-rails/request_forgery_protection_spec.rb new file mode 100644 index 0000000..fc31082 --- /dev/null +++ b/spec/omniauth-rails/request_forgery_protection_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe OmniAuth::Rails::RequestForgeryProtection do + let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) } + let(:env) do + { + 'rack.input' => '', + 'rack.session' => { + :_csrf_token => csrf_token, + }, + } + end + + describe '.call' do + context 'when the request method is GET' do + before do + env['REQUEST_METHOD'] = 'GET' + end + + it 'does not raise an exception' do + expect { described_class.call(env) }.not_to raise_exception + end + end + + context 'when the request method is POST' do + before do + env['REQUEST_METHOD'] = 'POST' + end + + context 'when the CSRF token is valid' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it 'does not raise an exception' do + expect { described_class.call(env) }.not_to raise_exception + end + end + + context 'when the CSRF token is invalid' do + before do + env['HTTP_X_CSRF_TOKEN'] = 'foo' + end + + it 'raises an ActionController::InvalidAuthenticityToken exception' do + expect { described_class.call(env) }.to raise_exception(ActionController::InvalidAuthenticityToken) + end + end + end + end + + describe '.verified?' do + context 'when the request method is GET' do + before do + env['REQUEST_METHOD'] = 'GET' + end + + it 'returns true' do + expect(described_class.verified?(env)).to be_truthy + end + end + + context 'when the request method is POST' do + before do + env['REQUEST_METHOD'] = 'POST' + end + + context 'when the CSRF token is valid' do + before do + env['HTTP_X_CSRF_TOKEN'] = csrf_token + end + + it 'returns true' do + expect(described_class.verified?(env)).to be_truthy + end + end + + context 'when the CSRF token is invalid' do + before do + env['HTTP_X_CSRF_TOKEN'] = 'foo' + end + + it 'returns false' do + expect(described_class.verified?(env)).to be_falsey + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..7574c09 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +$:.unshift File.expand_path('..', __FILE__) +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'simplecov' +SimpleCov.start + +require 'rspec' +require 'rack/test' +require 'omniauth' + +require 'omniauth-rails/railtie' +require 'omniauth-rails/request_forgery_protection' + +class TestApp < Rails::Application + config.root = __dir__ + config.session_store :cookie_store, key: 'cookie_store_key' + secrets.secret_key_base = 'secret_key_base' + config.eager_load = false + config.hosts = [] + + # This allow us to send all logs to STDOUT if we run test wth `VERBOSE=1` + config.logger = if ENV['VERBOSE'] + Logger.new($stdout) + else + Logger.new('/dev/null') + end + Rails.logger = config.logger + OmniAuth.config.logger = Rails.logger + + # Setup a simple OmniAuth configuration with only developer provider + config.middleware.use OmniAuth::Builder do + provider :developer + end + + # We need to call initialize! to run all railties + initialize! + + # Define our custom routes. This needs to be called after initialize! + routes.draw do + get "token" => "application#token" + end +end + +# A small test controller which we use to retrive the valid authenticity token +class ApplicationController < ActionController::Base + def token + render plain: form_authenticity_token + end +end + +RSpec.configure do |config| + config.include Rack::Test::Methods +end