require 'soar_xt' require 'jwt' module SoarAuthenticationToken class TokenValidator DEFAULT_CONFIGURATION = { 'expiry' => 604800 #a days worth of seconds } unless defined? DEFAULT_CONFIGURATION; DEFAULT_CONFIGURATION.freeze def initialize(configuration) @configuration = merge_with_default_configuration(configuration) validate_configuration @public_key = OpenSSL::PKey::EC.new(@configuration['public_key']) @public_key.private_key = nil end def inject_store_provider(store_provider) @store_provider = store_provider end def validate(authentication_token:,flow_identifier: nil) return validate_locally(authentication_token) if 'local' == @configuration['mode'] return validate_remotely(authentication_token,flow_identifier) end def token(authentication_token:,flow_identifier: nil) return validate_locally(authentication_token) if 'local' == @configuration['mode'] return validate_remotely(authentication_token,flow_identifier) end private def validate_locally(authentication_token) meta = token_meta(authentication_token) return [false, nil] if token_expired?(meta) return [false, nil] if token_exist_in_store?(meta) [true, meta] rescue JWT::VerificationError, JWT::DecodeError [false, nil] end def token_meta(authentication_token) decoded_token_payload = decode(authentication_token) { 'token_identifier' => decoded_token_payload[0]['token_identifier'], 'authenticated_identifier' => decoded_token_payload[0]['authenticated_identifier'], 'token_issue_time' => decoded_token_payload[0]['token_issue_time'], 'token_expiry_time' => decoded_token_payload[0]['token_expiry_time'], 'token_age' => token_age(decoded_token_payload[0]['token_issue_time']) } end def token_age(token_issue_time) Time.now - Time.parse(token_issue_time) end def validate_remotely(authentication_token,flow_identifier) uri = URI.parse(@configuration['validator-url']) request = Net::HTTP::Post.new uri request.set_form_data({'flow_identifier' => flow_identifier}) request.body = { 'authentication_token' => authentication_token }.to_json response = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') { |http| http.request request } validate_and_extract_information_from_response(response) end def validate_and_extract_information_from_response(response) raise "Failure validating token with token validation service. Code #{response.code} received" if '200' != response.code body = JSON.parse(response.body) if 'success' == body['status'] raise 'Token validation service did not provide authenticated_identifier' if body['data'].nil? or body['data']['authenticated_identifier'].nil? return [true, body['data']] end if 'fail' == body['status'] return [false, nil] end raise "Failure validating token with token validation service. Status '#{body['status']}' received" end def validate_configuration raise "'mode' must be configured" unless @configuration['mode'] raise "'mode' must be configured as either 'local' or 'remote'" unless ['local','remote'].include?(@configuration['mode']) if 'remote' == @configuration['mode'] raise "'validator-url' must be configured in remote mode" unless @configuration['validator-url'] else raise "'public_key' must be configured in local mode" unless @configuration['public_key'] raise "'expiry' must be configured in local mode" unless @configuration['expiry'] raise "'expiry' must be an integer" unless Integer(@configuration['expiry']) end end def merge_with_default_configuration(configuration) Hash.deep_merge(DEFAULT_CONFIGURATION,configuration) end def decode(authentication_token) JWT.decode(authentication_token, @public_key, true, { :algorithm => 'ES512' }) end def token_expired?(meta) Time.parse(meta['token_expiry_time']) < Time.now end def token_exist_in_store?(meta) @store_provider.token_exist?( token_identifier: nil,#meta['token_identifier'], #TODO FIX THIS authenticated_identifier: meta['authenticated_identifier'], token_issue_time: meta['token_issue_time'], token_expiry_time: meta['token_expiry_time']) end end end