require 'spec_helper' describe JSON::JWT do let(:jwt) { JSON::JWT.new claims } let(:jws) do jwt.alg = :HS256 jws = JSON::JWS.new jwt jws.signature = 'signature' jws end let(:claims) do { iss: 'joe', exp: 1300819380, 'http://example.com/is_root' => true }.with_indifferent_access end let(:no_signed) do 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' end context 'when not signed nor encrypted' do it do jwt.to_s.should == no_signed end end describe '#content_type' do it do jwt.content_type.should == 'application/jwt' end end describe '#sign' do [:HS256, :HS384, :HS512].each do |algorithm| context algorithm do it do jwt.sign(shared_secret, algorithm).should be_a JSON::JWS end end end [:RS256, :RS384, :RS512].each do |algorithm| context algorithm do it do jwt.sign(private_key, algorithm).should be_a JSON::JWS end end end context 'when no algirithm specified' do subject { jwt.sign(key) } context 'when key is String' do let(:key) { shared_secret } its(:alg) { should == :HS256 } end context 'otherwise' do let(:key) { private_key } its(:alg) { should == :RS256 } end end context 'when non-JWK key is given' do let(:key) { private_key } it 'should not set kid header automatically' do jws = jwt.sign(key, :RS256) jws.kid.should be_blank end end context 'when JWK is given' do let(:key) { JSON::JWK.new private_key } it 'should set kid header automatically' do jws = jwt.sign(key, :RS256) jwt.kid.should be_blank jws.kid.should == key[:kid] end end describe 'object copy behaviour' do before do @jwt = JSON::JWT.new(obj: {foo: :bar}) @jws = @jwt.sign('secret') end context 'when original JWT is modified' do before do @jwt.header[:x] = :x @jwt[:obj][:x] = :x end describe 'copied JWS' do it 'should be affected as shallow copy, but not as a simple reference' do @jws.header.should_not include :x @jws[:obj].should include :x end end end context 'when copied JWS is modified' do before do @jws.header[:x] = :x @jws[:obj][:x] = :x end describe 'original JWT' do it 'should be affected as shallow copy, but not as a simple reference' do @jwt.header.should_not include :x @jwt[:obj].should include :x end end end end end describe '#encrypt' do let(:shared_key) { SecureRandom.hex 16 } # default shared key is too short it 'should encryptable without signing' do jwt.encrypt(public_key).should be_a JSON::JWE end it 'should encryptable after signed' do jwt.sign(shared_key).encrypt(public_key).should be_a JSON::JWE end it 'should accept optional algorithm' do jwt.encrypt(shared_key, :dir).should be_a JSON::JWE end it 'should accept optional algorithm and encryption method' do jwt.encrypt(SecureRandom.hex(32), :dir, :'A256CBC-HS512').should be_a JSON::JWE end context 'when non-JWK key is given' do let(:key) { shared_key } it 'should not set kid header automatically' do jwe = jwt.encrypt(key, :dir) jwe.kid.should be_blank end end context 'when JWK is given' do let(:key) { JSON::JWK.new shared_key } it 'should set kid header automatically' do jwe = jwt.encrypt(key, :dir) jwt.kid.should be_blank jwe.kid.should == key[:kid] end end end describe '.decode' do context 'when not signed nor encrypted' do context 'no signature given' do it do JSON::JWT.decode(no_signed).should == jwt end end end context 'when signed' do context 'when no secret/key given' do it 'should do verification' do expect do JSON::JWT.decode jws.to_s end.to raise_error JSON::JWT::VerificationFailed end end context 'when secret/key given' do it 'should do verification' do expect do JSON::JWT.decode jws.to_s, 'secret' end.to raise_error JSON::JWT::VerificationFailed end end context 'when alg header malformed' do context 'from alg=HS256' do context 'to alg=none' do let(:malformed_jwt_string) do header, payload, signature = jws.to_s.split('.') malformed_header = {alg: :none}.to_json [ UrlSafeBase64.encode64(malformed_header), payload, '' ].join('.') end it do expect do JSON::JWT.decode malformed_jwt_string, 'secret' end.to raise_error JSON::JWT::VerificationFailed end end end context 'from alg=RS256' do let(:jws) do jwt.sign private_key, :RS256 end context 'to alg=none' do let(:malformed_jwt_string) do header, payload, signature = jws.to_s.split('.') malformed_header = {alg: :none}.to_json [ UrlSafeBase64.encode64(malformed_header), payload, '' ].join('.') end it do expect do JSON::JWT.decode malformed_jwt_string, public_key end.to raise_error JSON::JWT::UnexpectedAlgorithm end end context 'to alg=HS256' do let(:malformed_jwt_string) do header, payload, signature = jws.to_s.split('.') malformed_header = {alg: :HS256}.to_json malformed_signature = OpenSSL::HMAC.digest( OpenSSL::Digest.new('SHA256'), public_key.to_s, [UrlSafeBase64.encode64(malformed_header), payload].join('.') ) [ UrlSafeBase64.encode64(malformed_header), payload, UrlSafeBase64.encode64(malformed_signature) ].join('.') end it do expect do JSON::JWT.decode malformed_jwt_string, public_key end.to raise_error JSON::JWS::UnexpectedAlgorithm end end end context 'from alg=PS512' do let(:jws) do jwt.sign private_key, :PS512 end if pss_supported? context 'to alg=PS256' do let(:malformed_jwt_string) do header, payload, signature = jws.to_s.split('.') malformed_header = {alg: :PS256}.to_json digest = OpenSSL::Digest.new('SHA256') malformed_signature = private_key.sign_pss( digest, [UrlSafeBase64.encode64(malformed_header), payload].join('.'), salt_length: :digest, mgf1_hash: digest ) [ UrlSafeBase64.encode64(malformed_header), payload, UrlSafeBase64.encode64(malformed_signature) ].join('.') end context 'when verification algorithm is specified' do it do expect do JSON::JWT.decode malformed_jwt_string, public_key, :PS512 end.to raise_error JSON::JWS::UnexpectedAlgorithm, 'Unexpected alg header' end end context 'otherwise' do it do expect do JSON::JWT.decode malformed_jwt_string, public_key end.not_to raise_error end end end context 'to alg=RS516' do let(:malformed_jwt_string) do header, payload, signature = jws.to_s.split('.') malformed_header = {alg: :RS512}.to_json malformed_signature = private_key.sign( OpenSSL::Digest.new('SHA512'), [UrlSafeBase64.encode64(malformed_header), payload].join('.') ) [ UrlSafeBase64.encode64(malformed_header), payload, UrlSafeBase64.encode64(malformed_signature) ].join('.') end context 'when verification algorithm is specified' do it do expect do JSON::JWT.decode malformed_jwt_string, public_key, :PS512 end.to raise_error JSON::JWS::UnexpectedAlgorithm, 'Unexpected alg header' end end context 'otherwise' do it do expect do JSON::JWT.decode malformed_jwt_string, public_key end.not_to raise_error end end end else skip 'RSA PSS not supported' it do expect { jws }.to raise_error 'PS512 isn\'t supported. OpenSSL gem v2.1.0+ is required to use PS512.' end end end end context 'when :skip_verification given as secret/key' do it 'should skip verification' do expect do jwt = JSON::JWT.decode jws.to_s, :skip_verification jwt.header.should == {'alg' => 'HS256', 'typ' => 'JWT'} end.not_to raise_error end end context 'when JSON Serialization given' do let(:signed) { JSON::JWT.new(claims).sign('secret') } shared_examples_for :json_serialization_parser do context 'when proper secret given' do it { JSON::JWT.decode(serialized, 'secret').should == signed } end context 'when verification skipped' do it { JSON::JWT.decode(serialized, :skip_verification).should == signed } end context 'when wrong secret given' do it do expect do JSON::JWT.decode serialized, 'wrong' end.to raise_error JSON::JWT::VerificationFailed end end end context 'when general' do let(:serialized) do { payload: UrlSafeBase64.encode64(claims.to_json), signatures: [{ protected: UrlSafeBase64.encode64(signed.header.to_json), signature: UrlSafeBase64.encode64(signed.signature) }] } end it_behaves_like :json_serialization_parser end context 'when flattened' do let(:serialized) do { protected: UrlSafeBase64.encode64(signed.header.to_json), payload: UrlSafeBase64.encode64(claims.to_json), signature: UrlSafeBase64.encode64(signed.signature) } end it_behaves_like :json_serialization_parser end end end context 'when encrypted' do let(:input) { jwt.encrypt(public_key).to_s } let(:shared_key) { SecureRandom.hex 16 } # default shared key is too short it 'should decryptable' do JSON::JWT.decode(input, private_key).should be_instance_of JSON::JWE end context 'when :skip_decryption given as secret/key' do it 'should skip verification' do expect do jwe = JSON::JWT.decode input, :skip_decryption jwe.should be_instance_of JSON::JWE jwe.header.should == {'alg' => 'RSA1_5', 'enc' => 'A128CBC-HS256'} end.not_to raise_error end end context 'when alg & enc is specified' do context 'when expected' do it do expect do JSON::JWT.decode(input, private_key, 'RSA1_5', 'A128CBC-HS256') end.not_to raise_error end end context 'when alg is unexpected' do it do expect do JSON::JWT.decode(input, private_key, 'dir', 'A128CBC-HS256') end.to raise_error JSON::JWE::UnexpectedAlgorithm, 'Unexpected alg header' end end context 'when enc is unexpected' do it do expect do JSON::JWT.decode(input, private_key, 'RSA1_5', 'A128GCM') end.to raise_error JSON::JWE::UnexpectedAlgorithm, 'Unexpected enc header' end end end end context 'when JSON parse failed' do it do expect do JSON::JWT.decode('header.payload.signature') end.to raise_error JSON::JWT::InvalidFormat end end context 'when unexpected format' do context 'when too few dots' do it do expect do JSON::JWT.decode 'header' end.to raise_error JSON::JWT::InvalidFormat end end context 'when too many dots' do it do expect do JSON::JWT.decode 'header.payload.signature.something.wrong' end.to raise_error JSON::JWT::InvalidFormat end end end end describe '.pretty_generate' do subject { JSON::JWT.pretty_generate jws.to_s } its(:size) { should == 2 } its(:first) do should == <<~HEADER.chop { "typ": "JWT", "alg": "HS256" } HEADER end its(:last) do should == <<~HEADER.chop { "iss": "joe", "exp": 1300819380, "http://example.com/is_root": true } HEADER end end end