diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8c6511c5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +--- +name: ci +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" +jobs: + test: + strategy: + fail-fast: false + matrix: + ruby: + - 2.3 + - 2.4 + - 2.5 + - 2.6 + - 2.7 + gemfile: + - gemfiles/standalone.gemfile + - gemfiles/openssl.gemfile + - gemfiles/rbnacl.gemfile + experimental: [false] + include: + - ruby: 2.1 + experimental: true + - ruby: 2.2 + experimental: true + - ruby: 2.7 + coverage: "true" + gemfile: 'gemfiles/rbnacl.gemfile' + - ruby: "ruby-head" + experimental: true + - ruby: "truffleruby-head" + experimental: true + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.experimental }} + env: + BUNDLE_GEMFILE: ${{ matrix.gemfile }} + + steps: + - uses: actions/checkout@v2 + + - name: Install libsodium + run: | + sudo apt-get update -q + sudo apt-get install libsodium-dev -y + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Run tests + run: bundle exec rspec + + - name: Report coverage + if: ${{ success() && matrix.coverage == 'true' }} + run: bundle exec codeclimate-test-reporter diff --git a/.travis.yml b/.travis.yml index 2a65e90d..99938bd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,32 @@ -sudo: required cache: bundler -dist: trusty language: ruby rvm: - - 2.3 - - 2.4 - - 2.5 - 2.6 + - 2.5 + - 2.4 + - 2.3 + - 2.2 + - 2.1 + - ruby-head - truffleruby-head gemfile: - gemfiles/standalone.gemfile - - gemfiles/rails_5.0.gemfile - - gemfiles/rails_5.1.gemfile - - gemfiles/rails_5.2.gemfile - - gemfiles/rails_6.0.gemfile -script: "bundle exec rspec && bundle exec codeclimate-test-reporter" + - gemfiles/openssl.gemfile + - gemfiles/rbnacl.gemfile +script: + - bundle exec rspec before_install: - - sudo add-apt-repository ppa:chris-lea/libsodium -y - sudo apt-get update -q - sudo apt-get install libsodium-dev -y - - gem install bundler - matrix: fast_finish: true - exclude: - - gemfile: gemfiles/rails_6.0.gemfile - rvm: 2.3 - - gemfile: gemfiles/rails_6.0.gemfile - rvm: 2.4 - include: - - gemfile: gemfiles/standalone.gemfile - rvm: truffleruby-head + includes: + - rvm: 2.7 + name: "Ruby 2.7 and Code Climate" + after_script: + - bundle exec codeclimate-test-reporter + allow_failures: + - rvm: 2.2 + - rvm: 2.1 + - rvm: ruby-head + - rvm: truffleruby-head diff --git a/Appraisals b/Appraisals index 353d0202..21954496 100644 --- a/Appraisals +++ b/Appraisals @@ -1,18 +1,10 @@ appraise 'standalone' do end -appraise 'rails-5.0' do - gem 'rails', '~> 5.0.0' +appraise 'openssl' do + gem 'openssl', '~> 2.1' end -appraise 'rails-5.1' do - gem 'rails', '~> 5.1.0' -end - -appraise 'rails-5.2' do - gem 'rails', '~> 5.2.0' -end - -appraise 'rails-6.0' do - gem 'rails', '~> 6.0.0' +appraise 'rbnacl' do + gem 'rbnacl' end diff --git a/gemfiles/rails_5.0.gemfile b/gemfiles/openssl.gemfile similarity index 79% rename from gemfiles/rails_5.0.gemfile rename to gemfiles/openssl.gemfile index 10f52e7a..94ce8660 100644 --- a/gemfiles/rails_5.0.gemfile +++ b/gemfiles/openssl.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 5.0.0" +gem "openssl", "~> 2.1" gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile deleted file mode 100644 index 5a706dcb..00000000 --- a/gemfiles/rails_5.2.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 5.2.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_6.0.gemfile deleted file mode 100644 index 15b9b275..00000000 --- a/gemfiles/rails_6.0.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rails", "~> 6.0.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.1.gemfile b/gemfiles/rbnacl.gemfile similarity index 79% rename from gemfiles/rails_5.1.gemfile rename to gemfiles/rbnacl.gemfile index 6100e830..ad523a89 100644 --- a/gemfiles/rails_5.1.gemfile +++ b/gemfiles/rbnacl.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "rails", "~> 5.1.0" +gem "rbnacl" gemspec path: "../" diff --git a/ruby-jwt.gemspec b/ruby-jwt.gemspec index 02e06e58..a0415b7d 100644 --- a/ruby-jwt.gemspec +++ b/ruby-jwt.gemspec @@ -28,7 +28,4 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov-json' spec.add_development_dependency 'codeclimate-test-reporter' spec.add_development_dependency 'codacy-coverage' - spec.add_development_dependency 'rbnacl' - # RSASSA-PSS support provided by OpenSSL +2.1 - spec.add_development_dependency 'openssl', '~> 2.1' end diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index 361a7ab2..7488541c 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -79,7 +79,7 @@ { 'data' => 'test' }, { 'alg' => 'PS256' } ] - end + end if OpenSSL::VERSION >= '2.1' end context 'claims' do diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index e49f8dee..1ffedc99 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -7,7 +7,7 @@ let(:payload) { { 'user_id' => 'some@user.tld' } } let :data do - { + data = { :secret => 'My$ecretK3y', :rsa_private => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'rsa-2048-private.pem'))), :rsa_public => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'rsa-2048-public.pem'))), @@ -19,8 +19,6 @@ 'ES384_public' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec384-public.pem'))), 'ES512_private' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec512-private.pem'))), 'ES512_public' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec512-public.pem'))), - 'ED25519_private' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF'), - 'ED25519_public' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF').verify_key, 'NONE' => 'eyJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.', 'HS256' => 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.kWOVtIOpWcG7JnyJG0qOkTDbOy636XrrQhMm_8JrRQ8', 'HS512256' => 'eyJhbGciOiJIUzUxMjI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Ds_4ibvf7z4QOBoKntEjDfthy3WJ-3rKMspTEcHE2bA', @@ -36,6 +34,14 @@ 'PS384' => '', 'PS512' => '' } + + if defined?(RbNaCl) + data.merge!( + 'ED25519_private' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF'), + 'ED25519_public' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF').verify_key, + ) + end + data end after(:each) do @@ -99,7 +105,7 @@ expect(validator).to receive(:validate!) { true } payload = {} - JWT.encode payload, "secret", JWT::Algos::Hmac::SUPPORTED.sample + JWT.encode payload, "secret", 'HS256' end it 'does not validate the payload if it is not present' do @@ -107,11 +113,14 @@ expect(JWT::ClaimsValidator).not_to receive(:new) { validator } payload = nil - JWT.encode payload, "secret", JWT::Algos::Hmac::SUPPORTED.sample + JWT.encode payload, "secret", 'HS256' end end - %w[HS256 HS512256 HS384 HS512].each do |alg| + algorithms = %w[HS256 HS384 HS512] + algorithms << 'HS512256' if defined?(RbNaCl) + + algorithms.each do |alg| context "alg: #{alg}" do it 'should generate a valid token' do token = JWT.encode payload, data[:secret], alg @@ -180,38 +189,40 @@ end end - %w[ED25519].each do |alg| - context "alg: #{alg}" do - before(:each) do - data[alg] = JWT.encode payload, data["#{alg}_private"], alg - end + if defined?(RbNaCl) + %w[ED25519].each do |alg| + context "alg: #{alg}" do + before(:each) do + data[alg] = JWT.encode payload, data["#{alg}_private"], alg + end - let(:wrong_key) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-wrong-public.pem'))) } + let(:wrong_key) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-wrong-public.pem'))) } - it 'should generate a valid token' do - jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg + it 'should generate a valid token' do + jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg - expect(header['alg']).to eq alg - expect(jwt_payload).to eq payload - end + expect(header['alg']).to eq alg + expect(jwt_payload).to eq payload + end - it 'should decode a valid token' do - jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg + it 'should decode a valid token' do + jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg - expect(header['alg']).to eq alg - expect(jwt_payload).to eq payload - end + expect(header['alg']).to eq alg + expect(jwt_payload).to eq payload + end - it 'wrong key should raise JWT::DecodeError' do - expect do - JWT.decode data[alg], wrong_key - end.to raise_error JWT::DecodeError - end + it 'wrong key should raise JWT::DecodeError' do + expect do + JWT.decode data[alg], wrong_key + end.to raise_error JWT::DecodeError + end - it 'wrong key and verify = false should not raise JWT::DecodeError' do - expect do - JWT.decode data[alg], wrong_key, false - end.not_to raise_error + it 'wrong key and verify = false should not raise JWT::DecodeError' do + expect do + JWT.decode data[alg], wrong_key, false + end.not_to raise_error + end end end end @@ -252,51 +263,64 @@ end end - %w[PS256 PS384 PS512].each do |alg| - context "alg: #{alg}" do - before(:each) do - data[alg] = JWT.encode payload, data[:rsa_private], alg + unless OpenSSL::VERSION >= '2.1' + %w[PS256 PS384 PS512].each do |alg| + context "alg: #{alg}" do + it 'raises error about OpenSSL version' do + expect { JWT.encode payload, data[:rsa_private], alg }.to raise_error( + JWT::RequiredDependencyError, + /You currently have OpenSSL .*. PS support requires >= 2.1/ + ) + end end + end + else + %w[PS256 PS384 PS512].each do |alg| + context "alg: #{alg}" do + before(:each) do + data[alg] = JWT.encode payload, data[:rsa_private], alg + end - let(:wrong_key) { data[:wrong_rsa_public] } + let(:wrong_key) { data[:wrong_rsa_public] } - it 'should generate a valid token' do - token = data[alg] - - header, body, signature = token.split('.') - - expect(header).to eql(Base64.strict_encode64({ alg: alg }.to_json)) - expect(body).to eql(Base64.strict_encode64(payload.to_json)) - - # Validate signature is made of up header and body of JWT - translated_alg = alg.gsub('PS', 'sha') - valid_signature = data[:rsa_public].verify_pss( - translated_alg, - JWT::Base64.url_decode(signature), - [header, body].join('.'), - salt_length: :auto, - mgf1_hash: translated_alg - ) - expect(valid_signature).to be true - end + it 'should generate a valid token' do + token = data[alg] - it 'should decode a valid token' do - jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg + header, body, signature = token.split('.') - expect(header['alg']).to eq alg - expect(jwt_payload).to eq payload - end + expect(header).to eql(Base64.strict_encode64({ alg: alg }.to_json)) + expect(body).to eql(Base64.strict_encode64(payload.to_json)) - it 'wrong key should raise JWT::DecodeError' do - expect do - JWT.decode data[alg], wrong_key - end.to raise_error JWT::DecodeError - end + # Validate signature is made of up header and body of JWT + translated_alg = alg.gsub('PS', 'sha') + valid_signature = data[:rsa_public].verify_pss( + translated_alg, + JWT::Base64.url_decode(signature), + [header, body].join('.'), + salt_length: :auto, + mgf1_hash: translated_alg + ) + expect(valid_signature).to be true + end - it 'wrong key and verify = false should not raise JWT::DecodeError' do - expect do - JWT.decode data[alg], wrong_key, false - end.not_to raise_error + it 'should decode a valid token' do + jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg + + expect(header['alg']).to eq alg + expect(jwt_payload).to eq payload + end + + it 'wrong key should raise JWT::DecodeError' do + expect do + JWT.decode data[alg], wrong_key + end.to raise_error JWT::DecodeError + end + + it 'wrong key and verify = false should not raise JWT::DecodeError' do + expect do + JWT.decode data[alg], wrong_key, false + end.not_to raise_error + end end end end