require 'soar_xt' require 'jwt' require 'securerandom' require 'time' require 'net/http' require 'uri' require 'json' require 'authenticated_client' module SoarAuthenticationToken class TokenGenerator 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 @private_key = OpenSSL::PKey::EC.new(@configuration['private_key']) end def inject_store_provider(store_provider) @store_provider = store_provider end def generate(authenticated_identifier:, flow_identifier: nil) return generate_locally(authenticated_identifier) if 'local' == @configuration['mode'] return generate_remotely(authenticated_identifier,flow_identifier) end private def generate_locally(authenticated_identifier) token_meta = generate_meta(authenticated_identifier) token = encode(token_meta) add_token_to_store(token_meta) [token, token_meta] end def generate_remotely(authenticated_identifier,flow_identifier) client = authenticated_client(authenticated_identifier,flow_identifier) validate_and_extract_token_from_response(client.request) end def authenticated_client(authenticated_identifier,flow_identifier) client = AuthenticatedClient::Client.new client.url = @configuration['generator-url'] client.token = @configuration['generator-client-auth-token'] client.verb = :post client.parameters = {'flow_identifier' => flow_identifier} client.body = { 'authenticated_identifier' => authenticated_identifier } client end def validate_and_extract_token_from_response(response) raise "Failure generating token with token generation service. Code #{response.code}" if '200' != response.code body = JSON.parse(response.body) raise 'Failure generating token by token service' if 'success' != body['status'] raise 'Token service did not provide token' if body['data'].nil? or body['data']['token'].nil? body['data']['token'] end def generate_meta(authenticated_identifier) current_time = Time.now { 'authenticated_identifier' => authenticated_identifier, 'token_issue_time' => current_time.utc.iso8601(3), 'token_expiry_time' => (current_time + @configuration['expiry']).utc.iso8601(3), 'token_identifier' => SecureRandom.hex(32) } end def encode(meta) JWT.encode(meta, @private_key, 'ES512') 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']) validate_local_mode_configuration if 'local' == @configuration['mode'] validate_remote_mode_configuration if 'remote' == @configuration['mode'] end def validate_remote_mode_configuration raise "'generator-url' must be configured in remote mode" if @configuration['generator-url'].nil? end def validate_local_mode_configuration raise "'private_key' must be configured in local mode" unless @configuration['private_key'] raise "'expiry' must be configured in local mode" unless @configuration['expiry'] raise "'expiry' must be an integer" unless Integer(@configuration['expiry']) end def merge_with_default_configuration(configuration) configuration = {} unless configuration Hash.deep_merge(DEFAULT_CONFIGURATION,configuration) end def add_token_to_store(meta) @store_provider.add( token_identifier: meta['token_identifier'], authenticated_identifier: meta['authenticated_identifier'], token_issue_time: meta['token_issue_time'], token_expiry_time: meta['token_expiry_time']) end end end