require 'base64' require 'digest' require 'openssl' require 'securerandom' module OAuth2 class MACToken < AccessToken # Generates a MACToken from an AccessToken and secret # # @param [AccessToken] token the OAuth2::Token instance # @option [String] secret the secret key value # @param [Hash] opts the options to create the Access Token with # @see MACToken#initialize def self.from_access_token(token, secret, options = {}) new(token.client, token.token, secret, token.params.merge(:refresh_token => token.refresh_token, :expires_in => token.expires_in, :expires_at => token.expires_at).merge(options)) end attr_reader :secret, :algorithm # Initalize a MACToken # # @param [Client] client the OAuth2::Client instance # @param [String] token the Access Token value # @option [String] secret the secret key value # @param [Hash] opts the options to create the Access Token with # @option opts [String] :refresh_token (nil) the refresh_token value # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire # @option opts [FixNum, String] :algorithm (hmac-sha-256) the algorithm to use for the HMAC digest (one of 'hmac-sha-256', 'hmac-sha-1') def initialize(client, token, secret, opts = {}) @secret = secret self.algorithm = opts.delete(:algorithm) || 'hmac-sha-256' super(client, token, opts) end # Make a request with the MAC Token # # @param [Symbol] verb the HTTP request method # @param [String] path the HTTP URL path of the request # @param [Hash] opts the options to make the request with # @see Client#request def request(verb, path, opts = {}, &block) url = client.connection.build_url(path, opts[:params]).to_s opts[:headers] ||= {} opts[:headers]['Authorization'] = header(verb, url) @client.request(verb, path, opts, &block) end # Get the headers hash (always an empty hash) def headers {} end # Generate the MAC header # # @param [Symbol] verb the HTTP request method # @param [String] url the HTTP URL path of the request def header(verb, url) timestamp = Time.now.utc.to_i nonce = Digest::MD5.hexdigest([timestamp, SecureRandom.hex].join(':')) uri = URI.parse(url) raise(ArgumentError, "could not parse \"#{url}\" into URI") unless uri.is_a?(URI::HTTP) mac = signature(timestamp, nonce, verb, uri) "MAC id=\"#{token}\", ts=\"#{timestamp}\", nonce=\"#{nonce}\", mac=\"#{mac}\"" end # Generate the Base64-encoded HMAC digest signature # # @param [Fixnum] timestamp the timestamp of the request in seconds since epoch # @param [String] nonce the MAC header nonce # @param [Symbol] verb the HTTP request method # @param [String] url the HTTP URL path of the request def signature(timestamp, nonce, verb, uri) signature = [ timestamp, nonce, verb.to_s.upcase, uri.request_uri, uri.host, uri.port, '', nil ].join("\n") strict_encode64(OpenSSL::HMAC.digest(@algorithm, secret, signature)) end # Set the HMAC algorithm # # @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256') def algorithm=(alg) @algorithm = case alg.to_s when 'hmac-sha-1' begin OpenSSL::Digest('SHA1').new rescue StandardError OpenSSL::Digest.new('SHA1') end when 'hmac-sha-256' begin OpenSSL::Digest('SHA256').new rescue StandardError OpenSSL::Digest.new('SHA256') end else raise(ArgumentError, 'Unsupported algorithm') end end private # No-op since we need the verb and path # and the MAC always goes in a header def token=(_noop) end # Base64.strict_encode64 is not available on Ruby 1.8.7 def strict_encode64(str) Base64.encode64(str).delete("\n") end end end