Skip to content

Commit

Permalink
Merge pull request #117 from excpt/master
Browse files Browse the repository at this point in the history
Refactor decode and verify functionality
  • Loading branch information
excpt committed Dec 22, 2015
2 parents 59dd2e0 + 707376a commit 0a2fa6c
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 61 deletions.
70 changes: 11 additions & 59 deletions lib/jwt.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'base64'
require 'openssl'
require 'jwt/decode'
require 'jwt/json'

# JSON Web Token implementation
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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'] || '<none>'}") 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'] || '<none>'}") 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'] || '<none>'}") 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

Expand Down
57 changes: 57 additions & 0 deletions lib/jwt/decode.rb
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions lib/jwt/verify.rb
Original file line number Diff line number Diff line change
@@ -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'] || '<none>'}"
)
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'] || '<none>'}"
) 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'] || '<none>'}"
) unless payload['sub'].to_s == options[:sub].to_s
end
end
end
5 changes: 3 additions & 2 deletions spec/jwt_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'spec_helper'
require 'jwt'
require 'jwt/decode'

describe JWT do
let(:payload) { { 'user_id' => '[email protected]' } }
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 0a2fa6c

Please sign in to comment.