From a133496f172f52eba9afc74fba076e5908bcb864 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 24 Dec 2014 16:26:19 +0100 Subject: [PATCH 01/35] Prepare splitting code into JWT, JWS and JWA JWT, JWS, JWA logic should be represented in seperate modules. Added new pending tests for each module. Update travis configuration. Dropped support for jruby and rbx for now. Added some more configuration options for travis. Renamed spec/helper to spec/spec_helper. All tests now load the modules by relative path. Added empty JWA and JWS modules. --- .rspec | 2 + .travis.yml | 13 +++--- lib/jwa.rb | 2 + lib/jws.rb | 2 + spec/helper.rb | 6 --- spec/jwt_spec.rb | 3 +- spec/lib/jwa_spec.rb | 70 +++++++++++++++++++++++++++++++ spec/lib/jws_spec.rb | 42 +++++++++++++++++++ spec/lib/jwt_spec.rb | 98 ++++++++++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 4 ++ 10 files changed, 230 insertions(+), 12 deletions(-) create mode 100644 .rspec create mode 100644 lib/jwa.rb create mode 100644 lib/jws.rb delete mode 100644 spec/helper.rb create mode 100644 spec/lib/jwa_spec.rb create mode 100644 spec/lib/jws_spec.rb create mode 100644 spec/lib/jwt_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..16f9cdb0 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/.travis.yml b/.travis.yml index 42dbdb95..c58b3f2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,11 @@ rvm: - 1.9.2 - 1.9.3 - 2.0.0 - - 2.1.0 - - jruby - - rbx - - ree -script: "bundle exec rake test" + - 2.1.5 +script: "bundle exec rspec" +before_install: + - gem update --system + - gem --version +addons: + code_climate: + repo_token: f00428819bf1930e10b8ea2eecf6ecf57e295fcca4f4eb1d77bfd278f573e239 diff --git a/lib/jwa.rb b/lib/jwa.rb new file mode 100644 index 00000000..251e22d0 --- /dev/null +++ b/lib/jwa.rb @@ -0,0 +1,2 @@ +module JWA +end diff --git a/lib/jws.rb b/lib/jws.rb new file mode 100644 index 00000000..d8a185d1 --- /dev/null +++ b/lib/jws.rb @@ -0,0 +1,2 @@ +module JWS +end diff --git a/spec/helper.rb b/spec/helper.rb deleted file mode 100644 index 9f89c777..00000000 --- a/spec/helper.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'rspec' -require "#{File.dirname(__FILE__)}/../lib/jwt.rb" - -RSpec.configure do |c| -end - diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index dcf34b6a..bcb9237a 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -1,4 +1,5 @@ -require 'helper' +require 'spec_helper' +require_relative '../lib/jwt' describe JWT do before do diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb new file mode 100644 index 00000000..e4bb18c1 --- /dev/null +++ b/spec/lib/jwa_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' +require_relative '../../lib/jwa' + +describe JWA do + context 'HMAC signing, verifying' do + [256, 384, 512].each do |bit| + it "#{bit} should verify" + it "#{bit} should not verify" + it 'missing secret' + it 'weird input' + end + end + + context 'RSASSA signing, veryfying' do + [256, 384, 512].each do |bit| + it "#{bit} should verify" + it "#{bit} should not verify" + it "#{bit} missing sign key" + it "#{bit} missing verify key" + it "#{bit} weird input" + end + end + + [256, 384, 512].each do |bit| + context "RS#{bit} openssl sign -> ruby verify" do + it 'should verify' + it 'should not verify' + end + end + + [256, 384, 512].each do |bit| + context "RS#{bit} ruby sign -> openssl verify" do + it 'should verify' + it 'should not verify' + end + end + + context 'ECDSA signing, verifying' do + [256, 384, 512].each do |bit| + it "#{bit} should verify" + it "#{bit} should not verify" + it "#{bit} missing sign key" + it "#{bit} missing verify key" + it "#{bit} weird input" + end + end + + [256, 384, 512].each do |bit| + context "ES#{bit} openssl sign -> ruby verify" do + it 'should verify' + it 'should not verify' + end + end + + [256, 384, 512].each do |bit| + context "ES#{bit} ruby sign -> openssl verify" do + it 'should verify' + it 'should not verify' + end + end + + context 'NONE' do + it 'should verify' + it 'should not verify' + end + + context 'unsupported algorithm' do + it 'should throw' + end +end diff --git a/spec/lib/jws_spec.rb b/spec/lib/jws_spec.rb new file mode 100644 index 00000000..2a72310b --- /dev/null +++ b/spec/lib/jws_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require_relative '../../lib/jws' + +describe JWS do + [256, 384, 512].each do |bit| + context "HMAC using SHA-#{bit} hash algorithm" do + it 'should verify' + it 'should not verify' + it 'should match payload' + it 'should match header' + end + end + + [256, 384, 512].each do |bit| + context "RSASSA using SHA-#{bit} hash algorithm" do + it 'should verify' + it 'should not verify' + it 'should match payload' + it 'should match header' + end + end + + [256, 384, 512].each do |bit| + context "ECDSA using P-#{bit} curve and SHA-#{bit} hash algorithm" do + it 'should verify' + it 'should not verify' + it 'should match payload' + it 'should match header' + end + end + + context 'NONE' do + it 'should verify' + it 'should still verify' + it 'should match payload' + it 'should match header' + end + + context 'unsupported algorithm' do + it 'should throw' + end +end diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb new file mode 100644 index 00000000..e588bb8f --- /dev/null +++ b/spec/lib/jwt_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' +require_relative '../../lib/jwt' + +describe JWT do + context 'RSASSA' do + %w(RS256 RS384 RS512).each do |algorithm| + context "[#{algorithm}] when signing a token" do + it 'should be syntactically valid' + it 'should validate with public key' + it 'should throw with invalid public key' + end + end + end + + context 'HMAC' do + let(:secret) { 'valid-secret' } + + %w(HS256 HS384 HS512).each do |algorithm| + context "[#{algorithm}] when signing a token" do + it 'should be syntactically valid' + it 'should without options' + it 'should validate with secret' + it 'should throw with invalid secret' + it 'should throw with secret and token not signed' + it 'should throw when verifying null' + it 'should throw when the payload is not json' + end + end + end + + context 'ECDSA' do + %w(ES256 ES384 ES512).each do |algorithm| + context "[#{algorithm}] when signing a token" do + it 'should be syntactically valid' + it 'should without options' + it 'should validate with secret' + it 'should throw with invalid secret' + it 'should throw with secret and token not signed' + it 'should throw when verifying null' + it 'should throw when the payload is not json' + end + end + end + + context 'none' do + + end + + context 'when signing a token with expiration' do + it 'should be valid expiration' + it 'should be invalid' + end + + context 'when signing a token with audience' do + it 'should check audience' + it 'should check audience in array' + it 'should throw when invalid audience' + it 'should throw when invalid audience in array' + end + + context 'when signing a token with array audience' do + it 'should check audience' + it 'should check other audience' + it 'should check audience in array' + it 'should throw when invalid audience' + it 'should throw when invalid audience in array' + end + + context 'when signing a token without audience' do + it 'should check audience' + it 'should check audience in array' + end + + context 'when signing a token with issuer' do + it 'should check issuer' + it 'should throw when invalid issuer' + end + + context 'when signing a token without issuer' do + it 'should check issuer' + end + + context 'when verifying a malformed token' do + it 'should throw' + end + + context 'when decoding a jwt token with additional parts' do + it 'should throw' + end + + context 'when decoding a invalid jwt token' do + it 'should return nil' + end + + context 'when decoding a valid jwt token' do + it 'should return the payload' + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..5c1935db --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,4 @@ +require 'rspec' + +RSpec.configure do |c| +end From b2631a0b94c5c5371cab73484f61ec776b2ac50c Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 24 Dec 2014 16:55:03 +0100 Subject: [PATCH 02/35] Dropping mri 1.8 support, update configuration Dropping ruby mri 1.8 support due to the missing require_relative support. Adding codeclimate test reporter gem. Update spec/spec_helper configuration. --- .gitignore | 1 + .travis.yml | 1 - Gemfile | 1 + spec/spec_helper.rb | 13 +++++++++++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 55a9aa81..300a7b09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/coverage jwt.gemspec pkg Gemfile.lock diff --git a/.travis.yml b/.travis.yml index c58b3f2e..6f5c0fb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: ruby rvm: - - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 diff --git a/Gemfile b/Gemfile index 7e455828..54bafecd 100644 --- a/Gemfile +++ b/Gemfile @@ -13,4 +13,5 @@ end group :test, :development do gem 'rake' gem 'rspec', '~> 3' + gem 'codeclimate-test-reporter' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5c1935db..f3e98472 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,13 @@ -require 'rspec' +require "codeclimate-test-reporter" +CodeClimate::TestReporter.start -RSpec.configure do |c| +RSpec.configure do |config| + config.expect_with :rspec do |c| + c.syntax = [:should, :expect] + end + + config.run_all_when_everything_filtered = true + config.filter_run :focus + + config.order = 'random' end From c21a0c9e8e0c609d4a2f9e5ef1142c422cfb983d Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 24 Dec 2014 17:00:46 +0100 Subject: [PATCH 03/35] Remove obsolete gems --- Gemfile | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 54bafecd..cb23fa32 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,6 @@ source "https://rubygems.org" -gem 'json', '>= 1.2.4' -gem 'multi_json', '~> 1.0', :platforms => :ruby_18 -gem 'jruby-openssl', :platforms => :jruby - -gem 'rubysl', '~> 2.0', :platforms => :rbx +gem 'json' group :development do gem 'echoe', '>= 4.6.3' @@ -12,6 +8,6 @@ end group :test, :development do gem 'rake' - gem 'rspec', '~> 3' + gem 'rspec' gem 'codeclimate-test-reporter' end From fe71b94cf5efe52c50c1fb659b0b45fc8454bbb8 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 24 Dec 2014 18:46:12 +0100 Subject: [PATCH 04/35] JWA HMAC implementation Add first HMAC functionality to JWA. --- lib/jwa.rb | 27 +++++++++++++++++++++++++++ lib/jwa/hmac.rb | 15 +++++++++++++++ lib/jwt.rb | 4 ++-- spec/lib/jwa_spec.rb | 37 ++++++++++++++++++++++++++++++++----- 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 lib/jwa/hmac.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 251e22d0..e568d235 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,2 +1,29 @@ module JWA + autoload :HS, 'jwa/hmac' + + class Base + def normalize_input(input) + input = input.to_json unless input.is_a? String + input + end + protected :normalize_input + end + + def self.create(algorithm) + klass = nil + algo = algorithm.match(/(ES|HS|RS)(256|384|512)/) + + raise ArgumentError.new('Unsupported algorithm.') unless algo + + case algo[1] + when 'ES' + klass = ES.new algo[2].to_i + when 'HS' + klass = HS.new algo[2].to_i + when 'RS' + klass = RS.new algo[2].to_i + end + + klass + end end diff --git a/lib/jwa/hmac.rb b/lib/jwa/hmac.rb new file mode 100644 index 00000000..294768d2 --- /dev/null +++ b/lib/jwa/hmac.rb @@ -0,0 +1,15 @@ +module JWA + class HS < Base + def initialize(bits) + @algorithm = OpenSSL::Digest.new("sha#{bits}") + end + + def sign(input, secret) + OpenSSL::HMAC.digest(@algorithm, secret, normalize_input(input)) + end + + def verify(input, signature, secret) + sign(input, secret) === signature + end + end +end diff --git a/lib/jwt.rb b/lib/jwt.rb index 322997e4..66f9271b 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -99,13 +99,13 @@ def decode(jwt, key=nil, verify=true, options={}, &keyfinder) header, payload, signature, signing_input = decoded_segments(jwt, verify) raise JWT::DecodeError.new("Not enough or too many segments") unless header && payload - + default_options = { :verify_expiration => true, :leeway => 0 } options = default_options.merge(options) - + if verify algo, key = signature_algorithm_and_key(header, key, &keyfinder) verify_signature(algo, key, signing_input, signature) diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index e4bb18c1..b9097ef8 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -3,11 +3,35 @@ describe JWA do context 'HMAC signing, verifying' do + let(:input) { 'my super awesome input' } + let(:secret) { 'donottellanyone' } + [256, 384, 512].each do |bit| - it "#{bit} should verify" - it "#{bit} should not verify" - it 'missing secret' - it 'weird input' + before(:each) do + @jwa = JWA.create "HS#{bit}" + @sig = @jwa.sign input, secret + end + + it "#{bit} should verify" do + expect(@jwa.verify(input, @sig, secret)).to eq(true) + end + + it "#{bit} should not verify" do + expect(@jwa.verify('different', @sig, secret)).to eq(false) + expect(@jwa.verify(input, 'different', secret)).to eq(false) + expect(@jwa.verify(input, @sig, 'let me in')).to eq(false) + end + + it 'missing secret' do + expect{ @jwa.sign(input) }.to raise_error + end + + it 'weird input' do + inpt = { a: [1, 2, 3, 4] } + data = @jwa.sign(inpt, secret) + expect(@jwa.verify(inpt, data, secret)).to eq(true) + expect(@jwa.verify(inpt, data, 'let me in')).to eq(false) + end end end @@ -65,6 +89,9 @@ end context 'unsupported algorithm' do - it 'should throw' + it 'should throw' do + expect{ JWA.create('invalid') }.to raise_error + expect{ JWA.create('HS255') }.to raise_error + end end end From deb7dcb4aa680dfd533f63135b52a54ed5661baa Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 24 Dec 2014 18:50:46 +0100 Subject: [PATCH 05/35] JWA HMAC implementation Make rspec tests more readable. Add missing bit to description for HMAC rspec test cases. --- spec/lib/jwa_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index b9097ef8..fda93213 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -22,11 +22,11 @@ expect(@jwa.verify(input, @sig, 'let me in')).to eq(false) end - it 'missing secret' do + it "#{bit} missing secret" do expect{ @jwa.sign(input) }.to raise_error end - it 'weird input' do + it "#{bit} weird input" do inpt = { a: [1, 2, 3, 4] } data = @jwa.sign(inpt, secret) expect(@jwa.verify(inpt, data, secret)).to eq(true) From 27173510103403059e9e9fac11865d7c0bea28e3 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 25 Dec 2014 00:10:18 +0100 Subject: [PATCH 06/35] JWA RSASSA implementation Add basic support for RSA-SHA functionality. Add bin/prepare-test.sh file. Generates certificates required for running the tests. Update .gitignore file. Update .travis.yml file. --- .gitignore | 1 + .travis.yml | 2 ++ bin/prepare-test.sh | 20 ++++++++++++++++++++ lib/jwa.rb | 1 + lib/jwa/rsassa.rb | 15 +++++++++++++++ spec/lib/jwa_spec.rb | 45 +++++++++++++++++++++++++++++++++++++++----- spec/spec_helper.rb | 2 ++ 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100755 bin/prepare-test.sh create mode 100644 lib/jwa/rsassa.rb diff --git a/.gitignore b/.gitignore index 300a7b09..0568cfa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /coverage +/tmp jwt.gemspec pkg Gemfile.lock diff --git a/.travis.yml b/.travis.yml index 6f5c0fb7..b9932b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ script: "bundle exec rspec" before_install: - gem update --system - gem --version +before_script: + - bin/prepare-test.sh addons: code_climate: repo_token: f00428819bf1930e10b8ea2eecf6ecf57e295fcca4f4eb1d77bfd278f573e239 diff --git a/bin/prepare-test.sh b/bin/prepare-test.sh new file mode 100755 index 00000000..ab0aa281 --- /dev/null +++ b/bin/prepare-test.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +mkdir -p tmp/certs/jwa + +openssl genrsa 2048 > tmp/certs/jwa/rsa-private.pem +openssl genrsa 2048 > tmp/certs/jwa/rsa-wrong-private.pem +openssl rsa -in tmp/certs/jwa/rsa-private.pem -pubout > tmp/certs/jwa/rsa-public.pem +openssl rsa -in tmp/certs/jwa/rsa-wrong-private.pem -pubout > tmp/certs/jwa/rsa-wrong-public.pem +openssl ecparam -out tmp/certs/jwa/ec256-private.pem -name secp256k1 -genkey +openssl ecparam -out tmp/certs/jwa/ec256-wrong-private.pem -name secp256k1 -genkey +openssl ecparam -out tmp/certs/jwa/ec384-private.pem -name secp384r1 -genkey +openssl ecparam -out tmp/certs/jwa/ec384-wrong-private.pem -name secp384r1 -genkey +openssl ecparam -out tmp/certs/jwa/ec512-private.pem -name secp521r1 -genkey +openssl ecparam -out tmp/certs/jwa/ec512-wrong-private.pem -name secp521r1 -genkey +openssl ec -in tmp/certs/jwa/ec256-private.pem -pubout > tmp/certs/jwa/ec256-public.pem +openssl ec -in tmp/certs/jwa/ec256-wrong-private.pem -pubout > tmp/certs/jwa/ec256-wrong-public.pem +openssl ec -in tmp/certs/jwa/ec384-private.pem -pubout > tmp/certs/jwa/ec384-public.pem +openssl ec -in tmp/certs/jwa/ec384-wrong-private.pem -pubout > tmp/certs/jwa/ec384-wrong-public.pem +openssl ec -in tmp/certs/jwa/ec512-private.pem -pubout > tmp/certs/jwa/ec512-public.pem +openssl ec -in tmp/certs/jwa/ec512-wrong-private.pem -pubout > tmp/certs/jwa/ec512-wrong-public.pem diff --git a/lib/jwa.rb b/lib/jwa.rb index e568d235..5958ce1f 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,5 +1,6 @@ module JWA autoload :HS, 'jwa/hmac' + autoload :RS, 'jwa/rsassa' class Base def normalize_input(input) diff --git a/lib/jwa/rsassa.rb b/lib/jwa/rsassa.rb new file mode 100644 index 00000000..fda7f50f --- /dev/null +++ b/lib/jwa/rsassa.rb @@ -0,0 +1,15 @@ +module JWA + class RS < Base + def initialize(bits) + @algorithm = OpenSSL::Digest.new("sha#{bits}") + end + + def sign(input, rsa_key) + rsa_key.sign(@algorithm, normalize_input(input)) + end + + def verify(input, signature, rsa_key) + rsa_key.verify(@algorithm, signature, normalize_input(input)) + end + end +end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index fda93213..7aa7b6f7 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -36,12 +36,47 @@ end context 'RSASSA signing, veryfying' do + let(:input) { 'my super awesome input' } + let(:public_key) do + OpenSSL::PKey::RSA.new File.read(File.join(CERT_PATH, 'jwa', 'rsa-public.pem')) + end + + let(:private_key) do + OpenSSL::PKey::RSA.new File.read(File.join(CERT_PATH, 'jwa', 'rsa-private.pem')) + end + + let(:wrong_public_key) do + OpenSSL::PKey::RSA.new File.read(File.join(CERT_PATH, 'jwa', 'rsa-wrong-public.pem')) + end + [256, 384, 512].each do |bit| - it "#{bit} should verify" - it "#{bit} should not verify" - it "#{bit} missing sign key" - it "#{bit} missing verify key" - it "#{bit} weird input" + before(:each) do + @jwa = JWA.create "RS#{bit}" + @sig = @jwa.sign input, private_key + end + + it "#{bit} should verify" do + expect(@jwa.verify(input, @sig, public_key)).to eq(true) + end + + it "#{bit} should not verify" do + expect(@jwa.verify(input, @sig, wrong_public_key)).to eq(false) + end + + it "#{bit} missing sign key" do + expect{@jwa.sign(input)}.to raise_error + end + + it "#{bit} missing verify key" do + expect{@jwa.verify(input, @sig)}.to raise_error + end + + it "#{bit} weird input" do + inpt = { a: [1, 2, 3, 4] } + data = @jwa.sign(inpt, private_key) + expect(@jwa.verify(inpt, data, public_key)).to eq(true) + expect(@jwa.verify(inpt, data, wrong_public_key)).to eq(false) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f3e98472..58b34426 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,8 @@ require "codeclimate-test-reporter" CodeClimate::TestReporter.start +CERT_PATH = File.join(File.dirname(__FILE__), '..', 'tmp', 'certs') + RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] From 92ea9377ceacf702ea172ee90b579cbfdd817a44 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 25 Dec 2014 01:32:42 +0100 Subject: [PATCH 07/35] JWA ECDSA implementation [in theory] ECDSA cannot be implemented without fixing following bug in the ruby openssl libs: https://bugs.ruby-lang.org/issues/5600 --- lib/jwa.rb | 1 + lib/jwa/ecdsa.rb | 15 ++++++++++ spec/lib/jwa_spec.rb | 69 ++++++++++++++++++++++++-------------------- 3 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 lib/jwa/ecdsa.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 5958ce1f..84bb586f 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,4 +1,5 @@ module JWA + autoload :ES, 'jwa/ecdsa' autoload :HS, 'jwa/hmac' autoload :RS, 'jwa/rsassa' diff --git a/lib/jwa/ecdsa.rb b/lib/jwa/ecdsa.rb new file mode 100644 index 00000000..33fe6c5a --- /dev/null +++ b/lib/jwa/ecdsa.rb @@ -0,0 +1,15 @@ +module JWA + class ES < Base + def initialize(bits) + @algorithm = OpenSSL::Digest.new("sha#{bits}") + end + + def sign(input, ec_key) + ec_key.sign(@algorithm, normalize_input(input)) + end + + def verify(input, signature, ec_key) + ec_key.verify(@algorithm, signature, normalize_input(input)) + end + end +end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index 7aa7b6f7..876122eb 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -80,41 +80,48 @@ end end - [256, 384, 512].each do |bit| - context "RS#{bit} openssl sign -> ruby verify" do - it 'should verify' - it 'should not verify' - end - end + context 'ECDSA signing, verifying', skip: '@see: https://bugs.ruby-lang.org/issues/5600' do + let(:input) { 'my super awesome input' } + [256, 384, 512].each do |bit| + let(:private_key) do + OpenSSL::PKey::EC.new File.read(File.join(CERT_PATH, 'jwa', "ec#{bit}-private.pem")) + end - [256, 384, 512].each do |bit| - context "RS#{bit} ruby sign -> openssl verify" do - it 'should verify' - it 'should not verify' - end - end + let(:public_key) do + OpenSSL::PKey::EC.new File.read(File.join(CERT_PATH, 'jwa', "ec#{bit}-public.pem")) + end - context 'ECDSA signing, verifying' do - [256, 384, 512].each do |bit| - it "#{bit} should verify" - it "#{bit} should not verify" - it "#{bit} missing sign key" - it "#{bit} missing verify key" - it "#{bit} weird input" - end - end + let(:wrong_public_key) do + OpenSSL::PKey::EC.new File.read(File.join(CERT_PATH, 'jwa', "ec#{bit}-wrong-public.pem")) + end - [256, 384, 512].each do |bit| - context "ES#{bit} openssl sign -> ruby verify" do - it 'should verify' - it 'should not verify' - end - end + before(:each) do + @jwa = JWA.create("ES#{bit}") + @sig = @jwa.sign(input, private_key) + end + + it "#{bit} should verify" do + expect(@jwa.verify(input, @sig, public_key)).to eq(true) + end + + it "#{bit} should not verify" do + expect(@jwa.verify(input, @sig, wrong_public_key)).to eq(false) + end - [256, 384, 512].each do |bit| - context "ES#{bit} ruby sign -> openssl verify" do - it 'should verify' - it 'should not verify' + it "#{bit} missing sign key" do + expect{@jwa.sign(input)}.to raise_error + end + + it "#{bit} missing verify key" do + expect{@jwa.verify(input, @sig)}.to raise_error + end + + it "#{bit} weird input" do + inpt = { a: [1, 2, 3, 4] } + data = @jwa.sign(inpt, private_key) + expect(@jwa.verify(inpt, data, public_key)).to eq(true) + expect(@jwa.verify(inpt, data, wrong_public_key)).to eq(false) + end end end From af564c154f03758cc1754a2334bba6b268f897a2 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 25 Dec 2014 01:39:25 +0100 Subject: [PATCH 08/35] Enable travis-ci docker containers --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b9932b3d..27d82551 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: ruby rvm: - 1.9.2 From 277115e73842373791eb8a0daa46cd0c2d87c7bd Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 25 Dec 2014 02:38:38 +0100 Subject: [PATCH 09/35] JWA NONE implementation Add simple plain verification and signing functions. Uncomment and disable ecdsa features. --- lib/jwa.rb | 18 +++++++++++++----- lib/jwa/ecdsa.rb | 30 +++++++++++++++--------------- lib/jwa/none.rb | 14 ++++++++++++++ spec/lib/jwa_spec.rb | 16 ++++++++++++++-- 4 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 lib/jwa/none.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 84bb586f..d1a9f746 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,7 +1,8 @@ module JWA - autoload :ES, 'jwa/ecdsa' +# autoload :ES, 'jwa/ecdsa' autoload :HS, 'jwa/hmac' autoload :RS, 'jwa/rsassa' + autoload :Plain, 'jwa/none' class Base def normalize_input(input) @@ -13,17 +14,24 @@ def normalize_input(input) def self.create(algorithm) klass = nil - algo = algorithm.match(/(ES|HS|RS)(256|384|512)/) + algo = algorithm.match(/(HS|RS)(256|384|512)/) - raise ArgumentError.new('Unsupported algorithm.') unless algo + raise ArgumentError.new('Unsupported algorithm.') unless algo or algorithm == 'NONE' + + if algorithm == 'NONE' + algo = [] + algo[1] = algorithm + end case algo[1] - when 'ES' - klass = ES.new algo[2].to_i +# when 'ES' +# klass = ES.new algo[2].to_i when 'HS' klass = HS.new algo[2].to_i when 'RS' klass = RS.new algo[2].to_i + when 'NONE' + klass = Plain.new end klass diff --git a/lib/jwa/ecdsa.rb b/lib/jwa/ecdsa.rb index 33fe6c5a..0b3a6f19 100644 --- a/lib/jwa/ecdsa.rb +++ b/lib/jwa/ecdsa.rb @@ -1,15 +1,15 @@ -module JWA - class ES < Base - def initialize(bits) - @algorithm = OpenSSL::Digest.new("sha#{bits}") - end - - def sign(input, ec_key) - ec_key.sign(@algorithm, normalize_input(input)) - end - - def verify(input, signature, ec_key) - ec_key.verify(@algorithm, signature, normalize_input(input)) - end - end -end +#module JWA +# class ES < Base +# def initialize(bits) +# @algorithm = OpenSSL::Digest.new("sha#{bits}") +# end +# +# def sign(input, ec_key) +# ec_key.sign(@algorithm, normalize_input(input)) +# end +# +# def verify(input, signature, ec_key) +# ec_key.verify(@algorithm, signature, normalize_input(input)) +# end +# end +#end diff --git a/lib/jwa/none.rb b/lib/jwa/none.rb new file mode 100644 index 00000000..06eed185 --- /dev/null +++ b/lib/jwa/none.rb @@ -0,0 +1,14 @@ +module JWA + class Plain < Base + def initialize + end + + def sign(input) + normalize_input input + end + + def verify(input, signature) + signature === sign(input) + end + end +end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index 876122eb..54639d05 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -126,8 +126,20 @@ end context 'NONE' do - it 'should verify' - it 'should not verify' + let(:input) { 'my super awesome input' } + + before(:each) do + @jwa = JWA.create('NONE') + @sig = @jwa.sign(input) + end + + it 'should verify' do + expect(@jwa.verify(input, @sig)).to eq(true) + end + + it 'should not verify' do + expect(@jwa.verify(input, JWA.create('NONE').sign('test'))).to eq(false) + end end context 'unsupported algorithm' do From 816dbe5ebb8ceca83f46a04e977af5a732c6a43b Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 27 Dec 2014 05:16:23 +0100 Subject: [PATCH 10/35] Drop old ideas, restart at HS256 only Drop old code. Reformat code. Drop current code. Add first specs to cover simple HS256 decoding and encoding. --- .gitignore | 2 + .rspec | 1 - Rakefile | 12 ++-- lib/jwa.rb | 39 ----------- lib/jwa/ecdsa.rb | 15 ---- lib/jwa/hmac.rb | 15 ---- lib/jwa/none.rb | 14 ---- lib/jwa/rsassa.rb | 15 ---- lib/jws.rb | 2 - lib/jwt.rb | 163 ++++++++----------------------------------- lib/jwt/json.rb | 2 +- spec/jwt_spec.rb | 98 +++++++++++++------------- spec/lib/jwa_spec.rb | 151 --------------------------------------- spec/lib/jws_spec.rb | 42 ----------- spec/lib/jwt_spec.rb | 108 ++++++---------------------- 15 files changed, 106 insertions(+), 573 deletions(-) delete mode 100644 lib/jwa.rb delete mode 100644 lib/jwa/ecdsa.rb delete mode 100644 lib/jwa/hmac.rb delete mode 100644 lib/jwa/none.rb delete mode 100644 lib/jwa/rsassa.rb delete mode 100644 lib/jws.rb delete mode 100644 spec/lib/jwa_spec.rb delete mode 100644 spec/lib/jws_spec.rb diff --git a/.gitignore b/.gitignore index 0568cfa3..c4672d55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +/.idea /coverage /tmp +*.iml jwt.gemspec pkg Gemfile.lock diff --git a/.rspec b/.rspec index 16f9cdb0..4e1e0d2f 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1 @@ --color ---format documentation diff --git a/Rakefile b/Rakefile index 8a31b3ab..c38f1963 100644 --- a/Rakefile +++ b/Rakefile @@ -3,13 +3,13 @@ require 'rake' require 'echoe' Echoe.new('jwt', '1.2.0') do |p| - p.description = "JSON Web Token implementation in Ruby" - p.url = "http://github.com/progrium/ruby-jwt" - p.author = "Jeff Lindsay" - p.email = "progrium@gmail.com" - p.ignore_pattern = ["tmp/*"] + p.description = "JSON Web Token implementation in Ruby" + p.url = "http://github.com/progrium/ruby-jwt" + p.author = "Jeff Lindsay" + p.email = "progrium@gmail.com" + p.ignore_pattern = ["tmp/*"] p.development_dependencies = ["echoe >=4.6.3"] - p.licenses = "MIT" + p.licenses = "MIT" end task :test do diff --git a/lib/jwa.rb b/lib/jwa.rb deleted file mode 100644 index d1a9f746..00000000 --- a/lib/jwa.rb +++ /dev/null @@ -1,39 +0,0 @@ -module JWA -# autoload :ES, 'jwa/ecdsa' - autoload :HS, 'jwa/hmac' - autoload :RS, 'jwa/rsassa' - autoload :Plain, 'jwa/none' - - class Base - def normalize_input(input) - input = input.to_json unless input.is_a? String - input - end - protected :normalize_input - end - - def self.create(algorithm) - klass = nil - algo = algorithm.match(/(HS|RS)(256|384|512)/) - - raise ArgumentError.new('Unsupported algorithm.') unless algo or algorithm == 'NONE' - - if algorithm == 'NONE' - algo = [] - algo[1] = algorithm - end - - case algo[1] -# when 'ES' -# klass = ES.new algo[2].to_i - when 'HS' - klass = HS.new algo[2].to_i - when 'RS' - klass = RS.new algo[2].to_i - when 'NONE' - klass = Plain.new - end - - klass - end -end diff --git a/lib/jwa/ecdsa.rb b/lib/jwa/ecdsa.rb deleted file mode 100644 index 0b3a6f19..00000000 --- a/lib/jwa/ecdsa.rb +++ /dev/null @@ -1,15 +0,0 @@ -#module JWA -# class ES < Base -# def initialize(bits) -# @algorithm = OpenSSL::Digest.new("sha#{bits}") -# end -# -# def sign(input, ec_key) -# ec_key.sign(@algorithm, normalize_input(input)) -# end -# -# def verify(input, signature, ec_key) -# ec_key.verify(@algorithm, signature, normalize_input(input)) -# end -# end -#end diff --git a/lib/jwa/hmac.rb b/lib/jwa/hmac.rb deleted file mode 100644 index 294768d2..00000000 --- a/lib/jwa/hmac.rb +++ /dev/null @@ -1,15 +0,0 @@ -module JWA - class HS < Base - def initialize(bits) - @algorithm = OpenSSL::Digest.new("sha#{bits}") - end - - def sign(input, secret) - OpenSSL::HMAC.digest(@algorithm, secret, normalize_input(input)) - end - - def verify(input, signature, secret) - sign(input, secret) === signature - end - end -end diff --git a/lib/jwa/none.rb b/lib/jwa/none.rb deleted file mode 100644 index 06eed185..00000000 --- a/lib/jwa/none.rb +++ /dev/null @@ -1,14 +0,0 @@ -module JWA - class Plain < Base - def initialize - end - - def sign(input) - normalize_input input - end - - def verify(input, signature) - signature === sign(input) - end - end -end diff --git a/lib/jwa/rsassa.rb b/lib/jwa/rsassa.rb deleted file mode 100644 index fda7f50f..00000000 --- a/lib/jwa/rsassa.rb +++ /dev/null @@ -1,15 +0,0 @@ -module JWA - class RS < Base - def initialize(bits) - @algorithm = OpenSSL::Digest.new("sha#{bits}") - end - - def sign(input, rsa_key) - rsa_key.sign(@algorithm, normalize_input(input)) - end - - def verify(input, signature, rsa_key) - rsa_key.verify(@algorithm, signature, normalize_input(input)) - end - end -end diff --git a/lib/jws.rb b/lib/jws.rb deleted file mode 100644 index d8a185d1..00000000 --- a/lib/jws.rb +++ /dev/null @@ -1,2 +0,0 @@ -module JWS -end diff --git a/lib/jwt.rb b/lib/jwt.rb index 66f9271b..f44623e4 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,153 +1,46 @@ -# -# JSON Web Token implementation -# -# Should be up to date with the latest spec: -# http://self-issued.info/docs/draft-jones-json-web-token-06.html - -require "base64" -require "openssl" -require "jwt/json" +require 'openssl' +require 'base64' +require 'json' module JWT - class DecodeError < StandardError; end - class ExpiredSignature < StandardError; end - extend JWT::Json - - module_function - - def sign(algorithm, msg, key) - if ["HS256", "HS384", "HS512"].include?(algorithm) - sign_hmac(algorithm, msg, key) - elsif ["RS256", "RS384", "RS512"].include?(algorithm) - sign_rsa(algorithm, msg, key) - else - raise NotImplementedError.new("Unsupported signing method") - end - end - - def sign_rsa(algorithm, msg, private_key) - private_key.sign(OpenSSL::Digest.new(algorithm.sub("RS", "sha")), msg) + class DecodeError < Exception; end - - def verify_rsa(algorithm, public_key, signing_input, signature) - public_key.verify(OpenSSL::Digest.new(algorithm.sub("RS", "sha")), signature, signing_input) + class EncodeError < Exception; end - def sign_hmac(algorithm, msg, key) - OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub("HS", "sha")), key, msg) - end - - def base64url_decode(str) - str += "=" * (4 - str.length.modulo(4)) - Base64.decode64(str.tr("-_", "+/")) - end + def self.encode(payload, secret_or_key, algorithm = 'HS256', head = {}) + header = { + 'alg' => algorithm, + 'typ' => 'JWT' + } - def base64url_encode(str) - Base64.encode64(str).tr("+/", "-_").gsub(/[\n=]/, "") - end + token = [] - def encoded_header(algorithm="HS256", header_fields={}) - header = {"typ" => "JWT", "alg" => algorithm}.merge(header_fields) - base64url_encode(encode_json(header)) - end + token << Base64.urlsafe_encode64(header.merge(head).to_json) + token << Base64.urlsafe_encode64(payload.to_json) + token << Base64.urlsafe_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_or_key, token.join('.'))) - def encoded_payload(payload) - base64url_encode(encode_json(payload)) + token.join '.' end - def encoded_signature(signing_input, key, algorithm) - if algorithm == "none" - "" - else - signature = sign(algorithm, signing_input, key) - base64url_encode(signature) + def self.decode(token, secret_or_key = nil, verify = true, options = {}, &keyfinder) + begin + header, payload, signature = token.split('.') + + header = JSON.parse(Base64.urlsafe_decode64(header)) + payload = JSON.parse(Base64.urlsafe_decode64(payload)) + signature = Base64.urlsafe_decode64(signature) + rescue Exception => e + raise JWT::DecodeError end - end - def encode(payload, key, algorithm="HS256", header_fields={}) - algorithm ||= "none" - segments = [] - segments << encoded_header(algorithm, header_fields) - segments << encoded_payload(payload) - segments << encoded_signature(segments.join("."), key, algorithm) - segments.join(".") - end - - def raw_segments(jwt, verify=true) - segments = jwt.split(".") - required_num_segments = verify ? [3] : [2,3] - raise JWT::DecodeError.new("Not enough or too many segments") unless required_num_segments.include? segments.length - segments - end - - def decode_header_and_payload(header_segment, payload_segment) - header = decode_json(base64url_decode(header_segment)) - payload = decode_json(base64url_decode(payload_segment)) - [header, payload] - end - - def decoded_segments(jwt, verify=true) - header_segment, payload_segment, crypto_segment = raw_segments(jwt, verify) - header, payload = decode_header_and_payload(header_segment, payload_segment) - signature = base64url_decode(crypto_segment.to_s) if verify - signing_input = [header_segment, payload_segment].join(".") - [header, payload, signature, signing_input] - end - - def decode(jwt, key=nil, verify=true, options={}, &keyfinder) - raise JWT::DecodeError.new("Nil JSON web token") unless jwt - - header, payload, signature, signing_input = decoded_segments(jwt, verify) - raise JWT::DecodeError.new("Not enough or too many segments") unless header && payload - - default_options = { - :verify_expiration => true, - :leeway => 0 - } - options = default_options.merge(options) + valid = false if verify - algo, key = signature_algorithm_and_key(header, key, &keyfinder) - verify_signature(algo, key, signing_input, signature) - end - if options[:verify_expiration] && payload.include?('exp') - raise JWT::ExpiredSignature.new("Signature has expired") unless payload['exp'] > (Time.now.to_i - options[:leeway]) - end - return payload,header - end - - def signature_algorithm_and_key(header, key, &keyfinder) - if keyfinder - key = keyfinder.call(header) + valid = signature === Base64.urlsafe_decode64(encode(payload, secret_or_key, header['alg'], header).split('.').last()) + raise JWT::DecodeError unless valid end - [header['alg'], key] - end - def verify_signature(algo, key, signing_input, signature) - begin - if ["HS256", "HS384", "HS512"].include?(algo) - raise JWT::DecodeError.new("Signature verification failed") unless secure_compare(signature, sign_hmac(algo, signing_input, key)) - elsif ["RS256", "RS384", "RS512"].include?(algo) - raise JWT::DecodeError.new("Signature verification failed") unless verify_rsa(algo, key, signing_input, signature) - else - raise JWT::DecodeError.new("Algorithm not supported") - end - rescue OpenSSL::PKey::PKeyError - raise JWT::DecodeError.new("Signature verification failed") - ensure - OpenSSL.errors.clear - end + [header, payload, signature, valid] end - - # From devise - # constant-time comparison algorithm to prevent timing attacks - def secure_compare(a, b) - return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize - l = a.unpack "C#{a.bytesize}" - - res = 0 - b.each_byte { |byte| res |= byte ^ l.shift } - res == 0 - end - end diff --git a/lib/jwt/json.rb b/lib/jwt/json.rb index 49e1f92e..07eae11f 100644 --- a/lib/jwt/json.rb +++ b/lib/jwt/json.rb @@ -27,4 +27,4 @@ def encode_json(raw) end end end -end \ No newline at end of file +end diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index bcb9237a..afdbadc0 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -3,26 +3,26 @@ describe JWT do before do - @payload = {"foo" => "bar", "exp" => Time.now.to_i + 1} + @payload = { "foo" => "bar", "exp" => Time.now.to_i + 1 } end it "encodes and decodes JWTs" do - secret = "secret" - jwt = JWT.encode(@payload, secret) + secret = "secret" + jwt = JWT.encode(@payload, secret) decoded_payload = JWT.decode(jwt, secret) expect(decoded_payload).to include(@payload) end it "encodes and decodes JWTs for RSA signatures" do - private_key = OpenSSL::PKey::RSA.generate(512) - jwt = JWT.encode(@payload, private_key, "RS256") + private_key = OpenSSL::PKey::RSA.generate(512) + jwt = JWT.encode(@payload, private_key, "RS256") decoded_payload = JWT.decode(jwt, private_key.public_key) expect(decoded_payload).to include(@payload) end it "encodes and decodes JWTs with custom header fields" do - private_key = OpenSSL::PKey::RSA.generate(512) - jwt = JWT.encode(@payload, private_key, "RS256", {"kid" => 'default'}) + private_key = OpenSSL::PKey::RSA.generate(512) + jwt = JWT.encode(@payload, private_key, "RS256", { "kid" => 'default' }) decoded_payload = JWT.decode(jwt) do |header| expect(header["kid"]).to eq('default') private_key.public_key @@ -31,9 +31,9 @@ end it "decodes valid JWTs" do - example_payload = {"hello" => "world"} - example_secret = 'secret' - example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8' + example_payload = { "hello" => "world" } + example_secret = 'secret' + example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8' decoded_payload = JWT.decode(example_jwt, example_secret) expect(decoded_payload).to include(example_payload) end @@ -41,27 +41,27 @@ it "raises exception when the token is invalid" do example_secret = 'secret' # Same as above exmaple with some random bytes replaced - example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHiMomlwIjogIkJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8' + example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHiMomlwIjogIkJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8' expect { JWT.decode(example_jwt, example_secret) }.to raise_error(JWT::DecodeError) end it "raises exception with wrong hmac key" do right_secret = 'foo' - bad_secret = 'bar' - jwt_message = JWT.encode(@payload, right_secret, "HS256") + bad_secret = 'bar' + jwt_message = JWT.encode(@payload, right_secret, "HS256") expect { JWT.decode(jwt_message, bad_secret) }.to raise_error(JWT::DecodeError) end it "raises exception with wrong rsa key" do right_private_key = OpenSSL::PKey::RSA.generate(512) - bad_private_key = OpenSSL::PKey::RSA.generate(512) - jwt = JWT.encode(@payload, right_private_key, "RS256") + bad_private_key = OpenSSL::PKey::RSA.generate(512) + jwt = JWT.encode(@payload, right_private_key, "RS256") expect { JWT.decode(jwt, bad_private_key.public_key) }.to raise_error(JWT::DecodeError) end it "raises exception with invalid signature" do example_secret = 'secret' - example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.' + example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.' expect { JWT.decode(example_jwt, example_secret) }.to raise_error(JWT::DecodeError) end @@ -78,18 +78,18 @@ end it "allows decoding without key" do - right_secret = 'foo' - bad_secret = 'bar' - jwt = JWT.encode(@payload, right_secret) + right_secret = 'foo' + bad_secret = 'bar' + jwt = JWT.encode(@payload, right_secret) decoded_payload = JWT.decode(jwt, bad_secret, false) expect(decoded_payload).to include(@payload) end it "checks the key when verify is truthy" do right_secret = 'foo' - bad_secret = 'bar' - jwt = JWT.encode(@payload, right_secret) - verify = "yes" =~ /^y/i + bad_secret = 'bar' + jwt = JWT.encode(@payload, right_secret) + verify = "yes" =~ /^y/i expect { JWT.decode(jwt, bad_secret, verify) }.to raise_error(JWT::DecodeError) end @@ -111,8 +111,8 @@ end it "does not use == to compare digests" do - secret = "secret" - jwt = JWT.encode(@payload, secret) + secret = "secret" + jwt = JWT.encode(@payload, secret) crypto_segment = jwt.split(".").last signature = JWT.base64url_decode(crypto_segment) @@ -124,28 +124,28 @@ end it "raises error when expired" do - expired_payload = @payload.clone + expired_payload = @payload.clone expired_payload['exp'] = Time.now.to_i - 1 - secret = "secret" - jwt = JWT.encode(expired_payload, secret) + secret = "secret" + jwt = JWT.encode(expired_payload, secret) expect { JWT.decode(jwt, secret) }.to raise_error(JWT::ExpiredSignature) end - + it "performs normal decode with skipped expiration check" do - expired_payload = @payload.clone + expired_payload = @payload.clone expired_payload['exp'] = Time.now.to_i - 1 - secret = "secret" - jwt = JWT.encode(expired_payload, secret) - decoded_payload = JWT.decode(jwt, secret, true, {:verify_expiration => false}) + secret = "secret" + jwt = JWT.encode(expired_payload, secret) + decoded_payload = JWT.decode(jwt, secret, true, { :verify_expiration => false }) expect(decoded_payload).to include(expired_payload) end - + it "performs normal decode using leeway" do - expired_payload = @payload.clone + expired_payload = @payload.clone expired_payload['exp'] = Time.now.to_i - 2 - secret = "secret" - jwt = JWT.encode(expired_payload, secret) - decoded_payload = JWT.decode(jwt, secret, true, {:leeway => 3}) + secret = "secret" + jwt = JWT.encode(expired_payload, secret) + decoded_payload = JWT.decode(jwt, secret, true, { :leeway => 3 }) expect(decoded_payload).to include(expired_payload) end @@ -182,16 +182,16 @@ LDZZNliPhfENuKeC2MCGVXTEu8Cqhy1w6e4axavLlXoYf4laJIZ/e7au8SqDbY0B xwIDAQAB -----END PUBLIC KEY----- -PUBKEY + PUBKEY jwt = ( - 'eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY' + - 'XVkIjoiMTA2MDM1Nzg5MTY4OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSI' + - 'sImNpZCI6IjEwNjAzNTc4OTE2ODguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb' + - '20iLCJpZCI6IjExNjQ1MjgyNDMwOTg1Njc4MjE2MyIsInRva2VuX2hhc2giOiJ' + - '0Z2hEOUo4bjhWME4ydmN3NmVNaWpnIiwiaWF0IjoxMzIwNjcwOTc4LCJleHAiO' + - 'jEzMjA2NzQ4Nzh9.D8x_wirkxDElqKdJBcsIws3Ogesk38okz6MN7zqC7nEAA7' + - 'wcy1PxsROY1fmBvXSer0IQesAqOW-rPOCNReSn-eY8d53ph1x2HAF-AzEi3GOl' + - '6hFycH8wj7Su6JqqyEbIVLxE7q7DkAZGaMPkxbTHs1EhSd5_oaKQ6O4xO3ZnnT4' + 'eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY' + + 'XVkIjoiMTA2MDM1Nzg5MTY4OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSI' + + 'sImNpZCI6IjEwNjAzNTc4OTE2ODguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb' + + '20iLCJpZCI6IjExNjQ1MjgyNDMwOTg1Njc4MjE2MyIsInRva2VuX2hhc2giOiJ' + + '0Z2hEOUo4bjhWME4ydmN3NmVNaWpnIiwiaWF0IjoxMzIwNjcwOTc4LCJleHAiO' + + 'jEzMjA2NzQ4Nzh9.D8x_wirkxDElqKdJBcsIws3Ogesk38okz6MN7zqC7nEAA7' + + 'wcy1PxsROY1fmBvXSer0IQesAqOW-rPOCNReSn-eY8d53ph1x2HAF-AzEi3GOl' + + '6hFycH8wj7Su6JqqyEbIVLxE7q7DkAZGaMPkxbTHs1EhSd5_oaKQ6O4xO3ZnnT4' ) expect { JWT.decode(jwt, pubkey, true) }.to raise_error(JWT::DecodeError) end @@ -205,11 +205,11 @@ describe 'decoded_segments' do it "allows access to the decoded header and payload" do - secret = "secret" - jwt = JWT.encode(@payload, secret) + secret = "secret" + jwt = JWT.encode(@payload, secret) decoded_segments = JWT.decoded_segments(jwt) expect(decoded_segments.size).to eq(4) - expect(decoded_segments[0]).to eq({"typ" => "JWT", "alg" => "HS256"}) + expect(decoded_segments[0]).to eq({ "typ" => "JWT", "alg" => "HS256" }) expect(decoded_segments[1]).to eq(@payload) end end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb deleted file mode 100644 index 54639d05..00000000 --- a/spec/lib/jwa_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -require 'spec_helper' -require_relative '../../lib/jwa' - -describe JWA do - context 'HMAC signing, verifying' do - let(:input) { 'my super awesome input' } - let(:secret) { 'donottellanyone' } - - [256, 384, 512].each do |bit| - before(:each) do - @jwa = JWA.create "HS#{bit}" - @sig = @jwa.sign input, secret - end - - it "#{bit} should verify" do - expect(@jwa.verify(input, @sig, secret)).to eq(true) - end - - it "#{bit} should not verify" do - expect(@jwa.verify('different', @sig, secret)).to eq(false) - expect(@jwa.verify(input, 'different', secret)).to eq(false) - expect(@jwa.verify(input, @sig, 'let me in')).to eq(false) - end - - it "#{bit} missing secret" do - expect{ @jwa.sign(input) }.to raise_error - end - - it "#{bit} weird input" do - inpt = { a: [1, 2, 3, 4] } - data = @jwa.sign(inpt, secret) - expect(@jwa.verify(inpt, data, secret)).to eq(true) - expect(@jwa.verify(inpt, data, 'let me in')).to eq(false) - end - end - end - - context 'RSASSA signing, veryfying' do - let(:input) { 'my super awesome input' } - let(:public_key) do - OpenSSL::PKey::RSA.new File.read(File.join(CERT_PATH, 'jwa', 'rsa-public.pem')) - end - - let(:private_key) do - OpenSSL::PKey::RSA.new File.read(File.join(CERT_PATH, 'jwa', 'rsa-private.pem')) - end - - let(:wrong_public_key) do - OpenSSL::PKey::RSA.new File.read(File.join(CERT_PATH, 'jwa', 'rsa-wrong-public.pem')) - end - - [256, 384, 512].each do |bit| - before(:each) do - @jwa = JWA.create "RS#{bit}" - @sig = @jwa.sign input, private_key - end - - it "#{bit} should verify" do - expect(@jwa.verify(input, @sig, public_key)).to eq(true) - end - - it "#{bit} should not verify" do - expect(@jwa.verify(input, @sig, wrong_public_key)).to eq(false) - end - - it "#{bit} missing sign key" do - expect{@jwa.sign(input)}.to raise_error - end - - it "#{bit} missing verify key" do - expect{@jwa.verify(input, @sig)}.to raise_error - end - - it "#{bit} weird input" do - inpt = { a: [1, 2, 3, 4] } - data = @jwa.sign(inpt, private_key) - expect(@jwa.verify(inpt, data, public_key)).to eq(true) - expect(@jwa.verify(inpt, data, wrong_public_key)).to eq(false) - end - end - end - - context 'ECDSA signing, verifying', skip: '@see: https://bugs.ruby-lang.org/issues/5600' do - let(:input) { 'my super awesome input' } - [256, 384, 512].each do |bit| - let(:private_key) do - OpenSSL::PKey::EC.new File.read(File.join(CERT_PATH, 'jwa', "ec#{bit}-private.pem")) - end - - let(:public_key) do - OpenSSL::PKey::EC.new File.read(File.join(CERT_PATH, 'jwa', "ec#{bit}-public.pem")) - end - - let(:wrong_public_key) do - OpenSSL::PKey::EC.new File.read(File.join(CERT_PATH, 'jwa', "ec#{bit}-wrong-public.pem")) - end - - before(:each) do - @jwa = JWA.create("ES#{bit}") - @sig = @jwa.sign(input, private_key) - end - - it "#{bit} should verify" do - expect(@jwa.verify(input, @sig, public_key)).to eq(true) - end - - it "#{bit} should not verify" do - expect(@jwa.verify(input, @sig, wrong_public_key)).to eq(false) - end - - it "#{bit} missing sign key" do - expect{@jwa.sign(input)}.to raise_error - end - - it "#{bit} missing verify key" do - expect{@jwa.verify(input, @sig)}.to raise_error - end - - it "#{bit} weird input" do - inpt = { a: [1, 2, 3, 4] } - data = @jwa.sign(inpt, private_key) - expect(@jwa.verify(inpt, data, public_key)).to eq(true) - expect(@jwa.verify(inpt, data, wrong_public_key)).to eq(false) - end - end - end - - context 'NONE' do - let(:input) { 'my super awesome input' } - - before(:each) do - @jwa = JWA.create('NONE') - @sig = @jwa.sign(input) - end - - it 'should verify' do - expect(@jwa.verify(input, @sig)).to eq(true) - end - - it 'should not verify' do - expect(@jwa.verify(input, JWA.create('NONE').sign('test'))).to eq(false) - end - end - - context 'unsupported algorithm' do - it 'should throw' do - expect{ JWA.create('invalid') }.to raise_error - expect{ JWA.create('HS255') }.to raise_error - end - end -end diff --git a/spec/lib/jws_spec.rb b/spec/lib/jws_spec.rb deleted file mode 100644 index 2a72310b..00000000 --- a/spec/lib/jws_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' -require_relative '../../lib/jws' - -describe JWS do - [256, 384, 512].each do |bit| - context "HMAC using SHA-#{bit} hash algorithm" do - it 'should verify' - it 'should not verify' - it 'should match payload' - it 'should match header' - end - end - - [256, 384, 512].each do |bit| - context "RSASSA using SHA-#{bit} hash algorithm" do - it 'should verify' - it 'should not verify' - it 'should match payload' - it 'should match header' - end - end - - [256, 384, 512].each do |bit| - context "ECDSA using P-#{bit} curve and SHA-#{bit} hash algorithm" do - it 'should verify' - it 'should not verify' - it 'should match payload' - it 'should match header' - end - end - - context 'NONE' do - it 'should verify' - it 'should still verify' - it 'should match payload' - it 'should match header' - end - - context 'unsupported algorithm' do - it 'should throw' - end -end diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index e588bb8f..fbb0425a 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -1,98 +1,30 @@ require 'spec_helper' -require_relative '../../lib/jwt' +require 'jwt' describe JWT do - context 'RSASSA' do - %w(RS256 RS384 RS512).each do |algorithm| - context "[#{algorithm}] when signing a token" do - it 'should be syntactically valid' - it 'should validate with public key' - it 'should throw with invalid public key' - end + let(:jwt_header) { { 'alg' => 'HS256', 'typ' => 'JWT' } } + let(:jwt_header_base64) { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' } + let(:jwt_payload) { { 'sub' => 1234567890, 'name' => 'John Doe', 'admin' => true } } + let(:jwt_payload_base64) { 'eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ==' } + let(:secret) { 'secret' } + let(:jwt_signature_base64) { 'zzPVwrCDlyRQSEMsDCLrq4cjMl5t88H5T2msS_HgdqI=' } + let(:token) { "#{jwt_header_base64}.#{jwt_payload_base64}.#{jwt_signature_base64}" } + + context 'encode' do + it 'should match given pre-caclculated result' do + jwt = JWT.encode(jwt_payload, secret) + expect(jwt).to eq("#{jwt_header_base64}.#{jwt_payload_base64}.#{jwt_signature_base64}") end end - context 'HMAC' do - let(:secret) { 'valid-secret' } + context 'decode' do + it 'should match given input data' do + header, payload, signature, valid = JWT.decode(token, secret) - %w(HS256 HS384 HS512).each do |algorithm| - context "[#{algorithm}] when signing a token" do - it 'should be syntactically valid' - it 'should without options' - it 'should validate with secret' - it 'should throw with invalid secret' - it 'should throw with secret and token not signed' - it 'should throw when verifying null' - it 'should throw when the payload is not json' - end + expect(header).to eq(jwt_header) + expect(payload).to eq(jwt_payload) + expect(signature).to eq(Base64.urlsafe_decode64(jwt_signature_base64)) + expect(valid).to eq(true) end end - - context 'ECDSA' do - %w(ES256 ES384 ES512).each do |algorithm| - context "[#{algorithm}] when signing a token" do - it 'should be syntactically valid' - it 'should without options' - it 'should validate with secret' - it 'should throw with invalid secret' - it 'should throw with secret and token not signed' - it 'should throw when verifying null' - it 'should throw when the payload is not json' - end - end - end - - context 'none' do - - end - - context 'when signing a token with expiration' do - it 'should be valid expiration' - it 'should be invalid' - end - - context 'when signing a token with audience' do - it 'should check audience' - it 'should check audience in array' - it 'should throw when invalid audience' - it 'should throw when invalid audience in array' - end - - context 'when signing a token with array audience' do - it 'should check audience' - it 'should check other audience' - it 'should check audience in array' - it 'should throw when invalid audience' - it 'should throw when invalid audience in array' - end - - context 'when signing a token without audience' do - it 'should check audience' - it 'should check audience in array' - end - - context 'when signing a token with issuer' do - it 'should check issuer' - it 'should throw when invalid issuer' - end - - context 'when signing a token without issuer' do - it 'should check issuer' - end - - context 'when verifying a malformed token' do - it 'should throw' - end - - context 'when decoding a jwt token with additional parts' do - it 'should throw' - end - - context 'when decoding a invalid jwt token' do - it 'should return nil' - end - - context 'when decoding a valid jwt token' do - it 'should return the payload' - end end From e694fc4db18dfd966a69f4263d4177124e01811d Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 27 Dec 2014 05:32:42 +0100 Subject: [PATCH 11/35] Improve JWT::DecodeError exception output --- lib/jwt.rb | 6 +++--- spec/lib/jwt_spec.rb | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index f44623e4..faaea2ac 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -26,19 +26,19 @@ def self.encode(payload, secret_or_key, algorithm = 'HS256', head = {}) def self.decode(token, secret_or_key = nil, verify = true, options = {}, &keyfinder) begin header, payload, signature = token.split('.') - + header = JSON.parse(Base64.urlsafe_decode64(header)) payload = JSON.parse(Base64.urlsafe_decode64(payload)) signature = Base64.urlsafe_decode64(signature) rescue Exception => e - raise JWT::DecodeError + raise JWT::DecodeError.new e.message end valid = false if verify valid = signature === Base64.urlsafe_decode64(encode(payload, secret_or_key, header['alg'], header).split('.').last()) - raise JWT::DecodeError unless valid + raise JWT::DecodeError.new('Token verification failed. Data corrupted or pass phrase incorrect.') unless valid end [header, payload, signature, valid] diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index fbb0425a..19128656 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -7,6 +7,7 @@ let(:jwt_payload) { { 'sub' => 1234567890, 'name' => 'John Doe', 'admin' => true } } let(:jwt_payload_base64) { 'eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ==' } let(:secret) { 'secret' } + let(:wrong_secret) { 'wrong secret' } let(:jwt_signature_base64) { 'zzPVwrCDlyRQSEMsDCLrq4cjMl5t88H5T2msS_HgdqI=' } let(:token) { "#{jwt_header_base64}.#{jwt_payload_base64}.#{jwt_signature_base64}" } @@ -19,6 +20,8 @@ context 'decode' do it 'should match given input data' do + expect { JWT.decode(token, secret) }.not_to raise_error + header, payload, signature, valid = JWT.decode(token, secret) expect(header).to eq(jwt_header) @@ -26,5 +29,15 @@ expect(signature).to eq(Base64.urlsafe_decode64(jwt_signature_base64)) expect(valid).to eq(true) end + + context 'raises DecodeError' do + it 'if verification fails' do + expect { JWT.decode(token, wrong_secret) }.to raise_error(JWT::DecodeError) + end + + it 'if input data is not valid' do + expect { JWT.decode([token, token].join, secret) }.to raise_error(JWT::DecodeError) + end + end end end From 9a20006adb7438b713a4bd60d74cf4b699411f64 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 27 Dec 2014 05:50:15 +0100 Subject: [PATCH 12/35] Add plain token handling --- lib/jwt.rb | 17 ++++++++++++++--- spec/lib/jwt_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index faaea2ac..58f9c99e 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -18,7 +18,14 @@ def self.encode(payload, secret_or_key, algorithm = 'HS256', head = {}) token << Base64.urlsafe_encode64(header.merge(head).to_json) token << Base64.urlsafe_encode64(payload.to_json) - token << Base64.urlsafe_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_or_key, token.join('.'))) + + signature = if algorithm != 'none' + Base64.urlsafe_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_or_key, token.join('.'))) + else + '' + end + + token << signature token.join '.' end @@ -29,14 +36,18 @@ def self.decode(token, secret_or_key = nil, verify = true, options = {}, &keyfin header = JSON.parse(Base64.urlsafe_decode64(header)) payload = JSON.parse(Base64.urlsafe_decode64(payload)) - signature = Base64.urlsafe_decode64(signature) + signature = if header['alg'] == 'none' + '' + else + Base64.urlsafe_decode64(signature) + end rescue Exception => e raise JWT::DecodeError.new e.message end valid = false - if verify + if verify && header['alg'] != 'none' valid = signature === Base64.urlsafe_decode64(encode(payload, secret_or_key, header['alg'], header).split('.').last()) raise JWT::DecodeError.new('Token verification failed. Data corrupted or pass phrase incorrect.') unless valid end diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index 19128656..5bfc9615 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -16,6 +16,17 @@ jwt = JWT.encode(jwt_payload, secret) expect(jwt).to eq("#{jwt_header_base64}.#{jwt_payload_base64}.#{jwt_signature_base64}") end + + it 'should create plain tokens' do + header = jwt_header + header['alg'] = 'none' + header = Base64.urlsafe_encode64(header.to_json) + token = "#{header}.#{jwt_payload_base64}." + + jwt = JWT.encode(jwt_payload, '', 'none') + + expect(jwt).to eq(token) + end end context 'decode' do @@ -30,6 +41,20 @@ expect(valid).to eq(true) end + it 'should handle plain tokens' do + h = jwt_header + h['alg'] = 'none' + hb64 = Base64.urlsafe_encode64(h.to_json) + token = "#{hb64}.#{jwt_payload_base64}." + + header, payload, signature, valid = JWT.decode(token) + + expect(header).to eq(h) + expect(payload).to eq(jwt_payload) + expect(signature).to eq('') + expect(valid).to eq(false) + end + context 'raises DecodeError' do it 'if verification fails' do expect { JWT.decode(token, wrong_secret) }.to raise_error(JWT::DecodeError) From a7d4fde049768d057f177cde66c070fa641887df Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 27 Dec 2014 06:57:27 +0100 Subject: [PATCH 13/35] Add custom header test, remove unused variables --- lib/jwt.rb | 2 +- spec/lib/jwt_spec.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index 58f9c99e..95cde3bb 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -30,7 +30,7 @@ def self.encode(payload, secret_or_key, algorithm = 'HS256', head = {}) token.join '.' end - def self.decode(token, secret_or_key = nil, verify = true, options = {}, &keyfinder) + def self.decode(token, secret_or_key = nil, verify = true) begin header, payload, signature = token.split('.') diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index 5bfc9615..5f8ba8c4 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -65,4 +65,19 @@ end end end + + it 'should preserve custom header fields' do + h = jwt_header + h['alg'] = 'none' + h['test'] = 'test' + hb64 = Base64.urlsafe_encode64(h.to_json) + token = "#{hb64}.#{jwt_payload_base64}." + + header, payload, signature, valid = JWT.decode(token) + + expect(header).to eq(h) + expect(payload).to eq(jwt_payload) + expect(signature).to eq('') + expect(valid).to eq(false) + end end From 707b8e8819ff50cb2913e877e61638ac108bf96b Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 27 Dec 2014 12:15:51 +0100 Subject: [PATCH 14/35] Remove old code --- lib/jwt.rb | 6 +- lib/jwt/json.rb | 30 ------- spec/jwt_spec.rb | 216 ----------------------------------------------- 3 files changed, 4 insertions(+), 248 deletions(-) delete mode 100644 lib/jwt/json.rb delete mode 100644 spec/jwt_spec.rb diff --git a/lib/jwt.rb b/lib/jwt.rb index 95cde3bb..5607191e 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -3,12 +3,14 @@ require 'json' module JWT + extend self + class DecodeError < Exception; end class EncodeError < Exception; end - def self.encode(payload, secret_or_key, algorithm = 'HS256', head = {}) + def encode(payload, secret_or_key, algorithm = 'HS256', head = {}) header = { 'alg' => algorithm, 'typ' => 'JWT' @@ -30,7 +32,7 @@ def self.encode(payload, secret_or_key, algorithm = 'HS256', head = {}) token.join '.' end - def self.decode(token, secret_or_key = nil, verify = true) + def decode(token, secret_or_key = nil, verify = true) begin header, payload, signature = token.split('.') diff --git a/lib/jwt/json.rb b/lib/jwt/json.rb deleted file mode 100644 index 07eae11f..00000000 --- a/lib/jwt/json.rb +++ /dev/null @@ -1,30 +0,0 @@ -module JWT - module Json - if RUBY_VERSION >= "1.9" && !defined?(MultiJson) - require 'json' - - def decode_json(encoded) - JSON.parse(encoded) - rescue JSON::ParserError - raise JWT::DecodeError.new("Invalid segment encoding") - end - - def encode_json(raw) - JSON.generate(raw) - end - - else - require "multi_json" - - def decode_json(encoded) - MultiJson.decode(encoded) - rescue MultiJson::LoadError - raise JWT::DecodeError.new("Invalid segment encoding") - end - - def encode_json(raw) - MultiJson.encode(raw) - end - end - end -end diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb deleted file mode 100644 index afdbadc0..00000000 --- a/spec/jwt_spec.rb +++ /dev/null @@ -1,216 +0,0 @@ -require 'spec_helper' -require_relative '../lib/jwt' - -describe JWT do - before do - @payload = { "foo" => "bar", "exp" => Time.now.to_i + 1 } - end - - it "encodes and decodes JWTs" do - secret = "secret" - jwt = JWT.encode(@payload, secret) - decoded_payload = JWT.decode(jwt, secret) - expect(decoded_payload).to include(@payload) - end - - it "encodes and decodes JWTs for RSA signatures" do - private_key = OpenSSL::PKey::RSA.generate(512) - jwt = JWT.encode(@payload, private_key, "RS256") - decoded_payload = JWT.decode(jwt, private_key.public_key) - expect(decoded_payload).to include(@payload) - end - - it "encodes and decodes JWTs with custom header fields" do - private_key = OpenSSL::PKey::RSA.generate(512) - jwt = JWT.encode(@payload, private_key, "RS256", { "kid" => 'default' }) - decoded_payload = JWT.decode(jwt) do |header| - expect(header["kid"]).to eq('default') - private_key.public_key - end - expect(decoded_payload).to include(@payload) - end - - it "decodes valid JWTs" do - example_payload = { "hello" => "world" } - example_secret = 'secret' - example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8' - decoded_payload = JWT.decode(example_jwt, example_secret) - expect(decoded_payload).to include(example_payload) - end - - it "raises exception when the token is invalid" do - example_secret = 'secret' - # Same as above exmaple with some random bytes replaced - example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHiMomlwIjogIkJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8' - expect { JWT.decode(example_jwt, example_secret) }.to raise_error(JWT::DecodeError) - end - - it "raises exception with wrong hmac key" do - right_secret = 'foo' - bad_secret = 'bar' - jwt_message = JWT.encode(@payload, right_secret, "HS256") - expect { JWT.decode(jwt_message, bad_secret) }.to raise_error(JWT::DecodeError) - end - - it "raises exception with wrong rsa key" do - right_private_key = OpenSSL::PKey::RSA.generate(512) - bad_private_key = OpenSSL::PKey::RSA.generate(512) - jwt = JWT.encode(@payload, right_private_key, "RS256") - expect { JWT.decode(jwt, bad_private_key.public_key) }.to raise_error(JWT::DecodeError) - end - - it "raises exception with invalid signature" do - example_secret = 'secret' - example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.' - expect { JWT.decode(example_jwt, example_secret) }.to raise_error(JWT::DecodeError) - end - - it "raises exception with nonexistent header" do - expect { JWT.decode("..stuff") }.to raise_error(JWT::DecodeError) - end - - it "raises exception with nonexistent payload" do - expect { JWT.decode("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9..stuff") }.to raise_error(JWT::DecodeError) - end - - it "raises exception with nil jwt" do - expect { JWT.decode(nil) }.to raise_error(JWT::DecodeError) - end - - it "allows decoding without key" do - right_secret = 'foo' - bad_secret = 'bar' - jwt = JWT.encode(@payload, right_secret) - decoded_payload = JWT.decode(jwt, bad_secret, false) - expect(decoded_payload).to include(@payload) - end - - it "checks the key when verify is truthy" do - right_secret = 'foo' - bad_secret = 'bar' - jwt = JWT.encode(@payload, right_secret) - verify = "yes" =~ /^y/i - expect { JWT.decode(jwt, bad_secret, verify) }.to raise_error(JWT::DecodeError) - end - - it "raises exception on unsupported crypto algorithm" do - expect { JWT.encode(@payload, "secret", 'HS1024') }.to raise_error(NotImplementedError) - end - - it "encodes and decodes plaintext JWTs" do - jwt = JWT.encode(@payload, nil, nil) - expect(jwt.split('.').length).to eq(2) - decoded_payload = JWT.decode(jwt, nil, nil) - expect(decoded_payload).to include(@payload) - end - - it "requires a signature segment when verify is truthy" do - jwt = JWT.encode(@payload, nil, nil) - expect(jwt.split('.').length).to eq(2) - expect { JWT.decode(jwt, nil, true) }.to raise_error(JWT::DecodeError) - end - - it "does not use == to compare digests" do - secret = "secret" - jwt = JWT.encode(@payload, secret) - crypto_segment = jwt.split(".").last - - signature = JWT.base64url_decode(crypto_segment) - expect(signature).not_to receive('==') - expect(JWT).to receive(:base64url_decode).with(crypto_segment).once.and_return(signature) - expect(JWT).to receive(:base64url_decode).at_least(:once).and_call_original - - JWT.decode(jwt, secret) - end - - it "raises error when expired" do - expired_payload = @payload.clone - expired_payload['exp'] = Time.now.to_i - 1 - secret = "secret" - jwt = JWT.encode(expired_payload, secret) - expect { JWT.decode(jwt, secret) }.to raise_error(JWT::ExpiredSignature) - end - - it "performs normal decode with skipped expiration check" do - expired_payload = @payload.clone - expired_payload['exp'] = Time.now.to_i - 1 - secret = "secret" - jwt = JWT.encode(expired_payload, secret) - decoded_payload = JWT.decode(jwt, secret, true, { :verify_expiration => false }) - expect(decoded_payload).to include(expired_payload) - end - - it "performs normal decode using leeway" do - expired_payload = @payload.clone - expired_payload['exp'] = Time.now.to_i - 2 - secret = "secret" - jwt = JWT.encode(expired_payload, secret) - decoded_payload = JWT.decode(jwt, secret, true, { :leeway => 3 }) - expect(decoded_payload).to include(expired_payload) - end - - describe "secure comparison" do - it "returns true if strings are equal" do - expect(JWT.secure_compare("Foo", "Foo")).to be true - end - - it "returns false if either input is nil or empty" do - [nil, ""].each do |bad| - expect(JWT.secure_compare(bad, "Foo")).to be false - expect(JWT.secure_compare("Foo", bad)).to be false - end - end - - it "retuns false if the strings are different" do - expect(JWT.secure_compare("Foo", "Bar")).to be false - end - end - - # no method should leave OpenSSL.errors populated - after do - expect(OpenSSL.errors).to be_empty - end - - it "raise exception on invalid signature" do - pubkey = OpenSSL::PKey::RSA.new(<<-PUBKEY) ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCaY7425h964bjaoLeUm -SlZ8sK7VtVk9zHbGmZh2ygGYwfuUf2bmMye2Ofv99yDE/rd4loVIAcu7RVvDRgHq -3/CZTnIrSvHsiJQsHBNa3d+F1ihPfzURzf1M5k7CFReBj2SBXhDXd57oRfBQj12w -CVhhwP6kGTAWuoppbIIIBfNF2lE/Nvm7lVVYQqL9xOrP/AQ4xRbpQlB8Ll9sO9Or -SvbWhCDa/LMOWxHdmrcJi6XoSg1vnOyCoKbyAoauTt/XqdkHbkDdQ6HFbJieu9il -LDZZNliPhfENuKeC2MCGVXTEu8Cqhy1w6e4axavLlXoYf4laJIZ/e7au8SqDbY0B -xwIDAQAB ------END PUBLIC KEY----- - PUBKEY - jwt = ( - 'eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY' + - 'XVkIjoiMTA2MDM1Nzg5MTY4OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSI' + - 'sImNpZCI6IjEwNjAzNTc4OTE2ODguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb' + - '20iLCJpZCI6IjExNjQ1MjgyNDMwOTg1Njc4MjE2MyIsInRva2VuX2hhc2giOiJ' + - '0Z2hEOUo4bjhWME4ydmN3NmVNaWpnIiwiaWF0IjoxMzIwNjcwOTc4LCJleHAiO' + - 'jEzMjA2NzQ4Nzh9.D8x_wirkxDElqKdJBcsIws3Ogesk38okz6MN7zqC7nEAA7' + - 'wcy1PxsROY1fmBvXSer0IQesAqOW-rPOCNReSn-eY8d53ph1x2HAF-AzEi3GOl' + - '6hFycH8wj7Su6JqqyEbIVLxE7q7DkAZGaMPkxbTHs1EhSd5_oaKQ6O4xO3ZnnT4' - ) - expect { JWT.decode(jwt, pubkey, true) }.to raise_error(JWT::DecodeError) - end - - describe "urlsafe base64 encoding" do - it "replaces + and / with - and _" do - allow(Base64).to receive(:encode64) { "string+with/non+url-safe/characters_" } - expect(JWT.base64url_encode("foo")).to eq("string-with_non-url-safe_characters_") - end - end - - describe 'decoded_segments' do - it "allows access to the decoded header and payload" do - secret = "secret" - jwt = JWT.encode(@payload, secret) - decoded_segments = JWT.decoded_segments(jwt) - expect(decoded_segments.size).to eq(4) - expect(decoded_segments[0]).to eq({ "typ" => "JWT", "alg" => "HS256" }) - expect(decoded_segments[1]).to eq(@payload) - end - end -end From d2e01d476dd6eb837b04083a259531c74e906347 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 27 Dec 2014 16:49:46 +0100 Subject: [PATCH 15/35] Initial JWA Implementation See: JSON Web Algorithms (only for digital signatures) https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3 --- lib/jwa.rb | 33 +++++++++++++++++++++++++++++++++ lib/jwt.rb | 1 + spec/lib/jwa_spec.rb | 36 ++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 2 +- 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 lib/jwa.rb create mode 100644 spec/lib/jwa_spec.rb diff --git a/lib/jwa.rb b/lib/jwa.rb new file mode 100644 index 00000000..96612a24 --- /dev/null +++ b/lib/jwa.rb @@ -0,0 +1,33 @@ +module JWA + extend self + + ALGORITHMS = %w(HS256 HS384 HS512 RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none) + + class InvalidPayloadFormat < ArgumentError + end + + class InvalidAlgorithm < ArgumentError + end + + def sign(algorithm, data) + check_algorithm algorithm + check_data data + end + + def verify(algorithm, data) + check_algorithm algorithm + check_data data + end + + def check_algorithm(algorithm) + raise JWA::InvalidAlgorithm.new("JWA: Given algorithm [#{algorithm.to_s}] is not part of the JWS supported algorithms.") unless ALGORITHMS.include? algorithm + end + + private :check_algorithm + + def check_data(data) + raise JWA::InvalidPayloadFormat.new('JWA: Given data is not a string.') unless data.is_a? String + end + + private :check_data +end diff --git a/lib/jwt.rb b/lib/jwt.rb index 5607191e..193bdf63 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,6 +1,7 @@ require 'openssl' require 'base64' require 'json' +require 'jwa' module JWT extend self diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb new file mode 100644 index 00000000..382f3fd7 --- /dev/null +++ b/spec/lib/jwa_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'jwa' + +describe JWA do + let(:string) { 'My awesome string that works' } + let(:hash) { { text: 'My awesome hash that should not work.' } } + + it 'should only accept registered, case-sensitive algorithms' do + %w(HS256 HS384 HS512 RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| + expect { JWA.sign(algo, string) }.not_to raise_error + expect { JWA.verify(algo, string) }.not_to raise_error + end + + expect { JWA.sign('RSA1_5', string) }.to raise_error(JWA::InvalidAlgorithm) + expect { JWA.verify('RS513', string) }.to raise_error(JWA::InvalidAlgorithm) + expect { JWA.sign('hs256', string) }.to raise_error(JWA::InvalidAlgorithm) + expect { JWA.verify('NONE', string) }.to raise_error(JWA::InvalidAlgorithm) + end + + it 'should only accepts strings as input data' do + algo = 'HS256' + + expect { JWA.sign(algo, hash) }.to raise_error(JWA::InvalidPayloadFormat) + expect { JWA.sign(algo, string) }.not_to raise_error + + expect { JWA.verify(algo, hash) }.to raise_error(JWA::InvalidPayloadFormat) + expect { JWA.verify(algo, string) }.not_to raise_error + end + + context 'sign' do + [256, 384, 512].each do |bits| + context "HMAC using SHA-#{bits}" do + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 58b34426..fdba7c30 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,4 @@ -require "codeclimate-test-reporter" +require 'codeclimate-test-reporter' CodeClimate::TestReporter.start CERT_PATH = File.join(File.dirname(__FILE__), '..', 'tmp', 'certs') From 157551268fd39acbe4ff59203ef713cffe71a06b Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sun, 28 Dec 2014 10:47:45 +0100 Subject: [PATCH 16/35] Add first JWA HMAC logic --- lib/jwa.rb | 40 +++++++++++++++++++++++++++++----------- lib/jwa/hmac.rb | 15 +++++++++++++++ spec/lib/jwa_spec.rb | 24 ++++++++++++++++-------- 3 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 lib/jwa/hmac.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 96612a24..c35f639c 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,3 +1,5 @@ +require 'jwa/hmac' + module JWA extend self @@ -9,25 +11,41 @@ class InvalidPayloadFormat < ArgumentError class InvalidAlgorithm < ArgumentError end - def sign(algorithm, data) - check_algorithm algorithm - check_data data + class MissingSecretOrKey < ArgumentError + end + + def sign(algorithm, data, secret_or_private_key = '') + algo, bits = validate_algorithm algorithm + validate_data data + + case algo + when 'HS' + JWA::HMAC.new(bits).sign(data, secret_or_private_key) + when 'RS' + when 'ES' + when 'PS' + when 'none' + end end - def verify(algorithm, data) - check_algorithm algorithm - check_data data + def verify(algorithm, data, secret_or_private_key = '') + validate_algorithm algorithm + validate_data data end - def check_algorithm(algorithm) - raise JWA::InvalidAlgorithm.new("JWA: Given algorithm [#{algorithm.to_s}] is not part of the JWS supported algorithms.") unless ALGORITHMS.include? algorithm + def validate_algorithm(algorithm) + raise JWA::InvalidAlgorithm.new("JWA: Given algorithm [#{algorithm.to_s}] is not part of the JWS supported algorithms.") unless ALGORITHMS.include? algorithm # + + match = algorithm.match(/(HS|RS|ES|PS|none)(\d+)?/) + + [match[1], match[2]] end - private :check_algorithm + private :validate_algorithm - def check_data(data) + def validate_data(data) raise JWA::InvalidPayloadFormat.new('JWA: Given data is not a string.') unless data.is_a? String end - private :check_data + private :validate_data end diff --git a/lib/jwa/hmac.rb b/lib/jwa/hmac.rb new file mode 100644 index 00000000..8ea27c13 --- /dev/null +++ b/lib/jwa/hmac.rb @@ -0,0 +1,15 @@ +module JWA + class HMAC + def initialize(bits) + + end + + def sign(data, secret) + validate_secret secret + end + + def validate_secret(secret) + raise JWA::MissingSecretOrKey.new('JWA: HMAC signing always requires a secret to be set.') if secret.length == 0 + end + end +end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index 382f3fd7..c40d052b 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -4,11 +4,13 @@ describe JWA do let(:string) { 'My awesome string that works' } let(:hash) { { text: 'My awesome hash that should not work.' } } + let(:secret) { 'TopSecret' } + let(:wrong_secret) { 'TopWrongSecret' } it 'should only accept registered, case-sensitive algorithms' do %w(HS256 HS384 HS512 RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| - expect { JWA.sign(algo, string) }.not_to raise_error - expect { JWA.verify(algo, string) }.not_to raise_error + expect { JWA.sign(algo, string, secret) }.not_to raise_error + expect { JWA.verify(algo, string, secret) }.not_to raise_error end expect { JWA.sign('RSA1_5', string) }.to raise_error(JWA::InvalidAlgorithm) @@ -20,16 +22,22 @@ it 'should only accepts strings as input data' do algo = 'HS256' - expect { JWA.sign(algo, hash) }.to raise_error(JWA::InvalidPayloadFormat) - expect { JWA.sign(algo, string) }.not_to raise_error + expect { JWA.sign(algo, hash, secret) }.to raise_error(JWA::InvalidPayloadFormat) + expect { JWA.sign(algo, string, secret) }.not_to raise_error - expect { JWA.verify(algo, hash) }.to raise_error(JWA::InvalidPayloadFormat) - expect { JWA.verify(algo, string) }.not_to raise_error + expect { JWA.verify(algo, hash, secret) }.to raise_error(JWA::InvalidPayloadFormat) + expect { JWA.verify(algo, string, secret) }.not_to raise_error end - context 'sign' do + context 'sign and verify using' do + let(:payload) { 'A very string-ish payload.' } + [256, 384, 512].each do |bits| - context "HMAC using SHA-#{bits}" do + context "HMAC SHA-#{bits} (HS#{bits})" do + it 'should always require a password' do + expect { JWA.sign("HS#{bits}", payload) }.to raise_error(JWA::MissingSecretOrKey) + expect { JWA.sign("HS#{bits}", payload, secret) }.not_to raise_error + end end end end From 275638fba5365e248c0077418b6969c773981651 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Mon, 29 Dec 2014 13:11:53 +0100 Subject: [PATCH 17/35] Extend JWA HMAC, break tests Extend JWA HMAC logic, add sign/verify basic logic and tests. Break tests by removing padding from base64 encoded strings. TODO: Implement padding handling for base64 strings in order to be compatible with the JOSE JWT/JWS/JWA specs. --- lib/jwa.rb | 22 ++++++++++++++--- lib/jwa/hmac.rb | 10 +++++++- spec/lib/jwa_spec.rb | 59 +++++++++++++++++++++++++++++++++++--------- spec/lib/jwt_spec.rb | 4 +-- 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/lib/jwa.rb b/lib/jwa.rb index c35f639c..2550aa20 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -3,14 +3,20 @@ module JWA extend self + # The complete list of signing algorithms defined in the IETF JSON Web Algorithms (JWA) version 38 + # https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3.1 ALGORITHMS = %w(HS256 HS384 HS512 RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none) + # raises if the payload is not a string class InvalidPayloadFormat < ArgumentError end + # raises if a algorithm is called that is not defined in the specs + # Info: all algorithms a case-sensitive class InvalidAlgorithm < ArgumentError end + # raises if a secret or key is required but not provided in order to sign the data class MissingSecretOrKey < ArgumentError end @@ -28,13 +34,23 @@ def sign(algorithm, data, secret_or_private_key = '') end end - def verify(algorithm, data, secret_or_private_key = '') - validate_algorithm algorithm + def verify(algorithm, data, signature, secret_or_private_key = '') + algo, bits = validate_algorithm algorithm validate_data data + validate_data signature + + case algo + when 'HS' + JWA::HMAC.new(bits).verify(data, signature, secret_or_private_key) + when 'RS' + when 'ES' + when 'PS' + when 'none' + end end def validate_algorithm(algorithm) - raise JWA::InvalidAlgorithm.new("JWA: Given algorithm [#{algorithm.to_s}] is not part of the JWS supported algorithms.") unless ALGORITHMS.include? algorithm # + raise JWA::InvalidAlgorithm.new("JWA: Given algorithm [#{algorithm.to_s}] is not part of the JWS supported algorithms.") unless ALGORITHMS.include? algorithm match = algorithm.match(/(HS|RS|ES|PS|none)(\d+)?/) diff --git a/lib/jwa/hmac.rb b/lib/jwa/hmac.rb index 8ea27c13..68160aee 100644 --- a/lib/jwa/hmac.rb +++ b/lib/jwa/hmac.rb @@ -1,11 +1,19 @@ module JWA class HMAC def initialize(bits) - + @bits = bits end def sign(data, secret) validate_secret secret + + signature = OpenSSL::HMAC.digest OpenSSL::Digest.new("sha#{@bits}"), secret, data + + Base64.urlsafe_encode64 signature + end + + def verify(data, signature, secret) + signature === sign(data, secret) end def validate_secret(secret) diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index c40d052b..958ac564 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -8,35 +8,70 @@ let(:wrong_secret) { 'TopWrongSecret' } it 'should only accept registered, case-sensitive algorithms' do - %w(HS256 HS384 HS512 RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| + %w(HS256 HS384 HS512).each do |algo| + signature = JWA.sign algo, string, secret expect { JWA.sign(algo, string, secret) }.not_to raise_error - expect { JWA.verify(algo, string, secret) }.not_to raise_error + expect { JWA.verify(algo, string, signature, secret) }.not_to raise_error end expect { JWA.sign('RSA1_5', string) }.to raise_error(JWA::InvalidAlgorithm) - expect { JWA.verify('RS513', string) }.to raise_error(JWA::InvalidAlgorithm) + expect { JWA.verify('RS513', string, secret) }.to raise_error(JWA::InvalidAlgorithm) expect { JWA.sign('hs256', string) }.to raise_error(JWA::InvalidAlgorithm) - expect { JWA.verify('NONE', string) }.to raise_error(JWA::InvalidAlgorithm) + expect { JWA.verify('NONE', string, secret) }.to raise_error(JWA::InvalidAlgorithm) end - it 'should only accepts strings as input data' do + it 'should only accept strings as input data' do algo = 'HS256' + sign = JWA.sign algo, string, secret expect { JWA.sign(algo, hash, secret) }.to raise_error(JWA::InvalidPayloadFormat) expect { JWA.sign(algo, string, secret) }.not_to raise_error - expect { JWA.verify(algo, hash, secret) }.to raise_error(JWA::InvalidPayloadFormat) - expect { JWA.verify(algo, string, secret) }.not_to raise_error + expect { JWA.verify(algo, hash, secret, secret) }.to raise_error(JWA::InvalidPayloadFormat) + expect { JWA.verify(algo, string, sign, secret) }.not_to raise_error end - context 'sign and verify using' do - let(:payload) { 'A very string-ish payload.' } + context 'using' do + let(:payload) do + 'eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4' + end + + let(:secret) do + Base64.urlsafe_decode64 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg=' + end + + let(:signatures) do + { + '256' => 's0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0', + '384' => 'O1jhTTHkuaiubwDZoIBLv6zjEarXHc22NNu05IdYh_yzIKGYXJQcaI2WnF4BCq7j', + '512' => 'rdWYqzXuAJp4OW-exqIwrO8HJJQDYu0_fkTIUBHmyHMFJ0pVe7fjP7QtE7BaX-7FN5YiyiM11MwIEAxzxBj6qw' + } + end [256, 384, 512].each do |bits| - context "HMAC SHA-#{bits} (HS#{bits})" do + context "sign HMAC SHA-#{bits} (HS#{bits})" do + let(:algorithm) { "HS#{bits}" } + it 'should always require a password' do - expect { JWA.sign("HS#{bits}", payload) }.to raise_error(JWA::MissingSecretOrKey) - expect { JWA.sign("HS#{bits}", payload, secret) }.not_to raise_error + expect { JWA.sign(algorithm, payload) }.to raise_error(JWA::MissingSecretOrKey) + expect { JWA.sign(algorithm, payload, secret) }.not_to raise_error + end + + it 'should return the correct computed signature as base64 urlsafe string' do + expect(JWA.sign(algorithm, payload, secret)).to eq(signatures[bits.to_s]) + end + end + + context "verify HMAC SHA-#{bits} (HS#{bits})" do + let(:algorithm) { "HS#{bits}" } + + it 'should always require a password' do + expect { JWA.verify(algorithm, payload, signatures[bits.to_s]) }.to raise_error(JWA::MissingSecretOrKey) + expect { JWA.verify(algorithm, payload, signatures[bits.to_s], secret) }.not_to raise_error + end + + it 'should return true for matching signature and data' do + expect(JWA.verify(algorithm, payload, signatures[bits.to_s], secret)).to eq(true) end end end diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index 5f8ba8c4..ba81b5d4 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -5,10 +5,10 @@ let(:jwt_header) { { 'alg' => 'HS256', 'typ' => 'JWT' } } let(:jwt_header_base64) { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' } let(:jwt_payload) { { 'sub' => 1234567890, 'name' => 'John Doe', 'admin' => true } } - let(:jwt_payload_base64) { 'eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ==' } + let(:jwt_payload_base64) { 'eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ' } let(:secret) { 'secret' } let(:wrong_secret) { 'wrong secret' } - let(:jwt_signature_base64) { 'zzPVwrCDlyRQSEMsDCLrq4cjMl5t88H5T2msS_HgdqI=' } + let(:jwt_signature_base64) { 'zzPVwrCDlyRQSEMsDCLrq4cjMl5t88H5T2msS_HgdqI' } let(:token) { "#{jwt_header_base64}.#{jwt_payload_base64}.#{jwt_signature_base64}" } context 'encode' do From 5f660b884b8f25d6d7bae9f8c56c017fa6d34efa Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Mon, 29 Dec 2014 16:33:57 +0100 Subject: [PATCH 18/35] Add Base64 encode and decode handling --- lib/jwa.rb | 1 + lib/jwa/hmac.rb | 4 +++- lib/jwt.rb | 17 +++++++++-------- lib/jwt/base64.rb | 17 +++++++++++++++++ spec/lib/jwt_spec.rb | 6 +++--- 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 lib/jwt/base64.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 2550aa20..79f1fd0c 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,3 +1,4 @@ +require 'jwt/base64' require 'jwa/hmac' module JWA diff --git a/lib/jwa/hmac.rb b/lib/jwa/hmac.rb index 68160aee..faab68bb 100644 --- a/lib/jwa/hmac.rb +++ b/lib/jwa/hmac.rb @@ -1,3 +1,5 @@ +require 'jwt/base64' + module JWA class HMAC def initialize(bits) @@ -9,7 +11,7 @@ def sign(data, secret) signature = OpenSSL::HMAC.digest OpenSSL::Digest.new("sha#{@bits}"), secret, data - Base64.urlsafe_encode64 signature + JWT::Base64.encode signature end def verify(data, signature, secret) diff --git a/lib/jwt.rb b/lib/jwt.rb index 193bdf63..24e69572 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,6 +1,7 @@ require 'openssl' -require 'base64' require 'json' +require 'base64' +require 'jwt/base64' require 'jwa' module JWT @@ -19,11 +20,11 @@ def encode(payload, secret_or_key, algorithm = 'HS256', head = {}) token = [] - token << Base64.urlsafe_encode64(header.merge(head).to_json) - token << Base64.urlsafe_encode64(payload.to_json) + token << Base64.encode(header.merge(head).to_json) + token << Base64.encode(payload.to_json) signature = if algorithm != 'none' - Base64.urlsafe_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_or_key, token.join('.'))) + Base64.encode(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_or_key, token.join('.'))) else '' end @@ -37,12 +38,12 @@ def decode(token, secret_or_key = nil, verify = true) begin header, payload, signature = token.split('.') - header = JSON.parse(Base64.urlsafe_decode64(header)) - payload = JSON.parse(Base64.urlsafe_decode64(payload)) + header = JSON.parse(Base64.decode(header)) + payload = JSON.parse(Base64.decode(payload)) signature = if header['alg'] == 'none' '' else - Base64.urlsafe_decode64(signature) + Base64.decode(signature) end rescue Exception => e raise JWT::DecodeError.new e.message @@ -51,7 +52,7 @@ def decode(token, secret_or_key = nil, verify = true) valid = false if verify && header['alg'] != 'none' - valid = signature === Base64.urlsafe_decode64(encode(payload, secret_or_key, header['alg'], header).split('.').last()) + valid = signature === Base64.decode(encode(payload, secret_or_key, header['alg'], header).split('.').last()) raise JWT::DecodeError.new('Token verification failed. Data corrupted or pass phrase incorrect.') unless valid end diff --git a/lib/jwt/base64.rb b/lib/jwt/base64.rb new file mode 100644 index 00000000..92764a79 --- /dev/null +++ b/lib/jwt/base64.rb @@ -0,0 +1,17 @@ +require 'base64' + +module JWT + module Base64 + extend self + + def encode(data) + ::Base64.encode64(data).tr('+/', '-_').gsub /[\n=]/, '' + end + + def decode(data) + data += '=' * (4 - data.length.modulo(4)) + + ::Base64.decode64(data.tr('-_', '+/')) + end + end +end diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index ba81b5d4..6a782f24 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -8,7 +8,7 @@ let(:jwt_payload_base64) { 'eyJzdWIiOjEyMzQ1Njc4OTAsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlfQ' } let(:secret) { 'secret' } let(:wrong_secret) { 'wrong secret' } - let(:jwt_signature_base64) { 'zzPVwrCDlyRQSEMsDCLrq4cjMl5t88H5T2msS_HgdqI' } + let(:jwt_signature_base64) { 'eoaDVGTClRdfxUZXiPs3f8FmJDkDE_VCQFXqKxpLsts' } let(:token) { "#{jwt_header_base64}.#{jwt_payload_base64}.#{jwt_signature_base64}" } context 'encode' do @@ -20,7 +20,7 @@ it 'should create plain tokens' do header = jwt_header header['alg'] = 'none' - header = Base64.urlsafe_encode64(header.to_json) + header = JWT::Base64.encode(header.to_json) token = "#{header}.#{jwt_payload_base64}." jwt = JWT.encode(jwt_payload, '', 'none') @@ -37,7 +37,7 @@ expect(header).to eq(jwt_header) expect(payload).to eq(jwt_payload) - expect(signature).to eq(Base64.urlsafe_decode64(jwt_signature_base64)) + expect(signature).to eq(JWT::Base64.decode(jwt_signature_base64)) expect(valid).to eq(true) end From 70367132ef77b6aac14986514059f5584888031d Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Mon, 29 Dec 2014 16:55:36 +0100 Subject: [PATCH 19/35] Clean up code, add test for not implemented algorithms --- lib/jwa.rb | 17 ++++++++--------- lib/jwa/hmac.rb | 2 -- lib/jwt.rb | 1 - spec/lib/jwa_spec.rb | 6 ++++++ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/jwa.rb b/lib/jwa.rb index 79f1fd0c..8288726e 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,4 +1,3 @@ -require 'jwt/base64' require 'jwa/hmac' module JWA @@ -21,6 +20,10 @@ class InvalidAlgorithm < ArgumentError class MissingSecretOrKey < ArgumentError end + # raises if a part of code is not implemented + class NotImplemented < ArgumentError + end + def sign(algorithm, data, secret_or_private_key = '') algo, bits = validate_algorithm algorithm validate_data data @@ -28,10 +31,8 @@ def sign(algorithm, data, secret_or_private_key = '') case algo when 'HS' JWA::HMAC.new(bits).sign(data, secret_or_private_key) - when 'RS' - when 'ES' - when 'PS' - when 'none' + else + raise JWA::NotImplemented.new("JWA: #{algorithm} is not implemented yet.") end end @@ -43,10 +44,8 @@ def verify(algorithm, data, signature, secret_or_private_key = '') case algo when 'HS' JWA::HMAC.new(bits).verify(data, signature, secret_or_private_key) - when 'RS' - when 'ES' - when 'PS' - when 'none' + else + raise JWA::NotImplemented.new("JWA: #{algorithm} is not implemented yet.") end end diff --git a/lib/jwa/hmac.rb b/lib/jwa/hmac.rb index faab68bb..953e5950 100644 --- a/lib/jwa/hmac.rb +++ b/lib/jwa/hmac.rb @@ -1,5 +1,3 @@ -require 'jwt/base64' - module JWA class HMAC def initialize(bits) diff --git a/lib/jwt.rb b/lib/jwt.rb index 24e69572..f0dee8cb 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,6 +1,5 @@ require 'openssl' require 'json' -require 'base64' require 'jwt/base64' require 'jwa' diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index 958ac564..138468c1 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -20,6 +20,12 @@ expect { JWA.verify('NONE', string, secret) }.to raise_error(JWA::InvalidAlgorithm) end + it 'should raise if algorithm is not implemented' do + %w(RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| + expect { JWA.sign(algo, string, secret) }.to raise_error(JWA::NotImplemented) + end + end + it 'should only accept strings as input data' do algo = 'HS256' sign = JWA.sign algo, string, secret From 64aa4f47663f5b300fe8f480bf5425ec3d1bc9a4 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Tue, 30 Dec 2014 09:23:29 +0100 Subject: [PATCH 20/35] Fix some tests --- spec/lib/jwa_spec.rb | 2 +- spec/lib/jwt_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index 138468c1..12755348 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -43,7 +43,7 @@ end let(:secret) do - Base64.urlsafe_decode64 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg=' + JWT::Base64.decode 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' end let(:signatures) do diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index 6a782f24..e0229955 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -44,7 +44,7 @@ it 'should handle plain tokens' do h = jwt_header h['alg'] = 'none' - hb64 = Base64.urlsafe_encode64(h.to_json) + hb64 = JWT::Base64.encode(h.to_json) token = "#{hb64}.#{jwt_payload_base64}." header, payload, signature, valid = JWT.decode(token) @@ -70,7 +70,7 @@ h = jwt_header h['alg'] = 'none' h['test'] = 'test' - hb64 = Base64.urlsafe_encode64(h.to_json) + hb64 = JWT::Base64.encode(h.to_json) token = "#{hb64}.#{jwt_payload_base64}." header, payload, signature, valid = JWT.decode(token) From 621aaa670576918b4ccd20d864cd1a87fbe036d0 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 31 Dec 2014 22:03:48 +0100 Subject: [PATCH 21/35] Remove jwa folder from prepare-test script --- bin/prepare-test.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/bin/prepare-test.sh b/bin/prepare-test.sh index ab0aa281..884e63d0 100755 --- a/bin/prepare-test.sh +++ b/bin/prepare-test.sh @@ -2,19 +2,19 @@ mkdir -p tmp/certs/jwa -openssl genrsa 2048 > tmp/certs/jwa/rsa-private.pem -openssl genrsa 2048 > tmp/certs/jwa/rsa-wrong-private.pem -openssl rsa -in tmp/certs/jwa/rsa-private.pem -pubout > tmp/certs/jwa/rsa-public.pem -openssl rsa -in tmp/certs/jwa/rsa-wrong-private.pem -pubout > tmp/certs/jwa/rsa-wrong-public.pem -openssl ecparam -out tmp/certs/jwa/ec256-private.pem -name secp256k1 -genkey -openssl ecparam -out tmp/certs/jwa/ec256-wrong-private.pem -name secp256k1 -genkey -openssl ecparam -out tmp/certs/jwa/ec384-private.pem -name secp384r1 -genkey -openssl ecparam -out tmp/certs/jwa/ec384-wrong-private.pem -name secp384r1 -genkey -openssl ecparam -out tmp/certs/jwa/ec512-private.pem -name secp521r1 -genkey -openssl ecparam -out tmp/certs/jwa/ec512-wrong-private.pem -name secp521r1 -genkey -openssl ec -in tmp/certs/jwa/ec256-private.pem -pubout > tmp/certs/jwa/ec256-public.pem -openssl ec -in tmp/certs/jwa/ec256-wrong-private.pem -pubout > tmp/certs/jwa/ec256-wrong-public.pem -openssl ec -in tmp/certs/jwa/ec384-private.pem -pubout > tmp/certs/jwa/ec384-public.pem -openssl ec -in tmp/certs/jwa/ec384-wrong-private.pem -pubout > tmp/certs/jwa/ec384-wrong-public.pem -openssl ec -in tmp/certs/jwa/ec512-private.pem -pubout > tmp/certs/jwa/ec512-public.pem -openssl ec -in tmp/certs/jwa/ec512-wrong-private.pem -pubout > tmp/certs/jwa/ec512-wrong-public.pem +openssl genrsa 2048 > tmp/certs/rsa-private.pem +openssl genrsa 2048 > tmp/certs/rsa-wrong-private.pem +openssl rsa -in tmp/certs/rsa-private.pem -pubout > tmp/certs/rsa-public.pem +openssl rsa -in tmp/certs/rsa-wrong-private.pem -pubout > tmp/certs/rsa-wrong-public.pem +openssl ecparam -out tmp/certs/ec256-private.pem -name secp256k1 -genkey +openssl ecparam -out tmp/certs/ec256-wrong-private.pem -name secp256k1 -genkey +openssl ecparam -out tmp/certs/ec384-private.pem -name secp384r1 -genkey +openssl ecparam -out tmp/certs/ec384-wrong-private.pem -name secp384r1 -genkey +openssl ecparam -out tmp/certs/ec512-private.pem -name secp521r1 -genkey +openssl ecparam -out tmp/certs/ec512-wrong-private.pem -name secp521r1 -genkey +openssl ec -in tmp/certs/ec256-private.pem -pubout > tmp/certs/ec256-public.pem +openssl ec -in tmp/certs/ec256-wrong-private.pem -pubout > tmp/certs/ec256-wrong-public.pem +openssl ec -in tmp/certs/ec384-private.pem -pubout > tmp/certs/ec384-public.pem +openssl ec -in tmp/certs/ec384-wrong-private.pem -pubout > tmp/certs/ec384-wrong-public.pem +openssl ec -in tmp/certs/ec512-private.pem -pubout > tmp/certs/ec512-public.pem +openssl ec -in tmp/certs/ec512-wrong-private.pem -pubout > tmp/certs/ec512-wrong-public.pem From e432867bf89e7bc1a5a0041a6d92f82536ffdb4f Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 31 Dec 2014 22:04:19 +0100 Subject: [PATCH 22/35] Remove skipped jwa folder on mkdir --- bin/prepare-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/prepare-test.sh b/bin/prepare-test.sh index 884e63d0..3eba9406 100755 --- a/bin/prepare-test.sh +++ b/bin/prepare-test.sh @@ -1,6 +1,6 @@ #!/bin/sh -mkdir -p tmp/certs/jwa +mkdir -p tmp/certs openssl genrsa 2048 > tmp/certs/rsa-private.pem openssl genrsa 2048 > tmp/certs/rsa-wrong-private.pem From 9338632d1f6de21a7fd4ff839ea97d55d8d55244 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 31 Dec 2014 22:32:58 +0100 Subject: [PATCH 23/35] Add new keys for tests --- bin/prepare-test.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bin/prepare-test.sh b/bin/prepare-test.sh index 3eba9406..07a96898 100755 --- a/bin/prepare-test.sh +++ b/bin/prepare-test.sh @@ -1,11 +1,20 @@ #!/bin/sh +rm -rf tmp/certs/*.pem + mkdir -p tmp/certs -openssl genrsa 2048 > tmp/certs/rsa-private.pem -openssl genrsa 2048 > tmp/certs/rsa-wrong-private.pem -openssl rsa -in tmp/certs/rsa-private.pem -pubout > tmp/certs/rsa-public.pem -openssl rsa -in tmp/certs/rsa-wrong-private.pem -pubout > tmp/certs/rsa-wrong-public.pem +# RSA KEYS +openssl genrsa 1024 > tmp/certs/rsa-1024-private.pem +openssl rsa -in tmp/certs/rsa-1024-private.pem -pubout > tmp/certs/rsa-1024-public.pem +openssl genrsa 2048 > tmp/certs/rsa-2048-private.pem +openssl genrsa 2048 > tmp/certs/rsa-2048-wrong-private.pem +openssl rsa -in tmp/certs/rsa-2048-private.pem -pubout > tmp/certs/rsa-2048-public.pem +openssl rsa -in tmp/certs/rsa-2048-wrong-private.pem -pubout > tmp/certs/rsa-2048-wrong-public.pem +openssl genrsa 4096 > tmp/certs/rsa-4096-private.pem +openssl rsa -in tmp/certs/rsa-4096-private.pem -pubout > tmp/certs/rsa-4096-public.pem + +# ECDSA KEYS openssl ecparam -out tmp/certs/ec256-private.pem -name secp256k1 -genkey openssl ecparam -out tmp/certs/ec256-wrong-private.pem -name secp256k1 -genkey openssl ecparam -out tmp/certs/ec384-private.pem -name secp384r1 -genkey From d799c92c86a7e5c0bea0d5fb5492d3e57b80745f Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Wed, 31 Dec 2014 23:45:45 +0100 Subject: [PATCH 24/35] Add basic JWA RSASSA SHA implementation --- lib/jwa.rb | 9 +++-- lib/jwa/rsassa.rb | 38 ++++++++++++++++++++ spec/lib/jwa_spec.rb | 83 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 lib/jwa/rsassa.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 8288726e..01aec5db 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,4 +1,5 @@ require 'jwa/hmac' +require 'jwa/rsassa' module JWA extend self @@ -31,19 +32,23 @@ def sign(algorithm, data, secret_or_private_key = '') case algo when 'HS' JWA::HMAC.new(bits).sign(data, secret_or_private_key) + when 'RS' + JWA::RSASSA.new(bits).sign(data, secret_or_private_key) else raise JWA::NotImplemented.new("JWA: #{algorithm} is not implemented yet.") end end - def verify(algorithm, data, signature, secret_or_private_key = '') + def verify(algorithm, data, signature, secret_or_public_key = '') algo, bits = validate_algorithm algorithm validate_data data validate_data signature case algo when 'HS' - JWA::HMAC.new(bits).verify(data, signature, secret_or_private_key) + JWA::HMAC.new(bits).verify(data, signature, secret_or_public_key) + when 'RS' + JWA::RSASSA.new(bits).verify(data, signature, secret_or_public_key) else raise JWA::NotImplemented.new("JWA: #{algorithm} is not implemented yet.") end diff --git a/lib/jwa/rsassa.rb b/lib/jwa/rsassa.rb new file mode 100644 index 00000000..5612f68c --- /dev/null +++ b/lib/jwa/rsassa.rb @@ -0,0 +1,38 @@ +module JWA + class RSASSA + class KeyStrength < ArgumentError + end + + def initialize(bits) + @bits = bits + end + + def sign(data, private_key) + private_key = validate_key private_key + + signature = private_key.sign(OpenSSL::Digest.new("sha#{@bits}"), data) + + JWT::Base64.encode signature + end + + def verify(data, signature, public_key) + public_key = validate_key public_key + + public_key.verify(OpenSSL::Digest.new("sha#{@bits}"), JWT::Base64.decode(signature), data) + end + + def validate_key(key) + raise JWA::MissingSecretOrKey.new('JWA: RSA SHA signing and validating always requires a rsa key to be set.') if key.length == 0 + + key = OpenSSL::PKey::RSA.new key + + strength = key.to_text.match(/(Public|Private)-Key: \((\d{1,4}) bit\)/)[2].to_i + + raise JWA::RSASSA::KeyStrength.new('JWA: RSA SHA: A key strength of minimum 2048 bit is required.') if strength < 2048 + + key + end + + private :validate_key + end +end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index 12755348..a433949a 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -7,13 +7,42 @@ let(:secret) { 'TopSecret' } let(:wrong_secret) { 'TopWrongSecret' } + let(:rsa_key) do + { + '1024' => { + public: File.read(File.join(CERT_PATH, 'rsa-1024-public.pem')), + private: File.read(File.join(CERT_PATH, 'rsa-1024-private.pem')) + }, + '2048' => { + public: File.read(File.join(CERT_PATH, 'rsa-2048-public.pem')), + private: File.read(File.join(CERT_PATH, 'rsa-2048-private.pem')) + }, + '2048_wrong' => { + public: File.read(File.join(CERT_PATH, 'rsa-2048-wrong-public.pem')), + private: File.read(File.join(CERT_PATH, 'rsa-2048-wrong-private.pem')) + }, + '4096' => { + public: File.read(File.join(CERT_PATH, 'rsa-4096-public.pem')), + private: File.read(File.join(CERT_PATH, 'rsa-4096-private.pem')) + } + } + end + it 'should only accept registered, case-sensitive algorithms' do %w(HS256 HS384 HS512).each do |algo| signature = JWA.sign algo, string, secret + expect { JWA.sign(algo, string, secret) }.not_to raise_error expect { JWA.verify(algo, string, signature, secret) }.not_to raise_error end + %w(RS256 RS384 RS512).each do |algo| + signature = JWA.sign algo, string, rsa_key['2048'][:private] + + expect { JWA.sign(algo, string, rsa_key['2048'][:private]) }.not_to raise_error + expect { JWA.verify(algo, string, signature, rsa_key['2048'][:public]) }.not_to raise_error + end + expect { JWA.sign('RSA1_5', string) }.to raise_error(JWA::InvalidAlgorithm) expect { JWA.verify('RS513', string, secret) }.to raise_error(JWA::InvalidAlgorithm) expect { JWA.sign('hs256', string) }.to raise_error(JWA::InvalidAlgorithm) @@ -21,7 +50,7 @@ end it 'should raise if algorithm is not implemented' do - %w(RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| + %w(ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| expect { JWA.sign(algo, string, secret) }.to raise_error(JWA::NotImplemented) end end @@ -46,7 +75,7 @@ JWT::Base64.decode 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg' end - let(:signatures) do + let(:hmac_signatures) do { '256' => 's0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0', '384' => 'O1jhTTHkuaiubwDZoIBLv6zjEarXHc22NNu05IdYh_yzIKGYXJQcaI2WnF4BCq7j', @@ -54,6 +83,14 @@ } end + let(:rsa_2048_signatures) do + { + '256' => JWA.sign('RS256', payload, rsa_key['2048'][:private]), + '384' => JWA.sign('RS384', payload, rsa_key['2048'][:private]), + '512' => JWA.sign('RS512', payload, rsa_key['2048'][:private]) + } + end + [256, 384, 512].each do |bits| context "sign HMAC SHA-#{bits} (HS#{bits})" do let(:algorithm) { "HS#{bits}" } @@ -64,7 +101,7 @@ end it 'should return the correct computed signature as base64 urlsafe string' do - expect(JWA.sign(algorithm, payload, secret)).to eq(signatures[bits.to_s]) + expect(JWA.sign(algorithm, payload, secret)).to eq(hmac_signatures[bits.to_s]) end end @@ -72,14 +109,48 @@ let(:algorithm) { "HS#{bits}" } it 'should always require a password' do - expect { JWA.verify(algorithm, payload, signatures[bits.to_s]) }.to raise_error(JWA::MissingSecretOrKey) - expect { JWA.verify(algorithm, payload, signatures[bits.to_s], secret) }.not_to raise_error + expect { JWA.verify(algorithm, payload, hmac_signatures[bits.to_s]) }.to raise_error(JWA::MissingSecretOrKey) + expect { JWA.verify(algorithm, payload, hmac_signatures[bits.to_s], secret) }.not_to raise_error end it 'should return true for matching signature and data' do - expect(JWA.verify(algorithm, payload, signatures[bits.to_s], secret)).to eq(true) + expect(JWA.verify(algorithm, payload, hmac_signatures[bits.to_s], secret)).to eq(true) end end end + + [256, 384, 512].each do |bits| + context "sign RSA SHA-#{bits} (RS#{bits})" do + let(:algorithm) { "RS#{bits}" } + + it 'should always require a rsa key' do + expect { JWA.sign(algorithm, payload) }.to raise_error(JWA::MissingSecretOrKey) + expect { JWA.sign(algorithm, payload, rsa_key['2048'][:private]) }.not_to raise_error + end + + it 'should return the correct computed signature as base64 urlsafe string' do + expect(JWA.sign(algorithm, payload, rsa_key['2048'][:private])).to eq(rsa_2048_signatures[bits.to_s]) + end + end + + context "verify RSA SHA-#{bits} (RS#{bits})" do + let(:algorithm) { "RS#{bits}" } + + it 'should always require a key' do + expect { JWA.verify(algorithm, payload, rsa_2048_signatures[bits.to_s]) }.to raise_error(JWA::MissingSecretOrKey) + expect { JWA.verify(algorithm, payload, rsa_2048_signatures[bits.to_s], rsa_key['2048'][:public]) }.not_to raise_error + end + + it 'should return true for matching signature and data' do + expect(JWA.verify(algorithm, payload, rsa_2048_signatures[bits.to_s], rsa_key['2048'][:public])).to eq(true) + end + end + + it 'should raise if key is weaker than 2048 bits' do + expect { JWA.sign('RS256', payload, rsa_key['1024'][:private]) }.to raise_error(JWA::RSASSA::KeyStrength) + expect { JWA.sign('RS256', payload, rsa_key['2048'][:private]) }.not_to raise_error + expect { JWA.sign('RS256', payload, rsa_key['4096'][:private]) }.not_to raise_error + end + end end end From bd372dfca0d3e17e123459eae26e16595de5b78a Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 04:15:48 +0100 Subject: [PATCH 25/35] Add test cases for wrong keys in RSASSA verification --- spec/lib/jwa_spec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index a433949a..abaf6a78 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -144,6 +144,11 @@ it 'should return true for matching signature and data' do expect(JWA.verify(algorithm, payload, rsa_2048_signatures[bits.to_s], rsa_key['2048'][:public])).to eq(true) end + + it 'should return for mismatching signature and data' do + expect(JWA.verify(algorithm, payload, rsa_2048_signatures[bits.to_s], rsa_key['2048_wrong'][:public])).to eq(false) + expect(JWA.verify(algorithm, payload, rsa_2048_signatures[bits.to_s], rsa_key['4096'][:public])).to eq(false) + end end it 'should raise if key is weaker than 2048 bits' do From 0b0ff1685556c7e68394578020f8df3ad7b37eb5 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 05:31:34 +0100 Subject: [PATCH 26/35] Integrate JWA into JWT Add JWA::NONE for plain JWT Update JWT to integrate JWA --- lib/jwa.rb | 6 +++++- lib/jwa/none.rb | 11 +++++++++++ lib/jwt.rb | 38 +++++++++++++++++--------------------- spec/lib/jwa_spec.rb | 2 +- spec/lib/jwt_spec.rb | 4 ++-- 5 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 lib/jwa/none.rb diff --git a/lib/jwa.rb b/lib/jwa.rb index 01aec5db..856bf91e 100644 --- a/lib/jwa.rb +++ b/lib/jwa.rb @@ -1,4 +1,5 @@ require 'jwa/hmac' +require 'jwa/none' require 'jwa/rsassa' module JWA @@ -34,6 +35,8 @@ def sign(algorithm, data, secret_or_private_key = '') JWA::HMAC.new(bits).sign(data, secret_or_private_key) when 'RS' JWA::RSASSA.new(bits).sign(data, secret_or_private_key) + when 'none' + JWA::NONE.new.sign() else raise JWA::NotImplemented.new("JWA: #{algorithm} is not implemented yet.") end @@ -42,13 +45,14 @@ def sign(algorithm, data, secret_or_private_key = '') def verify(algorithm, data, signature, secret_or_public_key = '') algo, bits = validate_algorithm algorithm validate_data data - validate_data signature case algo when 'HS' JWA::HMAC.new(bits).verify(data, signature, secret_or_public_key) when 'RS' JWA::RSASSA.new(bits).verify(data, signature, secret_or_public_key) + when 'none' + JWA::NONE.new.verify() else raise JWA::NotImplemented.new("JWA: #{algorithm} is not implemented yet.") end diff --git a/lib/jwa/none.rb b/lib/jwa/none.rb new file mode 100644 index 00000000..0b53db4f --- /dev/null +++ b/lib/jwa/none.rb @@ -0,0 +1,11 @@ +module JWA + class NONE + def sign + '' + end + + def verify + true + end + end +end diff --git a/lib/jwt.rb b/lib/jwt.rb index f0dee8cb..8a37bb2d 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -17,44 +17,40 @@ def encode(payload, secret_or_key, algorithm = 'HS256', head = {}) 'typ' => 'JWT' } - token = [] + header = Base64.encode header.merge(head).to_json + payload = Base64.encode payload.to_json - token << Base64.encode(header.merge(head).to_json) - token << Base64.encode(payload.to_json) + token = [header, payload] - signature = if algorithm != 'none' - Base64.encode(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_or_key, token.join('.'))) - else - '' - end - - token << signature + token << JWA.sign(algorithm, (header + '.' + payload).to_s, secret_or_key) token.join '.' end def decode(token, secret_or_key = nil, verify = true) begin - header, payload, signature = token.split('.') - - header = JSON.parse(Base64.decode(header)) - payload = JSON.parse(Base64.decode(payload)) - signature = if header['alg'] == 'none' - '' - else - Base64.decode(signature) - end + orig_header, orig_payload, orig_signature = token.split('.') + + header = JSON.parse(Base64.decode(orig_header)) + payload = JSON.parse(Base64.decode(orig_payload)) + signature = header['alg'] == 'none' ? '' : Base64.decode(orig_signature) rescue Exception => e raise JWT::DecodeError.new e.message end valid = false - if verify && header['alg'] != 'none' - valid = signature === Base64.decode(encode(payload, secret_or_key, header['alg'], header).split('.').last()) + if verify + valid = verify(header['alg'], (orig_header + '.' + orig_payload), orig_signature, secret_or_key) raise JWT::DecodeError.new('Token verification failed. Data corrupted or pass phrase incorrect.') unless valid end [header, payload, signature, valid] end + + def verify(algorithm, payload, signature, secret_or_key) + JWA.verify(algorithm, payload, signature, secret_or_key) + end + + private :verify end diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index abaf6a78..dd31e5ea 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -50,7 +50,7 @@ end it 'should raise if algorithm is not implemented' do - %w(ES256 ES384 ES512 PS256 PS384 PS512 none).each do |algo| + %w(ES256 ES384 ES512 PS256 PS384 PS512).each do |algo| expect { JWA.sign(algo, string, secret) }.to raise_error(JWA::NotImplemented) end end diff --git a/spec/lib/jwt_spec.rb b/spec/lib/jwt_spec.rb index e0229955..99ccda61 100644 --- a/spec/lib/jwt_spec.rb +++ b/spec/lib/jwt_spec.rb @@ -52,7 +52,7 @@ expect(header).to eq(h) expect(payload).to eq(jwt_payload) expect(signature).to eq('') - expect(valid).to eq(false) + expect(valid).to eq(true) end context 'raises DecodeError' do @@ -78,6 +78,6 @@ expect(header).to eq(h) expect(payload).to eq(jwt_payload) expect(signature).to eq('') - expect(valid).to eq(false) + expect(valid).to eq(true) end end From 1be475dac3fcbec8e6322bfe52521e982c3ab58d Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 05:51:33 +0100 Subject: [PATCH 27/35] Add SimpleCov config, improve coverage to 100% --- lib/jwt.rb | 14 +++++--------- spec/lib/jwa_spec.rb | 3 +++ spec/spec_helper.rb | 10 ++++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/jwt.rb b/lib/jwt.rb index 8a37bb2d..deab4f14 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -28,16 +28,12 @@ def encode(payload, secret_or_key, algorithm = 'HS256', head = {}) end def decode(token, secret_or_key = nil, verify = true) - begin - orig_header, orig_payload, orig_signature = token.split('.') - - header = JSON.parse(Base64.decode(orig_header)) - payload = JSON.parse(Base64.decode(orig_payload)) - signature = header['alg'] == 'none' ? '' : Base64.decode(orig_signature) - rescue Exception => e - raise JWT::DecodeError.new e.message - end + orig_header, orig_payload, orig_signature = token.split('.') + header = JSON.parse(Base64.decode(orig_header)) + payload = JSON.parse(Base64.decode(orig_payload)) + signature = header['alg'] == 'none' ? '' : Base64.decode(orig_signature) + valid = false if verify diff --git a/spec/lib/jwa_spec.rb b/spec/lib/jwa_spec.rb index dd31e5ea..5e86e1df 100644 --- a/spec/lib/jwa_spec.rb +++ b/spec/lib/jwa_spec.rb @@ -47,11 +47,14 @@ expect { JWA.verify('RS513', string, secret) }.to raise_error(JWA::InvalidAlgorithm) expect { JWA.sign('hs256', string) }.to raise_error(JWA::InvalidAlgorithm) expect { JWA.verify('NONE', string, secret) }.to raise_error(JWA::InvalidAlgorithm) + + expect(JWA::ALGORITHMS).to eq(%w(HS256 HS384 HS512 RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 none)) end it 'should raise if algorithm is not implemented' do %w(ES256 ES384 ES512 PS256 PS384 PS512).each do |algo| expect { JWA.sign(algo, string, secret) }.to raise_error(JWA::NotImplemented) + expect { JWA.verify(algo, string, secret) }.to raise_error(JWA::NotImplemented) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fdba7c30..bcd1db55 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,16 @@ require 'codeclimate-test-reporter' CodeClimate::TestReporter.start +SimpleCov.configure do + root File.join(File.dirname(__FILE__), '..') + project_name 'Ruby JWT - Ruby JSON Web Token implementation' + formatter SimpleCov::Formatter::HTMLFormatter + + add_filter 'spec' +end + +SimpleCov.start if ENV['COVERAGE'] + CERT_PATH = File.join(File.dirname(__FILE__), '..', 'tmp', 'certs') RSpec.configure do |config| From ada171f1c58ecd66b69644f3668f3f747500f14c Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 05:59:13 +0100 Subject: [PATCH 28/35] Bunp version to 2.0.0.pre --- Gemfile | 2 +- README.md | 2 ++ Rakefile | 18 +++++++++--------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index cb23fa32..2f1a00cb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source "https://rubygems.org" +source 'https://rubygems.org' gem 'json' diff --git a/README.md b/README.md index b5a9f28a..30efabd7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# WARNING: This is the 2.x branch of ruby-jwt, currently under heavy development + # JWT A Ruby implementation of [JSON Web Token draft 06](http://self-issued.info/docs/draft-jones-json-web-token-06.html). diff --git a/Rakefile b/Rakefile index c38f1963..9b2951e0 100644 --- a/Rakefile +++ b/Rakefile @@ -2,16 +2,16 @@ require 'rubygems' require 'rake' require 'echoe' -Echoe.new('jwt', '1.2.0') do |p| - p.description = "JSON Web Token implementation in Ruby" - p.url = "http://github.com/progrium/ruby-jwt" - p.author = "Jeff Lindsay" - p.email = "progrium@gmail.com" - p.ignore_pattern = ["tmp/*"] - p.development_dependencies = ["echoe >=4.6.3"] - p.licenses = "MIT" +Echoe.new('jwt', '2.0.0.pre') do |p| + p.description = 'JSON Web Token implementation in Ruby' + p.url = 'http://github.com/progrium/ruby-jwt' + p.author = 'Jeff Lindsay' + p.email = 'progrium@gmail.com' + p.ignore_pattern = ['tmp/*'] + p.development_dependencies = ['echoe >=4.6.3'] + p.licenses = 'MIT' end task :test do - sh "rspec spec/jwt_spec.rb" + sh 'bundle exec rspec' end From 10562e56555285e461a9b25564b4d76712cf76eb Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 06:19:10 +0100 Subject: [PATCH 29/35] Add badges to README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 30efabd7..d6a23314 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # WARNING: This is the 2.x branch of ruby-jwt, currently under heavy development # JWT + +[![Build Status](https://travis-ci.org/excpt/ruby-jwt.svg?branch=refactoring)](https://travis-ci.org/excpt/ruby-jwt) +[![Codeship Status for excpt/moments](https://codeship.com/projects/f94b6890-6dc3-0132-8f82-52110d7a425c/status?branch=master)](https://codeship.com/projects/54274) +[![Code Climate](https://codeclimate.com/github/excpt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/excpt/ruby-jwt) +[![Test Coverage](https://codeclimate.com/github/excpt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/excpt/ruby-jwt) + A Ruby implementation of [JSON Web Token draft 06](http://self-issued.info/docs/draft-jones-json-web-token-06.html). ## Installing From 33b65b9e0d658da563047c4946fa396c4f801a5f Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 06:19:46 +0100 Subject: [PATCH 30/35] Add ruby 2.2.0 for travis-ci testing --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 27d82551..cd660383 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ rvm: - 1.9.3 - 2.0.0 - 2.1.5 + - 2.2.0 script: "bundle exec rspec" before_install: - gem update --system From 5a0eb5bfc834bd7cfd3914ff7765d6fd2dde03dd Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Thu, 1 Jan 2015 06:33:35 +0100 Subject: [PATCH 31/35] Update Gemfile, remove codeclimate from travis ci --- .travis.yml | 3 --- Gemfile | 2 +- Manifest | 6 ------ 3 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 Manifest diff --git a/.travis.yml b/.travis.yml index cd660383..35462034 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,3 @@ before_install: - gem --version before_script: - bin/prepare-test.sh -addons: - code_climate: - repo_token: f00428819bf1930e10b8ea2eecf6ecf57e295fcca4f4eb1d77bfd278f573e239 diff --git a/Gemfile b/Gemfile index 2f1a00cb..df726237 100644 --- a/Gemfile +++ b/Gemfile @@ -9,5 +9,5 @@ end group :test, :development do gem 'rake' gem 'rspec' - gem 'codeclimate-test-reporter' + gem 'codeclimate-test-reporter', require: false end diff --git a/Manifest b/Manifest deleted file mode 100644 index f81270d4..00000000 --- a/Manifest +++ /dev/null @@ -1,6 +0,0 @@ -Rakefile -lib/jwt.rb -lib/jwt/json.rb -spec/helper.rb -spec/jwt_spec.rb -Manifest From e0b2e6c4932c9aac965c70b09a64a1882f1ac1d1 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Fri, 2 Jan 2015 02:58:45 +0100 Subject: [PATCH 32/35] Update README.md --- README.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d6a23314..a9fd9cf1 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,31 @@ [![Code Climate](https://codeclimate.com/github/excpt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/excpt/ruby-jwt) [![Test Coverage](https://codeclimate.com/github/excpt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/excpt/ruby-jwt) -A Ruby implementation of [JSON Web Token draft 06](http://self-issued.info/docs/draft-jones-json-web-token-06.html). +A Ruby implementation of [JSON Web Token (JWT) - draft-ietf-oauth-json-web-token-32](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32). ## Installing - sudo gem install jwt +```bash +sudo gem install jwt +``` ## Usage +```ruby JWT.encode({"some" => "payload"}, "secret") +``` Note the resulting JWT will not be encrypted, but verifiable with a secret key. +```ruby JWT.decode("someJWTstring", "secret") +``` If the secret is wrong, it will raise a `JWT::DecodeError` telling you as such. You can still get at the payload by setting the verify argument to false. +```ruby JWT.decode("someJWTstring", nil, false) +``` ## Algorithms @@ -43,14 +51,18 @@ The JWT spec supports several algorithms for cryptographic signing. This library Change the algorithm with by setting it in encode: +```ruby JWT.encode({"some" => "payload"}, "secret", "HS512") +``` **Plaintext** We also support unsigned plaintext JWTs as introduced by draft 03 by explicitly specifying `nil` as the key and algorithm: +```ruby jwt = JWT.encode({"some" => "payload"}, nil, nil) JWT.decode(jwt, nil, nil) +``` ## Support for reserved claim names JSON Web Token defines some reserved claim names and defines how they should be @@ -72,18 +84,21 @@ From [draft 01 of the JWT spec](http://self-issued.info/docs/draft-jones-json-we You pass the expiration time as a UTC UNIX timestamp (an int). For example: +```ruby JWT.encode({"exp": 1371720939}, "secret") - JWT.encode({"exp": Time.now.to_i()}, "secret") +``` Expiration time is automatically verified in `JWT.decode()` and raises `JWT::ExpiredSignature` if the expiration time is in the past: +```ruby begin JWT.decode("JWT_STRING", "secret") rescue JWT::ExpiredSignature # Signature has expired end +``` Expiration time will be compared to the current UTC time (as given by `Time.now.to_i`), so be sure to use a UTC timestamp or datetime in encoding. @@ -96,21 +111,27 @@ For example, if you have a JWT payload with a expiration time set to 30 seconds after creation but you know that sometimes you will process it after 30 seconds, you can set a leeway of 10 seconds in order to have some margin: +```ruby jwt_payload = JWT.encode({'exp': Time.now.to_i + 30}, 'secret') sleep(32) # jwt_payload is now expired # But with some leeway, it will still validate JWT.decode(jwt_payload, 'secret', true, leeway=10) +``` ## Development and Tests We depend on [Echoe](http://rubygems.org/gems/echoe) for defining gemspec and performing releases to rubygems.org, which can be done with +```bash rake release +``` The tests are written with rspec. Given you have rake and rspec, you can run tests with +```bash rake test +``` **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)** @@ -126,6 +147,7 @@ The tests are written with rspec. Given you have rake and rspec, you can run tes * Ariel Salomon (Oscil8) * Paul Battley * Zane Shannon [@zshannon](https://github.com/zshannon) + * Tim Rudat [@excpt](https://github.com/excpt) ## License From 94b678840454ab90ee74d6fe8ca39470aed46585 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Fri, 2 Jan 2015 05:04:35 +0100 Subject: [PATCH 33/35] Update README.md --- README.md | 90 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a9fd9cf1..792cc8cb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,35 @@ [![Code Climate](https://codeclimate.com/github/excpt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/excpt/ruby-jwt) [![Test Coverage](https://codeclimate.com/github/excpt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/excpt/ruby-jwt) -A Ruby implementation of [JSON Web Token (JWT) - draft-ietf-oauth-json-web-token-32](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32). +## Goal + +The goal is to implement the complete JWT spec including the underlying specs JWS, JWA, JWK and JWE. + +* [JSON Web Token (JWT)](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32) +* [JSON Web Signature (JWS)](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-39) +* [JSON Web Encryption (JWE)](https://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-39) +* [JSON Web Key (JWK)](https://tools.ietf.org/html/draft-ietf-jose-json-web-key-39) +* [JSON Web Algorithms (JWA)](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-39) + +## Algorithms + +The JWT spec supports several algorithms for cryptographic signing. This library currently supports: + +**HMAC** + +* HS256 - HMAC using SHA-256 hash algorithm (default) +* HS384 - HMAC using SHA-384 hash algorithm +* HS512 - HMAC using SHA-512 hash algorithm + +**RSA** + +* RS256 - RSA using SHA-256 hash algorithm +* RS384 - RSA using SHA-384 hash algorithm +* RS512 - RSA using SHA-512 hash algorithm + +**NONE** + +* NONE - No signature ## Installing @@ -17,42 +45,28 @@ sudo gem install jwt ## Usage +Signing a JSON Web Token. + ```ruby - JWT.encode({"some" => "payload"}, "secret") +JWT.encode({"some" => "payload"}, "secret") ``` -Note the resulting JWT will not be encrypted, but verifiable with a secret key. +Note: The resulting JWT will not be encrypted, but verifiable with a secret key. ```ruby - JWT.decode("someJWTstring", "secret") +JWT.decode("someJWTstring", "secret") ``` If the secret is wrong, it will raise a `JWT::DecodeError` telling you as such. You can still get at the payload by setting the verify argument to false. ```ruby - JWT.decode("someJWTstring", nil, false) +JWT.decode("someJWTstring", nil, false) ``` -## Algorithms - -The JWT spec supports several algorithms for cryptographic signing. This library currently supports: - -**HMAC** - -* HS256 - HMAC using SHA-256 hash algorithm (default) -* HS384 - HMAC using SHA-384 hash algorithm -* HS512 - HMAC using SHA-512 hash algorithm - -**RSA** - -* RS256 - RSA using SHA-256 hash algorithm -* RS384 - RSA using SHA-384 hash algorithm -* RS512 - RSA using SHA-512 hash algorithm - Change the algorithm with by setting it in encode: ```ruby - JWT.encode({"some" => "payload"}, "secret", "HS512") +JWT.encode({"some" => "payload"}, "secret", "HS512") ``` **Plaintext** @@ -60,8 +74,8 @@ Change the algorithm with by setting it in encode: We also support unsigned plaintext JWTs as introduced by draft 03 by explicitly specifying `nil` as the key and algorithm: ```ruby - jwt = JWT.encode({"some" => "payload"}, nil, nil) - JWT.decode(jwt, nil, nil) +jwt = JWT.encode({"some" => "payload"}, nil, nil) +JWT.decode(jwt, nil, nil) ``` ## Support for reserved claim names @@ -85,19 +99,19 @@ From [draft 01 of the JWT spec](http://self-issued.info/docs/draft-jones-json-we You pass the expiration time as a UTC UNIX timestamp (an int). For example: ```ruby - JWT.encode({"exp": 1371720939}, "secret") - JWT.encode({"exp": Time.now.to_i()}, "secret") +JWT.encode({"exp": 1371720939}, "secret") +JWT.encode({"exp": Time.now.to_i()}, "secret") ``` Expiration time is automatically verified in `JWT.decode()` and raises `JWT::ExpiredSignature` if the expiration time is in the past: ```ruby - begin - JWT.decode("JWT_STRING", "secret") - rescue JWT::ExpiredSignature - # Signature has expired - end +begin + JWT.decode("JWT_STRING", "secret") +rescue JWT::ExpiredSignature + # Signature has expired +end ``` Expiration time will be compared to the current UTC time (as given by @@ -112,11 +126,11 @@ after creation but you know that sometimes you will process it after 30 seconds, you can set a leeway of 10 seconds in order to have some margin: ```ruby - jwt_payload = JWT.encode({'exp': Time.now.to_i + 30}, 'secret') - sleep(32) - # jwt_payload is now expired - # But with some leeway, it will still validate - JWT.decode(jwt_payload, 'secret', true, leeway=10) +jwt_payload = JWT.encode({'exp': Time.now.to_i + 30}, 'secret') +sleep(32) +# jwt_payload is now expired +# But with some leeway, it will still validate +JWT.decode(jwt_payload, 'secret', true, leeway=10) ``` ## Development and Tests @@ -124,13 +138,13 @@ you can set a leeway of 10 seconds in order to have some margin: We depend on [Echoe](http://rubygems.org/gems/echoe) for defining gemspec and performing releases to rubygems.org, which can be done with ```bash - rake release +rake release ``` The tests are written with rspec. Given you have rake and rspec, you can run tests with ```bash - rake test +rake test ``` **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)** From 7eef40e704fc091651b89c49eeb5f4717d994372 Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Fri, 2 Jan 2015 23:25:30 +0100 Subject: [PATCH 34/35] Update README.md Change examples to use to current hash syntax since ruby 1.8 dropped in version 2.x. --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 792cc8cb..2f36bf89 100644 --- a/README.md +++ b/README.md @@ -48,25 +48,25 @@ sudo gem install jwt Signing a JSON Web Token. ```ruby -JWT.encode({"some" => "payload"}, "secret") +JWT.encode({some: 'payload'}, 'secret') ``` Note: The resulting JWT will not be encrypted, but verifiable with a secret key. ```ruby -JWT.decode("someJWTstring", "secret") +JWT.decode('someJWTstring', 'secret') ``` If the secret is wrong, it will raise a `JWT::DecodeError` telling you as such. You can still get at the payload by setting the verify argument to false. ```ruby -JWT.decode("someJWTstring", nil, false) +JWT.decode('someJWTstring', nil, false) ``` Change the algorithm with by setting it in encode: ```ruby -JWT.encode({"some" => "payload"}, "secret", "HS512") +JWT.encode({some: 'payload'}, 'secret', 'HS512') ``` **Plaintext** @@ -74,7 +74,7 @@ JWT.encode({"some" => "payload"}, "secret", "HS512") We also support unsigned plaintext JWTs as introduced by draft 03 by explicitly specifying `nil` as the key and algorithm: ```ruby -jwt = JWT.encode({"some" => "payload"}, nil, nil) +jwt = JWT.encode({some: 'payload'}, nil, nil) JWT.decode(jwt, nil, nil) ``` @@ -99,8 +99,8 @@ From [draft 01 of the JWT spec](http://self-issued.info/docs/draft-jones-json-we You pass the expiration time as a UTC UNIX timestamp (an int). For example: ```ruby -JWT.encode({"exp": 1371720939}, "secret") -JWT.encode({"exp": Time.now.to_i()}, "secret") +JWT.encode({exp: 1371720939}, 'secret') +JWT.encode({exp: Time.now.to_i()}, 'secret') ``` Expiration time is automatically verified in `JWT.decode()` and raises @@ -108,7 +108,7 @@ Expiration time is automatically verified in `JWT.decode()` and raises ```ruby begin - JWT.decode("JWT_STRING", "secret") + JWT.decode('JWT_STRING', 'secret') rescue JWT::ExpiredSignature # Signature has expired end @@ -126,7 +126,7 @@ after creation but you know that sometimes you will process it after 30 seconds, you can set a leeway of 10 seconds in order to have some margin: ```ruby -jwt_payload = JWT.encode({'exp': Time.now.to_i + 30}, 'secret') +jwt_payload = JWT.encode({exp: Time.now.to_i + 30}, 'secret') sleep(32) # jwt_payload is now expired # But with some leeway, it will still validate From 8088bb163c99a31e26befc91feb0cad9af1fd53c Mon Sep 17 00:00:00 2001 From: Tim Rudat Date: Sat, 3 Jan 2015 06:44:25 +0100 Subject: [PATCH 35/35] Add codeclimate reporting fix provided by @jonathancadepowers --- spec/spec_helper.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bcd1db55..63031f60 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,7 +4,10 @@ SimpleCov.configure do root File.join(File.dirname(__FILE__), '..') project_name 'Ruby JWT - Ruby JSON Web Token implementation' - formatter SimpleCov::Formatter::HTMLFormatter + SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ + CodeClimate::TestReporter::Formatter, + SimpleCov::Formatter::HTMLFormatter + ] add_filter 'spec' end