-
Notifications
You must be signed in to change notification settings - Fork 374
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Decode class rewritten to increase understanding and to soon support …
…custom algorithms
- Loading branch information
Showing
8 changed files
with
254 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'jwt/signature' | ||
require 'jwt/verify' | ||
require 'jwt/x5c_key_finder' | ||
|
||
module JWT | ||
class DecodeToken | ||
def initialize(token, options = {}) | ||
raise ArgumentError, 'Provided token is not a String object' unless token.is_a?(String) | ||
|
||
@token = token | ||
@options = options | ||
end | ||
|
||
def decoded_segments | ||
validate_segment_count! | ||
|
||
if verify? | ||
verify_alg_header! | ||
verify_signature! | ||
verify_claims! | ||
end | ||
|
||
[payload, header] | ||
end | ||
|
||
private | ||
|
||
attr_reader :token, :options | ||
|
||
def algorithms | ||
@algorithms ||= Array(options[:algorithms]) | ||
end | ||
|
||
def segments | ||
@segments ||= token.split('.') | ||
end | ||
|
||
def signature | ||
@signature ||= Base64.urlsafe_decode64(segments[2] || '') | ||
end | ||
|
||
def header | ||
@header ||= decode_header(segments[0]) | ||
end | ||
|
||
def payload | ||
@payload ||= decode_payload(segments[1]) | ||
end | ||
|
||
def signing_input | ||
segments.first(2).join('.') | ||
end | ||
|
||
def verify? | ||
options[:verify] != false | ||
end | ||
|
||
def key | ||
@key ||= | ||
if options[:jwks] | ||
::JWT::JWK::KeyFinder.new(jwks: options[:jwks]).key_for(header['kid']) | ||
elsif (x5c_options = options[:x5c]) | ||
::JWT::X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c']) | ||
else | ||
options[:key] | ||
end | ||
end | ||
|
||
def verify_alg_header! | ||
return unless valid_algorithms.empty? | ||
|
||
raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' | ||
end | ||
|
||
def valid_algorithms | ||
@valid_algorithms ||= algorithms.select do |algorithm| | ||
if algorithm.is_a?(String) | ||
header['alg'] == algorithm | ||
else | ||
algorithm.valid_alg?(header['alg']) | ||
end | ||
end | ||
end | ||
|
||
def verify_signature! | ||
return if valid_algorithms.any? { |algorithm| verify_signature_for?(algorithm, key) } | ||
|
||
raise JWT::VerificationError, 'Signature verification failed' | ||
end | ||
|
||
def verify_signature_for?(algorithm, key) | ||
if algorithm.is_a?(String) | ||
raise JWT::DecodeError, 'No verification key available' unless key | ||
|
||
Array(key).any? { |k| Signature.verify(algorithm, k, signing_input, signature) } | ||
else | ||
algorithm.verify(signing_input, signature, key: key, header: header, payload: payload) | ||
end | ||
end | ||
|
||
def verify_claims! | ||
Verify.verify_claims(payload, options) | ||
Verify.verify_required_claims(payload, options) | ||
end | ||
|
||
def validate_segment_count! | ||
segment_count = segments.size | ||
|
||
return if segment_count == 3 | ||
return if segment_count == 2 && (!verify? || header['alg'] == 'none') | ||
|
||
raise JWT::DecodeError, 'Not enough or too many segments' | ||
end | ||
|
||
def decode_header(raw_header) | ||
decode_segment_default(raw_header) | ||
end | ||
|
||
def decode_payload(raw_segment) | ||
if @options[:decode_payload_proc] | ||
@options[:decode_payload_proc].call(raw_segment, header, signature) | ||
else | ||
decode_segment_default(raw_segment) | ||
end | ||
end | ||
|
||
def decode_segment_default(raw_segment) | ||
json_parse(Base64.urlsafe_decode64(raw_segment)) | ||
rescue ArgumentError | ||
raise JWT::DecodeError, 'Invalid segment encoding' | ||
end | ||
|
||
def json_parse(decoded_segment) | ||
JWT::JSON.parse(decoded_segment) | ||
rescue ::JSON::ParserError | ||
raise JWT::DecodeError, 'Invalid segment encoding' | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe 'Custom Signing algorithm' do | ||
let(:payload) { { 'pay' => 'load'} } | ||
|
||
let(:signing_algo) do | ||
Class.new do | ||
class << self | ||
def alg | ||
'CustomStatic' | ||
end | ||
|
||
def valid_alg?(_alg) | ||
true | ||
end | ||
|
||
def sign(_to_sign, _options) | ||
'static' | ||
end | ||
|
||
def verify(_to_verify, signature, _options) | ||
signature == 'static' | ||
end | ||
end | ||
end | ||
end | ||
|
||
subject(:extension) do | ||
algo = signing_algo | ||
|
||
Class.new do | ||
include JWT | ||
algorithm algo | ||
end | ||
end | ||
|
||
context 'when encoding' do | ||
it 'adds the custom signature to the end' do | ||
expect(::Base64.decode64(subject.encode!(payload).split('.')[2])).to eq('static') | ||
end | ||
end | ||
|
||
context 'when decoding signed token' do | ||
let(:presigned_token) { subject.encode!(payload) } | ||
it 'verifies and decodes the payload' do | ||
expect(subject.decode!(presigned_token)).to eq([{'pay' => 'load'}, {'alg' => 'CustomStatic'}]) | ||
end | ||
end | ||
end |