Skip to content

Commit

Permalink
Merge pull request #32 from petergoldstein/feature/refactor_and_prep_…
Browse files Browse the repository at this point in the history
…for_making_multi_json_optional

Allow access to header and payload without signature verification
  • Loading branch information
progrium committed Jan 30, 2014
2 parents 0108e4c + 41375f8 commit 46ec006
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 38 deletions.
100 changes: 64 additions & 36 deletions lib/jwt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

require "base64"
require "openssl"
require "multi_json"
require "jwt/json"

module JWT
class DecodeError < StandardError; end
extend JWT::Json

module_function

Expand Down Expand Up @@ -44,59 +45,86 @@ def base64url_encode(str)
Base64.encode64(str).tr("+/", "-_").gsub(/[\n=]/, "")
end

def encode(payload, key, algorithm="HS256", header_fields={})
algorithm ||= "none"
segments = []
def encoded_header(algorithm="HS256", header_fields={})
header = {"typ" => "JWT", "alg" => algorithm}.merge(header_fields)
segments << base64url_encode(MultiJson.encode(header))
segments << base64url_encode(MultiJson.encode(payload))
signing_input = segments.join(".")
base64url_encode(encode_json(header))
end

def encoded_payload(payload)
base64url_encode(encode_json(payload))
end

def encoded_signature(signing_input, key, algorithm)
if algorithm == "none"
segments << ""
""
else
signature = sign(algorithm, signing_input, key)
segments << base64url_encode(signature)
base64url_encode(signature)
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 decode(jwt, key=nil, verify=true, &keyfinder)
def raw_segments(jwt, verify=true)
segments = jwt.split(".")
raise JWT::DecodeError.new("Not enough or too many segments") unless [2,3].include? segments.length
header_segment, payload_segment, crypto_segment = segments
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(".")
begin
header = MultiJson.decode(base64url_decode(header_segment))
payload = MultiJson.decode(base64url_decode(payload_segment))
signature = base64url_decode(crypto_segment.to_s) if verify
rescue MultiJson::LoadError
raise JWT::DecodeError.new("Invalid segment encoding")
end
[header, payload, signature, signing_input]
end

def decode(jwt, key=nil, verify=true, &keyfinder)
header, payload, signature, signing_input = decoded_segments(jwt, verify)
raise JWT::DecodeError.new("Not enough or too many segments") unless header && payload

if verify
algo = header["alg"]
algo, key = signature_algorithm_and_key(header, key, &keyfinder)
verify_signature(algo, key, signing_input, signature)
end
payload
end

if keyfinder
key = keyfinder.call(header)
end
def signature_algorithm_and_key(header, key, &keyfinder)
if keyfinder
key = keyfinder.call(header)
end
[header['alg'], key]
end

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
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
payload
end

# From devise
Expand Down
16 changes: 16 additions & 0 deletions lib/jwt/json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module JWT
module Json

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
21 changes: 19 additions & 2 deletions spec/jwt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
expect(decoded_payload).to eq(@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)
Expand All @@ -125,8 +131,8 @@
end
end

it "retuns falise of the strings are different" do
expect(JWT.secure_compare("Foo", "Bar")).to be_false
it "retuns false if the strings are different" do
expect(JWT.secure_compare("Foo", "Bar")).to be_false
end
end

Expand Down Expand Up @@ -166,4 +172,15 @@
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

0 comments on commit 46ec006

Please sign in to comment.