diff options
author | Ash McKenzie <amckenzie@gitlab.com> | 2018-11-08 20:28:13 +1100 |
---|---|---|
committer | Ash McKenzie <amckenzie@gitlab.com> | 2018-11-13 08:36:57 +1100 |
commit | a3c80014f5dc849af1933570877f8230d98417f1 (patch) | |
tree | 71b39ceca7c983125dc565356572767811a2fcbe | |
parent | b3b9817e5100ae2e827794d87ac6a6649571eddc (diff) | |
download | gitlab-ce-a3c80014f5dc849af1933570877f8230d98417f1.tar.gz |
Relocate JSONWebToken::HMACToken from EEashmckenzie/hmac-token-decode-and-tests
-rw-r--r-- | changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml | 5 | ||||
-rw-r--r-- | lib/json_web_token/hmac_token.rb | 28 | ||||
-rw-r--r-- | lib/json_web_token/token.rb | 9 | ||||
-rw-r--r-- | spec/lib/json_web_token/hmac_token_spec.rb | 133 |
4 files changed, 173 insertions, 2 deletions
diff --git a/changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml b/changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml new file mode 100644 index 00000000000..d15c5654d99 --- /dev/null +++ b/changelogs/unreleased/ashmckenzie-hmac-token-decode-and-tests.yml @@ -0,0 +1,5 @@ +--- +title: Relocate JSONWebToken::HMACToken from EE +merge_request: 22906 +author: +type: changed diff --git a/lib/json_web_token/hmac_token.rb b/lib/json_web_token/hmac_token.rb new file mode 100644 index 00000000000..ceb1b9c913f --- /dev/null +++ b/lib/json_web_token/hmac_token.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'jwt' + +module JSONWebToken + class HMACToken < Token + IAT_LEEWAY = 60 + JWT_ALGORITHM = 'HS256' + + def initialize(secret) + super() + + @secret = secret + end + + def self.decode(token, secret, leeway: IAT_LEEWAY, verify_iat: true) + JWT.decode(token, secret, true, leeway: leeway, verify_iat: verify_iat, algorithm: JWT_ALGORITHM) + end + + def encoded + JWT.encode(payload, secret, JWT_ALGORITHM) + end + + private + + attr_reader :secret + end +end diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb index ce5d6f248d0..c59beef02c9 100644 --- a/lib/json_web_token/token.rb +++ b/lib/json_web_token/token.rb @@ -1,17 +1,22 @@ # frozen_string_literal: true +require 'securerandom' + module JSONWebToken class Token attr_accessor :issuer, :subject, :audience, :id attr_accessor :issued_at, :not_before, :expire_time + DEFAULT_NOT_BEFORE_TIME = 5 + DEFAULT_EXPIRE_TIME = 60 + def initialize @id = SecureRandom.uuid @issued_at = Time.now # we give a few seconds for time shift - @not_before = issued_at - 5.seconds + @not_before = issued_at - DEFAULT_NOT_BEFORE_TIME # default 60 seconds should be more than enough for this authentication token - @expire_time = issued_at + 1.minute + @expire_time = issued_at + DEFAULT_EXPIRE_TIME @custom_payload = {} end diff --git a/spec/lib/json_web_token/hmac_token_spec.rb b/spec/lib/json_web_token/hmac_token_spec.rb new file mode 100644 index 00000000000..f2cbc381967 --- /dev/null +++ b/spec/lib/json_web_token/hmac_token_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'json' +require 'timecop' + +describe JSONWebToken::HMACToken do + let(:secret) { 'shh secret squirrel' } + + shared_examples 'a valid, non-expired token' do + it 'is an Array with two elements' do + expect(decoded_token).to be_a(Array) + expect(decoded_token.count).to eq(2) + end + + it 'contains the following keys in the first Array element Hash - jti, iat, nbf, exp' do + expect(decoded_token[0].keys).to include('jti', 'iat', 'nbf', 'exp') + end + + it 'contains the following keys in the second Array element Hash - typ and alg' do + expect(decoded_token[1]['typ']).to eql('JWT') + expect(decoded_token[1]['alg']).to eql('HS256') + end + end + + describe '.decode' do + let(:leeway) { described_class::IAT_LEEWAY } + let(:decoded_token) { described_class.decode(encoded_token, secret, leeway: leeway) } + + context 'with an invalid token' do + context 'that is junk' do + let(:encoded_token) { 'junk' } + + it "raises exception saying 'Not enough or too many segments'" do + expect { decoded_token }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') + end + end + + context 'that has been fiddled with' do + let(:encoded_token) do + described_class.new(secret).encoded.tap { |token| token[0] = 'E' } + end + + it "raises exception saying 'Invalid segment encoding'" do + expect { decoded_token }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') + end + end + + context 'that was generated using a different secret' do + let(:encoded_token) { described_class.new('some other secret').encoded } + + it "raises exception saying 'Signature verification raised" do + expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification raised') + end + end + + context 'that is expired' do + # Needs the ! so Timecop.freeze() is effective + let!(:encoded_token) { described_class.new(secret).encoded } + + it "raises exception saying 'Signature has expired'" do + # Needs to be 120 seconds, because the default expiry is 60 seconds + # with an additional 60 second leeway. + Timecop.freeze(Time.now + 120) do + expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') + end + end + end + end + + context 'with a valid token' do + let(:encoded_token) do + hmac_token = described_class.new(secret) + hmac_token.expire_time = Time.now + expire_time + hmac_token.encoded + end + + context 'that has expired' do + let(:expire_time) { 0 } + + context 'with the default leeway' do + Timecop.freeze(Time.now + 1) do + it_behaves_like 'a valid, non-expired token' + end + end + + context 'with a leeway of 0 seconds' do + let(:leeway) { 0 } + + it "raises exception saying 'Signature has expired'" do + Timecop.freeze(Time.now + 1) do + expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') + end + end + end + end + + context 'that has not expired' do + let(:expire_time) { described_class::DEFAULT_EXPIRE_TIME } + + it_behaves_like 'a valid, non-expired token' + end + end + end + + describe '#encoded' do + let(:decoded_token) { described_class.decode(encoded_token, secret) } + + context 'without data' do + let(:encoded_token) { described_class.new(secret).encoded } + + it_behaves_like 'a valid, non-expired token' + end + + context 'with data' do + let(:data) { { secret_key: 'secret value' }.to_json } + let(:encoded_token) do + ec = described_class.new(secret) + ec[:data] = data + ec.encoded + end + + it_behaves_like 'a valid, non-expired token' + + it "contains the 'data' key in the first Array element Hash" do + expect(decoded_token[0]).to have_key('data') + end + + it 'can re-read back the data' do + expect(decoded_token[0]['data']).to eql(data) + end + end + end +end |