diff --git a/lib/jwt.rb b/lib/jwt.rb index c5810091..238d11ca 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -1,5 +1,6 @@ require 'base64' require 'openssl' +require 'jwt/decode' require 'jwt/json' # JSON Web Token implementation @@ -71,11 +72,6 @@ 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 base64url_encode(str) Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '') end @@ -107,34 +103,10 @@ def encode(payload, key, algorithm = 'HS256', header_fields = {}) segments.join('.') end - def raw_segments(jwt, verify = true) - segments = jwt.split('.') - required_num_segments = verify ? [3] : [2, 3] - fail(JWT::DecodeError, '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) + def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder) fail(JWT::DecodeError, 'Nil JSON web token') unless jwt - header, payload, signature, signing_input = decoded_segments(jwt, verify) - fail(JWT::DecodeError, 'Not enough or too many segments') unless header && payload - - default_options = { + options = { verify_expiration: true, verify_not_before: true, verify_iss: false, @@ -145,42 +117,22 @@ def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) leeway: 0 } - options = default_options.merge(options) + merged_options = options.merge(custom_options) + + decoder = Decode.new jwt, key, verify, merged_options, &keyfinder + header, payload, signature, signing_input = decoder.decode_segments + decoder.verify + + fail(JWT::DecodeError, 'Not enough or too many segments') unless header && payload if verify algo, key = signature_algorithm_and_key(header, key, &keyfinder) - if options[:algorithm] && algo != options[:algorithm] + if merged_options[:algorithm] && algo != merged_options[:algorithm] fail JWT::IncorrectAlgorithm, 'Expected a different algorithm' end verify_signature(algo, key, signing_input, signature) end - if options[:verify_expiration] && payload.include?('exp') - fail(JWT::ExpiredSignature, 'Signature has expired') unless payload['exp'].to_i > (Time.now.to_i - options[:leeway]) - end - if options[:verify_not_before] && payload.include?('nbf') - fail(JWT::ImmatureSignature, 'Signature nbf has not been reached') unless payload['nbf'].to_i <= (Time.now.to_i + options[:leeway]) - end - if options[:verify_iss] && options[:iss] - fail(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options[:iss]}, received #{payload['iss'] || ''}") unless payload['iss'].to_s == options[:iss].to_s - end - if options[:verify_iat] && payload.include?('iat') - fail(JWT::InvalidIatError, 'Invalid iat') unless payload['iat'].is_a?(Integer) && payload['iat'].to_i <= (Time.now.to_i + options[:leeway]) - end - if options[:verify_aud] && options[:aud] - if payload[:aud].is_a?(Array) - fail(JWT::InvalidAudError, 'Invalid audience') unless payload['aud'].include?(options[:aud].to_s) - else - fail(JWT::InvalidAudError, "Invalid audience. Expected #{options[:aud]}, received #{payload['aud'] || ''}") unless payload['aud'].to_s == options[:aud].to_s - end - end - if options[:verify_sub] && options.include?(:sub) - fail(JWT::InvalidSubError, "Invalid subject. Expected #{options[:sub]}, received #{payload['sub'] || ''}") unless payload['sub'].to_s == options[:sub].to_s - end - if options[:verify_jti] - fail(JWT::InvalidJtiError, 'Missing jti') if payload['jti'].to_s == '' - end - [payload, header] end diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb new file mode 100644 index 00000000..ae205734 --- /dev/null +++ b/lib/jwt/decode.rb @@ -0,0 +1,57 @@ +require 'jwt/json' +require 'jwt/verify' + +# JWT::Decode module +module JWT + extend JWT::Json + + # Decoding logic for JWT + class Decode + attr_reader :header, :payload, :signature + + def initialize(jwt, key, verify, options, &keyfinder) + @jwt = jwt + @key = key + @verify = verify + @options = options + @keyfinder = keyfinder + end + + def decode_segments + 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 raw_segments(jwt, verify) + segments = jwt.split('.') + required_num_segments = verify ? [3] : [2, 3] + fail(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length + segments + end + private :raw_segments + + def decode_header_and_payload(header_segment, payload_segment) + header = JWT.decode_json(base64url_decode(header_segment)) + payload = JWT.decode_json(base64url_decode(payload_segment)) + [header, payload] + end + private :decode_header_and_payload + + def base64url_decode(str) + str += '=' * (4 - str.length.modulo(4)) + Base64.decode64(str.tr('-_', '+/')) + end + private :base64url_decode + + def verify + @options.each do |key, val| + next unless key.to_s.match(/verify/) + + Verify.send(key, payload, @options) if val + end + end + end +end diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb new file mode 100644 index 00000000..79ba21ab --- /dev/null +++ b/lib/jwt/verify.rb @@ -0,0 +1,69 @@ +module JWT + # JWT verify methods + module Verify + def self.verify_expiration(payload, options) + return unless payload.include?('exp') + + if payload['exp'].to_i < (Time.now.to_i - options[:leeway]) + fail(JWT::ExpiredSignature, 'Signature has expired') + end + end + + def self.verify_not_before(payload, options) + return unless payload.include?('nbf') + + if payload['nbf'].to_i > (Time.now.to_i + options[:leeway]) + fail(JWT::ImmatureSignature, 'Signature nbf has not been reached') + end + end + + def self.verify_iss(payload, options) + return unless options[:iss] + + if payload['iss'].to_s != options[:iss].to_s + fail( + JWT::InvalidIssuerError, + "Invalid issuer. Expected #{options[:iss]}, received #{payload['iss'] || ''}" + ) + end + end + + def self.verify_iat(payload, options) + return unless payload.include?('iat') + + if !(payload['iat'].is_a?(Integer)) || payload['iat'].to_i > (Time.now.to_i + options[:leeway]) + fail(JWT::InvalidIatError, 'Invalid iat') + end + end + + def self.verify_jti(payload, _options) + fail(JWT::InvalidJtiError, 'Missing jti') if payload['jti'].to_s == '' + end + + def self.verify_aud(payload, options) + return unless options[:aud] + + if payload[:aud].is_a?(Array) + fail( + JWT::InvalidAudError, + 'Invalid audience' + ) unless payload['aud'].include?(options[:aud].to_s) + else + fail( + JWT::InvalidAudError, + "Invalid audience. Expected #{options[:aud]}, received #{payload['aud'] || ''}" + ) unless payload['aud'].to_s == options[:aud].to_s + end + end + + def self.verify_sub(payload, options) + return unless options[:sub] + + + fail( + JWT::InvalidSubError, + "Invalid subject. Expected #{options[:sub]}, received #{payload['sub'] || ''}" + ) unless payload['sub'].to_s == options[:sub].to_s + end + end +end diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index 5f83fc33..2bd98e81 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'jwt' +require 'jwt/decode' describe JWT do let(:payload) { { 'user_id' => 'some@user.tld' } } @@ -340,8 +341,8 @@ end let :invalid_token do - new_payload = payload.merge('sub' => 'we are not the druids you are looking for') - JWT.encode new_payload, data[:secret] + invalid_payload = payload.merge('sub' => 'we are not the druids you are looking for') + JWT.encode invalid_payload, data[:secret] end it 'invalid sub should raise JWT::InvalidSubError' do