From dc8230dcc21a8f13641202ee2fcae780b6eb00e7 Mon Sep 17 00:00:00 2001 From: Maciej Szlosarczyk Date: Fri, 20 Jul 2018 15:21:10 +0300 Subject: [PATCH] Create AuthTokenCreator and AuthTokenDecryptor classes --- .../api/v1/registrant/auth_controller.rb | 6 +- lib/auth_token.rb | 26 ------- lib/auth_token/auth_token_creator.rb | 41 ++++++++++ lib/auth_token/auth_token_decryptor.rb | 43 +++++++++++ test/fixtures/users.yml | 1 + .../lib/auth_token/auth_token_creator_test.rb | 52 +++++++++++++ .../auth_token/auth_token_decryptor_test.rb | 77 +++++++++++++++++++ .../registrant_api_authentication_test.rb | 8 +- 8 files changed, 221 insertions(+), 33 deletions(-) delete mode 100644 lib/auth_token.rb create mode 100644 lib/auth_token/auth_token_creator.rb create mode 100644 lib/auth_token/auth_token_decryptor.rb create mode 100644 test/lib/auth_token/auth_token_creator_test.rb create mode 100644 test/lib/auth_token/auth_token_decryptor_test.rb diff --git a/app/controllers/api/v1/registrant/auth_controller.rb b/app/controllers/api/v1/registrant/auth_controller.rb index bfd99baad..5de962437 100644 --- a/app/controllers/api/v1/registrant/auth_controller.rb +++ b/app/controllers/api/v1/registrant/auth_controller.rb @@ -1,5 +1,5 @@ require 'rails5_api_controller_backport' -require 'auth_token' +require 'auth_token/auth_token_creator' module Api module V1 @@ -32,8 +32,8 @@ module Api end def create_token(user) - token = AuthToken.new - hash = token.generate_token(user) + token_creator = AuthTokenCreator.create_with_defaults(user) + hash = token_creator.token_in_hash hash end end diff --git a/lib/auth_token.rb b/lib/auth_token.rb deleted file mode 100644 index 5313d603d..000000000 --- a/lib/auth_token.rb +++ /dev/null @@ -1,26 +0,0 @@ -class AuthToken - def initialize; end - - def generate_token(user, secret = Rails.application.config.secret_key_base) - cipher = OpenSSL::Cipher::AES.new(256, :CBC) - expires_at = (Time.now.utc + 2.hours).strftime("%F %T %Z") - - data = { - username: user.username, - expires_at: expires_at - } - - hashable = data.to_json - - cipher.encrypt - cipher.key = secret - encrypted = cipher.update(hashable) + cipher.final - base64_encoded = Base64.encode64(encrypted) - - { - access_token: base64_encoded, - expires_at: expires_at, - type: "Bearer" - } - end -end diff --git a/lib/auth_token/auth_token_creator.rb b/lib/auth_token/auth_token_creator.rb new file mode 100644 index 000000000..2272a3650 --- /dev/null +++ b/lib/auth_token/auth_token_creator.rb @@ -0,0 +1,41 @@ +class AuthTokenCreator + DEFAULT_VALIDITY = 2.hours + + attr_reader :user + attr_reader :key + attr_reader :expires_at + + def self.create_with_defaults(user) + self.new(user, Rails.application.config.secret_key_base, Time.now + DEFAULT_VALIDITY) + end + + def initialize(user, key, expires_at) + @user = user + @key = key + @expires_at = expires_at.utc.strftime("%F %T %Z") + end + + def hashable + { + user_ident: user.registrant_ident, + user_username: user.username, + expires_at: expires_at + }.to_json + end + + def encrypted_token + encryptor = OpenSSL::Cipher::AES.new(256, :CBC) + encryptor.encrypt + encryptor.key = key + encrypted_bytes = encryptor.update(hashable) + encryptor.final + Base64.encode64(encrypted_bytes) + end + + def token_in_hash + { + access_token: encrypted_token, + expires_at: expires_at, + type: 'Bearer' + } + end +end diff --git a/lib/auth_token/auth_token_decryptor.rb b/lib/auth_token/auth_token_decryptor.rb new file mode 100644 index 000000000..1f513bece --- /dev/null +++ b/lib/auth_token/auth_token_decryptor.rb @@ -0,0 +1,43 @@ +class AuthTokenDecryptor + attr_reader :decrypted_data + attr_reader :token + attr_reader :key + attr_reader :user + + def self.create_with_defaults(token) + self.new(token, Rails.application.config.secret_key_base) + end + + def initialize(token, key) + @token = token + @key = key + end + + def decrypt_token + decipher = OpenSSL::Cipher::AES.new(256, :CBC) + decipher.decrypt + decipher.key = key + + base64_decoded = Base64.decode64(token) + plain = decipher.update(base64_decoded) + decipher.final + + @decrypted_data = JSON.parse(plain, symbolize_names: true) + rescue OpenSSL::Cipher::CipherError + false + end + + def valid? + decrypted_data && valid_user? && still_valid? + end + + private + + def valid_user? + @user = RegistrantUser.find_by(registrant_ident: decrypted_data[:user_ident]) + @user&.username == decrypted_data[:user_username] + end + + def still_valid? + decrypted_data[:expires_at] > Time.now + end +end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index b20bd8a83..5fd2dc925 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -26,3 +26,4 @@ admin: registrant: type: RegistrantUser registrant_ident: US-1234 + username: Registrant User diff --git a/test/lib/auth_token/auth_token_creator_test.rb b/test/lib/auth_token/auth_token_creator_test.rb new file mode 100644 index 000000000..4fab724c1 --- /dev/null +++ b/test/lib/auth_token/auth_token_creator_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' +require 'openssl' +require_relative '../../../lib/auth_token/auth_token_creator' + +class AuthTokenCreatorTest < ActiveSupport::TestCase + def setup + super + + @user = users(:registrant) + time = Time.zone.parse('2010-07-05 00:30:00 +0000') + @random_bytes = SecureRandom.random_bytes(64) + @token_creator = AuthTokenCreator.new(@user, @random_bytes, time) + end + + def test_hashable_is_constructed_as_expected + expected_hashable = { user_ident: 'US-1234', user_username: 'Registrant User', + expires_at: '2010-07-05 00:30:00 UTC' }.to_json + assert_equal(expected_hashable, @token_creator.hashable) + end + + def test_encrypted_token_is_decryptable + encryptor = OpenSSL::Cipher::AES.new(256, :CBC) + encryptor.decrypt + encryptor.key = @random_bytes + + base64_decoded = Base64.decode64(@token_creator.encrypted_token) + result = encryptor.update(base64_decoded) + encryptor.final + + hashable = { user_ident: 'US-1234', user_username: 'Registrant User', + expires_at: '2010-07-05 00:30:00 UTC' }.to_json + + assert_equal(hashable, result) + end + + def test_token_in_json_returns_expected_values + @token_creator.stub(:encrypted_token, 'super_secure_token') do + token = @token_creator.token_in_hash + assert_equal('2010-07-05 00:30:00 UTC', token[:expires_at]) + assert_equal('Bearer', token[:type]) + end + end + + def test_create_with_defaults_injects_values + travel_to Time.zone.parse('2010-07-05 00:30:00 +0000') + + token_creator_with_defaults = AuthTokenCreator.create_with_defaults(@user) + assert_equal(Rails.application.config.secret_key_base, token_creator_with_defaults.key) + assert_equal('2010-07-05 02:30:00 UTC', token_creator_with_defaults.expires_at) + + travel_back + end +end diff --git a/test/lib/auth_token/auth_token_decryptor_test.rb b/test/lib/auth_token/auth_token_decryptor_test.rb new file mode 100644 index 000000000..d83de7990 --- /dev/null +++ b/test/lib/auth_token/auth_token_decryptor_test.rb @@ -0,0 +1,77 @@ +require 'test_helper' +require_relative '../../../lib/auth_token/auth_token_decryptor' +require_relative '../../../lib/auth_token/auth_token_creator' + +class AuthTokenDecryptorTest < ActiveSupport::TestCase + def setup + super + + travel_to Time.parse("2010-07-05 00:15:00 UTC") + @user = users(:registrant) + + # For testing purposes, the token needs to be random and long enough, hence: + @key = "b8+PtSq1+iXzUVnGEqciKsITNR0KmLl7uPiSTHbteqCoEBdbMLUl3GXlIDWD\nDZp1hIgKWnIMPNEgbuCa/7qccA==\n" + @faulty_key = "FALSE+iXzUVnGEqciKsITNR0KmLl7uPiSTHbteqCoEBdbMLUl3GXlIDWD\nDZp1hIgKWnIMPNEgbuCa/7qccA==\n" + + # this token corresponds to: + # {:user_ident=>"US-1234", :user_username=>"Registrant User", :expires_at=>"2010-07-05 02:15:00 UTC"} + @access_token = "q27NWIsKD5snWj9vZzJ0RcOYvgocEyu7H9yCaDjfmGi54sogovpBeALMPWTZ\nHMcdFQzSiq6b4cI0p5tO0/5UEOHic2jRzNW7mkhi+bn+Y2W9l9TJV0IdiTj9\nbaf+JvlbyaJh6+/eXIm0tuV5E8Ra9Q==\n" + end + + def teardown + super + + travel_back + end + + def test_decrypt_token_returns_a_hash_when_token_is_valid + decryptor = AuthTokenDecryptor.new(@access_token, @key) + + assert(decryptor.decrypt_token.is_a?(Hash)) + end + + def test_decrypt_token_return_false_when_token_is_invalid + faulty_decryptor = AuthTokenDecryptor.new(@access_token, @faulty_key) + refute(faulty_decryptor.decrypt_token) + end + + def test_valid_returns_true_for_valid_token + decryptor = AuthTokenDecryptor.new(@access_token, @key) + decryptor.decrypt_token + + assert(decryptor.valid?) + end + + def test_valid_returns_false_for_invalid_token + faulty_decryptor = AuthTokenDecryptor.new(@access_token, @faulty_key) + faulty_decryptor.decrypt_token + + refute(faulty_decryptor.valid?) + end + + def test_valid_returns_false_for_expired_token + travel_to Time.parse("2010-07-05 10:15:00 UTC") + + decryptor = AuthTokenDecryptor.new(@access_token, @key) + decryptor.decrypt_token + + refute(decryptor.valid?) + end + + def test_returns_false_for_non_existing_user + # This token was created from an admin user and @key. Decrypted, it corresponds to: + # {:user_ident=>nil, :user_username=>"test", :expires_at=>"2010-07-05 00:15:00 UTC"} + other_token = "rMkjgpyRcj2xOnHVwvvQ5RAS0yQepUSrw3XM5BrwM4TMH+h+TBeLve9InC/z\naPneMMnCs0NHQHt1EpH95A2YhX5P3HsyYITRErDmtlzUf21e185q/CUkW5NG\nWa4rar+6\n" + + decryptor = AuthTokenDecryptor.new(other_token, @key) + decryptor.decrypt_token + + refute(decryptor.valid?) + end + + def test_create_with_defaults_injects_values + decryptor = AuthTokenDecryptor.create_with_defaults(@access_token) + + assert_equal(Rails.application.config.secret_key_base, decryptor.key) + end +end diff --git a/test/system/api/registrant/registrant_api_authentication_test.rb b/test/system/api/registrant/registrant_api_authentication_test.rb index 82b5f48fd..72da06fff 100644 --- a/test/system/api/registrant/registrant_api_authentication_test.rb +++ b/test/system/api/registrant/registrant_api_authentication_test.rb @@ -4,7 +4,7 @@ class RegistrantApiAuthenticationTest < ApplicationSystemTestCase def setup super - @user_hash = {ident: "37010100049", first_name: 'Adam', last_name: 'Baker'} + @user_hash = {ident: '37010100049', first_name: 'Adam', last_name: 'Baker'} @existing_user = RegistrantUser.find_or_create_by_api_data(@user_hash) end @@ -15,9 +15,9 @@ class RegistrantApiAuthenticationTest < ApplicationSystemTestCase def test_request_creates_user_when_one_does_not_exist params = { - ident: "30110100103", - first_name: "John", - last_name: "Smith", + ident: '30110100103', + first_name: 'John', + last_name: 'Smith', } post '/api/v1/registrant/auth/eid', params