require 'soar_xt' require 'jwt' require 'authenticated_client' 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,flow_identifier) if 'local' == @configuration['mode'] return validate_statically(authentication_token) if 'static' == @configuration['mode'] return validate_remotely(authentication_token,flow_identifier) if 'remote' == @configuration['mode'] raise 'invalid validation mode configured' end private def validate_statically(authentication_token) found_static_token = find_configured_static_token(authentication_token) return rejection_result(reason: 'Unknown static token') if found_static_token.nil? meta = compile_meta(token_identifier: 'static_token', authenticated_identifier: found_static_token['authenticated_identifier'], token_issue_time: found_static_token['token_issue_time'], token_expiry_time: found_static_token['token_expiry_time']) return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta) return success_result(token_meta: meta) end def find_configured_static_token(authentication_token) @configuration['static_tokens'].each { |static_token| if authentication_token == static_token['token'] return static_token end } nil end def validate_locally(authentication_token,flow_identifier) meta = decode_token_meta(authentication_token) return rejection_result(reason: "Expired token <#{meta['token_expiry_time']}> for <#{meta['authenticated_identifier']}>") if token_expired?(meta) return rejection_result(reason: "Unknown token for <#{meta['authenticated_identifier']}>") unless token_exist_in_store?(meta,flow_identifier) success_result(token_meta: meta) rescue JWT::VerificationError, JWT::DecodeError rejection_result(reason: 'Token decode/verification failure') end def decode_token_meta(authentication_token) decoded_token_payload = decode(authentication_token) compile_meta(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']) end def compile_meta(token_identifier:, authenticated_identifier:, token_issue_time:, token_expiry_time:) { 'token_identifier' => token_identifier, 'authenticated_identifier' => authenticated_identifier, 'token_issue_time' => token_issue_time, 'token_expiry_time' => token_expiry_time, 'token_age' => token_age(token_issue_time) } end def token_age(token_issue_time) Time.now - Time.parse(token_issue_time.to_s) end def validate_remotely(authentication_token,flow_identifier) response = send_request(authentication_token,flow_identifier) validate_and_extract_information_from_response(response) end def send_request(authentication_token,flow_identifier) uri = URI.parse(@configuration['validator-url']) uri.query = URI.encode_www_form( {'flow_identifier' => flow_identifier} ) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.is_a?(URI::HTTPS) request = Net::HTTP::Post.new(uri.request_uri) request.body = { 'authentication_token' => authentication_token }.to_json http.request(request) 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']) and body['data'] token_validity = body['data']['token_validity'] token_meta = body['data']['token_meta'] message = body['data']['message'] raise 'Token validation service did not provide token_validity' if token_validity.nil? raise 'Token validation service did not provide token_meta' if token_validity and token_meta.nil? raise 'Token validation service did not provide message' if message.nil? return [token_validity, token_meta, message] end if 'fail' == body['status'] return rejection_result(reason: 'remote validation failed') 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', 'remote' or 'static'" unless ['local','remote', 'static'].include?(@configuration['mode']) validate_remote_configuration if 'remote' == @configuration['mode'] validate_local_configuration if 'local' == @configuration['mode'] validate_static_configuration if 'static' == @configuration['mode'] end def validate_remote_configuration raise "'validator-url' must be configured in remote mode" unless @configuration['validator-url'] end def validate_local_configuration 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 def validate_static_configuration raise "'static_tokens' must be configured in local mode" unless @configuration['static_tokens'] 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'].to_s) < Time.now end def token_exist_in_store?(meta,flow_identifier) @store_provider.token_exist?( token_identifier: meta['token_identifier'], authenticated_identifier: meta['authenticated_identifier'], token_issue_time: meta['token_issue_time'], token_expiry_time: meta['token_expiry_time'], flow_identifier: flow_identifier) end def rejection_result(reason:) [false, nil, reason] end def success_result(token_meta:) [true, token_meta, "Valid token for <#{token_meta['authenticated_identifier']}>" ] end end end