signet-0.17.0/ 0000755 0000041 0000041 00000000000 14256367611 013133 5 ustar www-data www-data signet-0.17.0/README.md 0000644 0000041 0000041 00000003503 14256367611 014413 0 ustar www-data www-data # Signet
:temporary_credential_uri
-
# The OAuth temporary credentials URI.
# - :authorization_uri
-
# The OAuth authorization URI.
# - :token_credential_uri
-
# The OAuth token credentials URI.
# - :client_credential_key
-
# The OAuth client credential key.
# - :client_credential_secret
-
# The OAuth client credential secret.
# - :callback
- The OAuth callback. Defaults to 'oob'.
#
# @example
# client = Signet::OAuth1::Client.new(
# :temporary_credential_uri =>
# 'https://www.google.com/accounts/OAuthGetRequestToken',
# :authorization_uri =>
# 'https://www.google.com/accounts/OAuthAuthorizeToken',
# :token_credential_uri =>
# 'https://www.google.com/accounts/OAuthGetAccessToken',
# :client_credential_key => 'anonymous',
# :client_credential_secret => 'anonymous'
# )
def initialize options = {}
update! options
end
##
# Updates an OAuth 1.0 client.
#
# @param [Hash] options
# The configuration parameters for the client.
# - :temporary_credential_uri
-
# The OAuth temporary credentials URI.
# - :authorization_uri
-
# The OAuth authorization URI.
# - :token_credential_uri
-
# The OAuth token credentials URI.
# - :client_credential_key
-
# The OAuth client credential key.
# - :client_credential_secret
-
# The OAuth client credential secret.
# - :callback
- The OAuth callback. Defaults to 'oob'.
#
# @example
# client.update!(
# :temporary_credential_uri =>
# 'https://www.google.com/accounts/OAuthGetRequestToken',
# :authorization_uri =>
# 'https://www.google.com/accounts/OAuthAuthorizeToken',
# :token_credential_uri =>
# 'https://www.google.com/accounts/OAuthGetAccessToken',
# :client_credential_key => 'anonymous',
# :client_credential_secret => 'anonymous'
# )
#
# @see Signet::OAuth1::Client#initialize
def update! options = {}
# Normalize key to String to allow indifferent access.
options = options.to_h.transform_keys(&:to_s)
self.temporary_credential_uri = options["temporary_credential_uri"]
self.authorization_uri = options["authorization_uri"]
self.token_credential_uri = options["token_credential_uri"]
# Technically... this would allow you to pass in a :client key...
# But that would be weird. Don't do that.
self.client_credential_key =
Signet::OAuth1.extract_credential_key_option "client", options
self.client_credential_secret =
Signet::OAuth1.extract_credential_secret_option "client", options
self.temporary_credential_key =
Signet::OAuth1.extract_credential_key_option "temporary", options
self.temporary_credential_secret =
Signet::OAuth1.extract_credential_secret_option "temporary", options
self.token_credential_key =
Signet::OAuth1.extract_credential_key_option "token", options
self.token_credential_secret =
Signet::OAuth1.extract_credential_secret_option "token", options
self.callback = options["callback"]
self.two_legged = options["two_legged"] || false
self
end
##
# Returns the temporary credentials URI for this client.
#
# @return [Addressable::URI] The temporary credentials URI.
def temporary_credential_uri
@temporary_credential_uri
end
alias request_token_uri temporary_credential_uri
##
# Sets the temporary credentials URI for this client.
#
# @param [Addressable::URI, String, #to_str]
# new_temporary_credential_uri
# The temporary credentials URI.
def temporary_credential_uri= new_temporary_credential_uri
if new_temporary_credential_uri.nil?
@temporary_credential_uri = nil
else
new_temporary_credential_uri =
Addressable::URI.parse new_temporary_credential_uri
@temporary_credential_uri = new_temporary_credential_uri
end
end
alias request_token_uri= temporary_credential_uri=
##
# Returns the authorization URI that the user should be redirected to.
#
# @return [Addressable::URI] The authorization URI.
#
# @see Signet::OAuth1.generate_authorization_uri
def authorization_uri options = {}
options = options.merge(
temporary_credential_key: temporary_credential_key,
callback: callback
)
return nil if @authorization_uri.nil?
Addressable::URI.parse(
::Signet::OAuth1.generate_authorization_uri(
@authorization_uri, options
)
)
end
##
# Sets the authorization URI for this client.
#
# @param [Addressable::URI, String, #to_str] new_authorization_uri
# The authorization URI.
def authorization_uri= new_authorization_uri
if new_authorization_uri.nil?
@authorization_uri = nil
else
new_authorization_uri = Addressable::URI.send(
new_authorization_uri.is_a?(Hash) ? :new : :parse,
new_authorization_uri
)
@authorization_uri = new_authorization_uri
end
end
##
# Returns the token credential URI for this client.
#
# @return [Addressable::URI] The token credential URI.
def token_credential_uri
@token_credential_uri
end
alias access_token_uri token_credential_uri
##
# Sets the token credential URI for this client.
#
# @param [Addressable::URI, Hash, String, #to_str] new_token_credential_uri
# The token credential URI.
def token_credential_uri= new_token_credential_uri
if new_token_credential_uri.nil?
@token_credential_uri = nil
else
new_token_credential_uri = Addressable::URI.send(
new_token_credential_uri.is_a?(Hash) ? :new : :parse,
new_token_credential_uri
)
@token_credential_uri = new_token_credential_uri
end
end
alias access_token_uri= token_credential_uri=
# Lots of duplicated code here, but for the sake of auto-generating
# documentation, we're going to let it slide. Oh well.
##
# Returns the client credential for this client.
#
# @return [Signet::OAuth1::Credential] The client credentials.
def client_credential
if client_credential_key && client_credential_secret
::Signet::OAuth1::Credential.new(
client_credential_key,
client_credential_secret
)
elsif !client_credential_key && !client_credential_secret
nil
else
raise ArgumentError, "The client credential key and secret must be set."
end
end
alias consumer_token client_credential
##
# Sets the client credential for this client.
#
# @param [Signet::OAuth1::Credential] new_client_credential
# The client credentials.
def client_credential= new_client_credential
if new_client_credential.nil?
@client_credential_key = nil
@client_credential_secret = nil
else
unless new_client_credential.is_a? ::Signet::OAuth1::Credential
raise TypeError,
"Expected Signet::OAuth1::Credential, " \
"got #{new_client_credential.class}."
end
@client_credential_key = new_client_credential.key
@client_credential_secret = new_client_credential.secret
end
end
alias consumer_token= client_credential=
##
# Returns the client credential key for this client.
#
# @return [String] The client credential key.
def client_credential_key
@client_credential_key
end
alias consumer_key client_credential_key
##
# Sets the client credential key for this client.
#
# @param [String, #to_str] new_client_credential_key
# The client credential key.
def client_credential_key= new_client_credential_key
if new_client_credential_key.nil?
@client_credential_key = nil
else
unless new_client_credential_key.respond_to? :to_str
raise TypeError,
"Can't convert #{new_client_credential_key.class} into String."
end
new_client_credential_key = new_client_credential_key.to_str
@client_credential_key = new_client_credential_key
end
end
alias consumer_key= client_credential_key=
##
# Returns the client credential secret for this client.
#
# @return [String] The client credential secret.
def client_credential_secret
@client_credential_secret
end
alias consumer_secret client_credential_secret
##
# Sets the client credential secret for this client.
#
# @param [String, #to_str] new_client_credential_secret
# The client credential secret.
def client_credential_secret= new_client_credential_secret
if new_client_credential_secret.nil?
@client_credential_secret = nil
else
unless new_client_credential_secret.respond_to? :to_str
raise TypeError,
"Can't convert #{new_client_credential_secret.class} " \
"into String."
end
new_client_credential_secret = new_client_credential_secret.to_str
@client_credential_secret = new_client_credential_secret
end
end
alias consumer_secret= client_credential_secret=
##
# Returns the temporary credential for this client.
#
# @return [Signet::OAuth1::Credential] The temporary credentials.
def temporary_credential
if temporary_credential_key && temporary_credential_secret
::Signet::OAuth1::Credential.new(
temporary_credential_key,
temporary_credential_secret
)
elsif !temporary_credential_key && !temporary_credential_secret
nil
else
raise ArgumentError, "The temporary credential key and secret must be set."
end
end
alias request_token temporary_credential
##
# Sets the temporary credential for this client.
#
# @param [Signet::OAuth1::Credential] new_temporary_credential
# The temporary credentials.
def temporary_credential= new_temporary_credential
if new_temporary_credential.nil?
@temporary_credential_key = nil
@temporary_credential_secret = nil
else
unless new_temporary_credential.is_a? ::Signet::OAuth1::Credential
raise TypeError,
"Expected Signet::OAuth1::Credential, " \
"got #{new_temporary_credential.class}."
end
@temporary_credential_key = new_temporary_credential.key
@temporary_credential_secret = new_temporary_credential.secret
end
end
alias request_token= temporary_credential=
##
# Returns the temporary credential key for this client.
#
# @return [String] The temporary credential key.
def temporary_credential_key
@temporary_credential_key
end
alias request_token_key temporary_credential_key
##
# Sets the temporary credential key for this client.
#
# @param [String, #to_str] new_temporary_credential_key
# The temporary credential key.
def temporary_credential_key= new_temporary_credential_key
if new_temporary_credential_key.nil?
@temporary_credential_key = nil
else
unless new_temporary_credential_key.respond_to? :to_str
raise TypeError,
"Can't convert #{new_temporary_credential_key.class} " \
"into String."
end
new_temporary_credential_key = new_temporary_credential_key.to_str
@temporary_credential_key = new_temporary_credential_key
end
end
alias request_token_key= temporary_credential_key=
##
# Returns the temporary credential secret for this client.
#
# @return [String] The temporary credential secret.
def temporary_credential_secret
@temporary_credential_secret
end
alias request_token_secret temporary_credential_secret
##
# Sets the temporary credential secret for this client.
#
# @param [String, #to_str] new_temporary_credential_secret
# The temporary credential secret.
def temporary_credential_secret= new_temporary_credential_secret
if new_temporary_credential_secret.nil?
@temporary_credential_secret = nil
else
unless new_temporary_credential_secret.respond_to? :to_str
raise TypeError,
"Can't convert #{new_temporary_credential_secret.class} " \
"into String."
end
new_temporary_credential_secret =
new_temporary_credential_secret.to_str
@temporary_credential_secret = new_temporary_credential_secret
end
end
alias request_token_secret= temporary_credential_secret=
##
# Returns the token credential for this client.
#
# @return [Signet::OAuth1::Credential] The token credentials.
def token_credential
if token_credential_key && token_credential_secret
::Signet::OAuth1::Credential.new(
token_credential_key,
token_credential_secret
)
elsif !token_credential_key && !token_credential_secret
nil
else
raise ArgumentError, "The token credential key and secret must be set."
end
end
alias access_token token_credential
##
# Sets the token credential for this client.
#
# @param [Signet::OAuth1::Credential] new_token_credential
# The token credentials.
def token_credential= new_token_credential
if new_token_credential.nil?
@token_credential_key = nil
@token_credential_secret = nil
else
unless new_token_credential.is_a? ::Signet::OAuth1::Credential
raise TypeError,
"Expected Signet::OAuth1::Credential, " \
"got #{new_token_credential.class}."
end
@token_credential_key = new_token_credential.key
@token_credential_secret = new_token_credential.secret
end
end
alias access_token= token_credential=
##
# Returns the token credential key for this client.
#
# @return [String] The token credential key.
def token_credential_key
@token_credential_key
end
alias access_token_key token_credential_key
##
# Sets the token credential key for this client.
#
# @param [String, #to_str] new_token_credential_key
# The token credential key.
def token_credential_key= new_token_credential_key
if new_token_credential_key.nil?
@token_credential_key = nil
else
unless new_token_credential_key.respond_to? :to_str
raise TypeError,
"Can't convert #{new_token_credential_key.class} " \
"into String."
end
new_token_credential_key = new_token_credential_key.to_str
@token_credential_key = new_token_credential_key
end
end
alias access_token_key= token_credential_key=
##
# Returns the token credential secret for this client.
#
# @return [String] The token credential secret.
def token_credential_secret
@token_credential_secret
end
alias access_token_secret token_credential_secret
##
# Sets the token credential secret for this client.
#
# @param [String, #to_str] new_token_credential_secret
# The token credential secret.
def token_credential_secret= new_token_credential_secret
if new_token_credential_secret.nil?
@token_credential_secret = nil
else
unless new_token_credential_secret.respond_to? :to_str
raise TypeError,
"Can't convert #{new_token_credential_secret.class} " \
"into String."
end
new_token_credential_secret =
new_token_credential_secret.to_str
@token_credential_secret = new_token_credential_secret
end
end
alias access_token_secret= token_credential_secret=
##
# Returns the callback for this client.
#
# @return [String] The OAuth callback.
def callback
@callback || ::Signet::OAuth1::OUT_OF_BAND
end
##
# Sets the callback for this client.
#
# @param [String, #to_str] new_callback
# The OAuth callback.
def callback= new_callback
if new_callback.nil?
@callback = nil
else
unless new_callback.respond_to? :to_str
raise TypeError,
"Can't convert #{new_callback.class} into String."
end
new_callback = new_callback.to_str
@callback = new_callback
end
end
##
# Returns whether the client is in two-legged mode.
#
# @return [TrueClass, FalseClass]
# true
for two-legged mode, false
otherwise.
def two_legged
@two_legged ||= false
end
##
# Sets the client for two-legged mode.
#
# @param [TrueClass, FalseClass] new_two_legged
# true
for two-legged mode, false
otherwise.
def two_legged= new_two_legged
if new_two_legged != true && new_two_legged != false
raise TypeError,
"Expected true or false, got #{new_two_legged.class}."
else
@two_legged = new_two_legged
end
end
##
# Serialize the client object to JSON.
#
# @note A serialized client contains sensitive information. Persist or transmit with care.
#
# @return [String] A serialized JSON representation of the client.
def to_json *_args
MultiJson.dump(
"temporary_credential_uri" => temporary_credential_uri,
"authorization_uri" => authorization_uri,
"token_credential_uri" => token_credential_uri,
"callback" => callback,
"two_legged" => two_legged,
"client_credential_key" => client_credential_key,
"client_credential_secret" => client_credential_secret,
"temporary_credential_key" => temporary_credential_key,
"temporary_credential_secret" => temporary_credential_secret,
"token_credential_key" => token_credential_key,
"token_credential_secret" => token_credential_secret
)
end
##
# Generates a request for temporary credentials.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :additional_parameters
-
# Non-standard additional parameters.
# - :realm
-
# The Authorization realm. See RFC 2617.
#
# @return [Array] The request object.
def generate_temporary_credential_request options = {}
verifications = {
temporary_credential_uri: "Temporary credentials URI",
client_credential_key: "Client credential key",
client_credential_secret: "Client credential secret"
}
# Make sure all required state is set
verifications.each do |(key, _value)|
raise ArgumentError, "#{key} was not set." unless send key
end
options = {
signature_method: "HMAC-SHA1",
additional_parameters: [],
realm: nil,
connection: Faraday.default_connection
}.merge(options)
method = :post
parameters = ::Signet::OAuth1.unsigned_temporary_credential_parameters(
client_credential_key: client_credential_key,
callback: callback,
signature_method: options[:signature_method],
additional_parameters: options[:additional_parameters]
)
signature = ::Signet::OAuth1.sign_parameters(
method,
temporary_credential_uri,
parameters,
client_credential_secret
)
parameters << ["oauth_signature", signature]
authorization_header = [
"Authorization",
::Signet::OAuth1.generate_authorization_header(
parameters, options[:realm]
)
]
headers = [authorization_header]
if method == :post
headers << ["Content-Type", "application/x-www-form-urlencoded"]
headers << ["Content-Length", "0"]
end
options[:connection].build_request method.to_s.downcase.to_sym do |req|
req.url(Addressable::URI.parse(
temporary_credential_uri.to_str
).normalize.to_s)
req.headers = Faraday::Utils::Headers.new headers
end
end
alias generate_request_token_request generate_temporary_credential_request
##
# Transmits a request for a temporary credential. This method does not
# have side-effects within the client.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :additional_parameters
-
# Non-standard additional parameters.
# - :realm
-
# The Authorization realm. See RFC 2617.
# - :connection
-
# The HTTP connection to use.
# Must be of type Faraday::Connection
.
#
# @return [Signet::OAuth1::Credential] The temporary credential.
#
# @example
# temporary_credential = client.fetch_temporary_credential(
# :additional_parameters => {
# :scope => 'https://mail.google.com/mail/feed/atom'
# }
# )
def fetch_temporary_credential options = {}
options[:connection] ||= Faraday.default_connection
request = generate_temporary_credential_request options
request_env = request.to_env options[:connection]
request_env[:request] ||= request
response = options[:connection].app.call request_env
return ::Signet::OAuth1.parse_form_encoded_credentials response.body if response.status.to_i == 200
message = if [400, 401, 403].include? response.status.to_i
"Authorization failed."
else
"Unexpected status code: #{response.status}."
end
message += " Server message:\n#{response.body.to_s.strip}" unless response.body.to_s.strip.empty?
raise ::Signet::AuthorizationError.new(
message, request: request, response: response
)
end
alias fetch_request_token fetch_temporary_credential
##
# Transmits a request for a temporary credential. This method updates
# the client with the new temporary credential.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :additional_parameters
-
# Non-standard additional parameters.
# - :realm
-
# The Authorization realm. See RFC 2617.
# - :connection
-
# The HTTP connection to use.
# Must be of type Faraday::Connection
.
#
# @return [Signet::OAuth1::Credential] The temporary credential.
#
# @example
# client.fetch_temporary_credential!(:additional_parameters => {
# :scope => 'https://mail.google.com/mail/feed/atom'
# })
def fetch_temporary_credential! options = {}
credential = fetch_temporary_credential options
self.temporary_credential = credential
end
alias fetch_request_token! fetch_temporary_credential!
##
# Generates a request for token credentials.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :verifier
-
# The OAuth verifier provided by the server. Required.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :realm
-
# The Authorization realm. See RFC 2617.
#
# @return [Array] The request object.
def generate_token_credential_request options = {}
verifications = {
token_credential_uri: "Token credentials URI",
client_credential_key: "Client credential key",
client_credential_secret: "Client credential secret",
temporary_credential_key: "Temporary credential key",
temporary_credential_secret: "Temporary credential secret"
}
# Make sure all required state is set
verifications.each do |(key, _value)|
raise ArgumentError, "#{key} was not set." unless send key
end
options = {
signature_method: "HMAC-SHA1",
realm: nil,
connection: Faraday.default_connection
}.merge(options)
method = :post
parameters = ::Signet::OAuth1.unsigned_token_credential_parameters(
client_credential_key: client_credential_key,
temporary_credential_key: temporary_credential_key,
signature_method: options[:signature_method],
verifier: options[:verifier]
)
signature = ::Signet::OAuth1.sign_parameters(
method,
token_credential_uri,
parameters,
client_credential_secret,
temporary_credential_secret
)
parameters << ["oauth_signature", signature]
authorization_header = [
"Authorization",
::Signet::OAuth1.generate_authorization_header(
parameters, options[:realm]
)
]
headers = [authorization_header]
headers << ["Cache-Control", "no-store"]
if method == :post
headers << ["Content-Type", "application/x-www-form-urlencoded"]
headers << ["Content-Length", "0"]
end
options[:connection].build_request method.to_s.downcase.to_sym do |req|
req.url(Addressable::URI.parse(
token_credential_uri.to_str
).normalize.to_s)
req.headers = Faraday::Utils::Headers.new headers
end
end
alias generate_access_token_request generate_token_credential_request
##
# Transmits a request for a token credential. This method does not
# have side-effects within the client.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :verifier
-
# The OAuth verifier provided by the server. Required.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :realm
-
# The Authorization realm. See RFC 2617.
# - :connection
-
# The HTTP connection to use.
# Must be of type Faraday::Connection
.
#
# @return [Signet::OAuth1::Credential] The token credential.
#
# @example
# token_credential = client.fetch_token_credential(
# :verifier => '12345'
# )
def fetch_token_credential options = {}
options[:connection] ||= Faraday.default_connection
request = generate_token_credential_request options
request_env = request.to_env options[:connection]
request_env[:request] ||= request
response = options[:connection].app.call request_env
return ::Signet::OAuth1.parse_form_encoded_credentials response.body if response.status.to_i == 200
message = if [400, 401, 403].include? response.status.to_i
"Authorization failed."
else
"Unexpected status code: #{response.status}."
end
message += " Server message:\n#{response.body.to_s.strip}" unless response.body.to_s.strip.empty?
raise ::Signet::AuthorizationError.new(
message, request: request, response: response
)
end
alias fetch_access_token fetch_token_credential
##
# Transmits a request for a token credential. This method updates
# the client with the new token credential.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :additional_parameters
-
# Non-standard additional parameters.
# - :realm
-
# The Authorization realm. See RFC 2617.
# - :connection
-
# The HTTP connection to use.
# Must be of type Faraday::Connection
.
#
# @return [Signet::OAuth1::Credential] The token credential.
#
# @example
# client.fetch_token_credential!(:verifier => '12345')
def fetch_token_credential! options = {}
credential = fetch_token_credential options
self.token_credential = credential
end
alias fetch_access_token! fetch_token_credential!
##
# Generates an authenticated request for protected resources.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :request
-
# A pre-constructed request to sign.
# - :method
-
# The HTTP method for the request. Defaults to :get.
# - :uri
-
# The URI for the request.
# - :headers
-
# The HTTP headers for the request.
# - :body
-
# The HTTP body for the request.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :realm
-
# The Authorization realm. See RFC 2617.
#
# @return [Array] The request object.
def generate_authenticated_request options = {}
verifications = {
client_credential_key: "Client credential key",
client_credential_secret: "Client credential secret"
}
unless two_legged
verifications.update(
token_credential_key: "Token credential key",
token_credential_secret: "Token credential secret"
)
end
# Make sure all required state is set
verifications.each do |(key, _value)|
raise ArgumentError, "#{key} was not set." unless send key
end
options = {
signature_method: "HMAC-SHA1",
realm: nil,
connection: Faraday.default_connection
}.merge(options)
if options[:request].is_a? Faraday::Request
request = options[:request]
else
if options[:request].is_a? Array
method, uri, headers, body = options[:request]
else
method = options[:method] || :get
uri = options[:uri]
headers = options[:headers] || []
body = options[:body] || ""
end
headers = headers.to_a if headers.is_a? Hash
request_components = {
method: method,
uri: uri,
headers: headers,
body: body
}
# Verify that we have all pieces required to return an HTTP request
request_components.each do |(key, value)|
raise ArgumentError, "Missing :#{key} parameter." unless value
end
if !body.is_a?(String) && body.respond_to?(:each)
# Just in case we get a chunked body
merged_body = StringIO.new
body.each do |chunk|
merged_body.write chunk
end
body = merged_body.string
end
raise TypeError, "Expected String, got #{body.class}." unless body.is_a? String
method = method.to_s.downcase.to_sym
request = options[:connection].build_request method do |req|
req.url Addressable::URI.parse(uri).normalize.to_s
req.headers = Faraday::Utils::Headers.new headers
req.body = body
end
end
parameters = ::Signet::OAuth1.unsigned_resource_parameters(
client_credential_key: client_credential_key,
token_credential_key: token_credential_key,
signature_method: options[:signature_method],
two_legged: two_legged
)
env = request.to_env options[:connection]
content_type = request["Content-Type"].to_s
content_type = content_type.split(";", 2).first if content_type.index ";"
if request.http_method == :post && content_type == "application/x-www-form-urlencoded"
# Serializes the body in case a hash/array was passed. Noop if already string like
encoder = Faraday::Request::UrlEncoded.new(->(_env) {})
encoder.call env
request.body = env[:body]
post_parameters = Addressable::URI.form_unencode env[:body]
parameters.concat post_parameters
end
# No need to attach URI query parameters, the .sign_parameters
# method takes care of that automatically.
signature = ::Signet::OAuth1.sign_parameters(
env[:method],
env[:url],
parameters,
client_credential_secret,
token_credential_secret
)
parameters << ["oauth_signature", signature]
request["Authorization"] = ::Signet::OAuth1.generate_authorization_header(
parameters, options[:realm]
)
request["Cache-Control"] = "no-store"
request
end
##
# Transmits a request for a protected resource.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :request
-
# A pre-constructed request to sign.
# - :method
-
# The HTTP method for the request. Defaults to :get.
# - :uri
-
# The URI for the request.
# - :headers
-
# The HTTP headers for the request.
# - :body
-
# The HTTP body for the request.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :realm
-
# The Authorization realm. See RFC 2617.
# - :connection
-
# The HTTP connection to use.
# Must be of type Faraday::Connection
.
#
# @example
# # Using Net::HTTP
# response = client.fetch_protected_resource(
# :uri => 'http://www.example.com/protected/resource'
# )
#
# @example
# # Using Typhoeus
# response = client.fetch_protected_resource(
# :request => Typhoeus::Request.new(
# 'http://www.example.com/protected/resource'
# ),
# :connection => connection
# )
#
# @return [Array] The response object.
def fetch_protected_resource options = {}
options[:connection] ||= Faraday.default_connection
request = generate_authenticated_request options
request_env = request.to_env options[:connection]
request_env[:request] ||= request
response = options[:connection].app.call request_env
return response unless response.status.to_i == 401
# When accessing a protected resource, we only want to raise an
# error for 401 responses.
message = "Authorization failed."
message += " Server message:\n#{response.body.to_s.strip}" unless response.body.to_s.strip.empty?
raise ::Signet::AuthorizationError.new(
message, request: request, response: response
)
end
end
end
end
signet-0.17.0/lib/signet/oauth_1/server.rb 0000644 0000041 0000041 00000047774 14256367611 020410 0 ustar www-data www-data # Copyright (C) 2011 The Yakima Herald-Republic.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require "faraday"
require "stringio"
require "addressable/uri"
require "signet"
require "signet/errors"
require "signet/oauth_1"
require "signet/oauth_1/credential"
module Signet
module OAuth1
class Server
# @return [Proc] lookup the value from this Proc.
attr_accessor :nonce_timestamp, :client_credential, :token_credential,
:temporary_credential, :verifier
##
# Creates an OAuth 1.0 server.
# @overload initialize(options)
# @param [Proc] nonce_timestamp verify a nonce/timestamp pair.
# @param [Proc] client_credential find a client credential.
# @param [Proc] token_credential find a token credential.
# @param [Proc] temporary_credential find a temporary credential.
# @param [Proc] verifier validate a verifier value.
#
# @example
# server = Signet::OAuth1::Server.new(
# :nonce_timestamp =>
# lambda { |n,t| OauthNonce.remember(n,t) },
# :client_credential =>
# lambda { |key| ClientCredential.find_by_key(key).to_hash },
# :token_credential =>
# lambda { |key| TokenCredential.find_by_key(key).to_hash },
# :temporary_credential =>
# lambda { |key| TemporaryCredential.find_by_key(key).to_hash },
# :verifier =>
# lambda {|verifier| Verifier.find_by_verifier(verifier).active? }
# )
def initialize options = {}
[:nonce_timestamp, :client_credential, :token_credential,
:temporary_credential, :verifier].each do |attr|
instance_variable_set "@#{attr}", options[attr]
end
end
# Constant time string comparison.
def safe_equals? left, right
check = left.bytesize ^ right.bytesize
left.bytes.zip(right.bytes) { |x, y| check |= x ^ y.to_i }
check.zero?
end
##
# Determine if the supplied nonce/timestamp pair is valid by calling
# the {#nonce_timestamp} Proc.
#
# @param [String, #to_str] nonce value from the request
# @param [String, #to_str] timestamp value from the request
# @return [Boolean] if the nonce/timestamp pair is valid.
def validate_nonce_timestamp nonce, timestamp
if @nonce_timestamp.respond_to? :call
nonce =
@nonce_timestamp.call nonce, timestamp
end
nonce ? true : false
end
##
# Find the appropriate client credential by calling
# the {#client_credential} Proc.
#
# @param [String] key provided to the {#client_credential} Proc.
# @return [Signet::OAuth1::Credential] The client credential.
def find_client_credential key
call_credential_lookup @client_credential, key
end
##
# Find the appropriate client credential by calling
# the {#token_credential} Proc.
#
# @param [String] key provided to the {#token_credential} Proc.
# @return [Signet::OAuth1::Credential] if the credential is found.
def find_token_credential key
call_credential_lookup @token_credential, key
end
##
# Find the appropriate client credential by calling
# the {#temporary_credential} Proc.
#
# @param [String] key provided to the {#temporary_credential} Proc.
# @return [Signet::OAuth1::Credential] if the credential is found.
def find_temporary_credential key
call_credential_lookup @temporary_credential, key
end
##
# Call a credential lookup, and cast the result to a proper Credential.
#
# @param [Proc] credential to call.
# @param [String] key provided to the Proc in credential
# @return [Signet::OAuth1::Credential] credential provided by
# credential
(if any).
def call_credential_lookup credential, key
cred = credential.call key if
credential.respond_to? :call
return nil if cred.nil?
return nil unless cred.respond_to?(:to_str) ||
cred.respond_to?(:to_ary) ||
cred.respond_to?(:to_hash)
if cred.instance_of? ::Signet::OAuth1::Credential
cred
else
::Signet::OAuth1::Credential.new cred
end
end
##
# Determine if the verifier is valid by calling the Proc in {#verifier}.
#
# @param [String] verifier Key provided to the {#verifier} Proc.
# @return [Boolean] if the verifier Proc returns anything other than
# nil
or false
.
def find_verifier verifier
verified = @verifier.call verifier if @verifier.respond_to? :call
verified ? true : false
end
##
# Validate and normalize the components from an HTTP request.
# @overload verify_request_components(options)
# @param [Faraday::Request] request A pre-constructed request to verify.
# @param [String] method the HTTP method , defaults to GET
# @param [Addressable::URI, String] uri the URI .
# @param [Hash, Array] headers the HTTP headers.
# @param [StringIO, String] body The HTTP body.
# @param [HTTPAdapter] adapter The HTTP adapter(optional).
# @return [Hash] normalized request components
def verify_request_components options = {}
if options[:request]
if options[:request].is_a? Faraday::Request
request = options[:request]
elsif options[:adapter]
request = options[:adapter].adapt_request options[:request]
end
method = request.http_method
uri = request.path
headers = request.headers
body = request.body
else
method = options[:method] || :get
uri = options[:uri]
headers = options[:headers] || []
body = options[:body] || ""
end
headers = headers.to_a if headers.is_a? Hash
method = method.to_s.upcase
request_components = {
method: method,
uri: uri,
headers: headers
}
# Verify that we have all the pieces required to validate the HTTP request
request_components.each do |(key, value)|
raise ArgumentError, "Missing :#{key} parameter." unless value
end
request_components[:body] = body
request_components
end
##
# Validate and normalize the HTTP Authorization header.
#
# @param [Array] headers from HTTP request.
# @return [Hash] Hash of Authorization header.
def verify_auth_header_components headers
auth_header = headers.find { |x| x[0] == "Authorization" }
raise MalformedAuthorizationError, "Authorization header is missing" if auth_header.nil? || auth_header[1] == ""
::Signet::OAuth1.parse_authorization_header(auth_header[1]).to_h.transform_keys(&:downcase)
end
##
# @overload request_realm(options)
# @param [Hash] request A pre-constructed request to verify.
# @param [String] method the HTTP method , defaults to GET
# @param [Addressable::URI, String] uri the URI .
# @param [Hash, Array] headers the HTTP headers.
# @param [StringIO, String] body The HTTP body.
# @param [HTTPAdapter] adapter The HTTP adapter(optional).
# @return [String] The Authorization realm(see RFC 2617) of the request.
def request_realm options = {}
request_components = if options[:request]
verify_request_components(
request: options[:request],
adapter: options[:adapter]
)
else
verify_request_components(
method: options[:method],
uri: options[:uri],
headers: options[:headers],
body: options[:body]
)
end
auth_header = request_components[:headers].find { |x| x[0] == "Authorization" }
raise MalformedAuthorizationError, "Authorization header is missing" if auth_header.nil? || auth_header[1] == ""
auth_hash = ::Signet::OAuth1.parse_authorization_header(auth_header[1]).to_h.transform_keys(&:downcase)
auth_hash["realm"]
end
##
# Authenticates a temporary credential request. If no oauth_callback is
# present in the request, oob
will be returned.
#
# @overload authenticate_temporary_credential_request(options)
# @param [Hash] request The configuration parameters for the request.
# @param [String] method the HTTP method , defaults to GET
# @param [Addressable::URI, String] uri the URI .
# @param [Hash, Array] headers the HTTP headers.
# @param [StringIO, String] body The HTTP body.
# @param [HTTPAdapter] adapter The HTTP adapter(optional).
# @return [String] The oauth_callback value, or false
if not valid.
def authenticate_temporary_credential_request options = {}
verifications = {
client_credential: lambda { |_x|
::Signet::OAuth1::Credential.new("Client credential key",
"Client credential secret")
}
}
verifications.each do |(key, _value)|
raise ArgumentError, "#{key} was not set." unless send key
end
request_components = if options[:request]
verify_request_components(
request: options[:request],
adapter: options[:adapter]
)
else
verify_request_components(
method: options[:method],
uri: options[:uri],
headers: options[:headers]
)
end
# body should be blank; we don't care in any case.
method = request_components[:method]
uri = request_components[:uri]
headers = request_components[:headers]
auth_hash = verify_auth_header_components headers
return false unless (client_credential = find_client_credential(
auth_hash["oauth_consumer_key"]
))
return false unless validate_nonce_timestamp(auth_hash["oauth_nonce"],
auth_hash["oauth_timestamp"])
client_credential_secret = client_credential.secret if client_credential
computed_signature = ::Signet::OAuth1.sign_parameters(
method,
uri,
# Realm isn't used, and will throw the signature off.
auth_hash.reject { |k, _v| k == "realm" }.to_a,
client_credential_secret,
nil
)
if safe_equals? computed_signature, auth_hash["oauth_signature"]
if auth_hash.fetch("oauth_callback", "oob").empty?
"oob"
else
auth_hash.fetch "oauth_callback"
end
else
false
end
end
##
# Authenticates a token credential request.
# @overload authenticate_token_credential_request(options)
# @param [Hash] request The configuration parameters for the request.
# @param [String] method the HTTP method , defaults to GET
# @param [Addressable::URI, String] uri the URI .
# @param [Hash, Array] headers the HTTP headers.
# @param [StringIO, String] body The HTTP body.
# @param [HTTPAdapter] adapter The HTTP adapter(optional).
# @return [Hash] A hash of credentials and realm for a valid request,
# or nil
if not valid.
def authenticate_token_credential_request options = {}
verifications = {
client_credential: lambda { |_x|
::Signet::OAuth1::Credential.new("Client credential key",
"Client credential secret")
},
temporary_credential: lambda { |_x|
::Signet::OAuth1::Credential.new("Temporary credential key",
"Temporary credential secret")
},
verifier: ->(_x) { "Verifier" }
}
verifications.each do |(key, _value)|
raise ArgumentError, "#{key} was not set." unless send key
end
request_components = if options[:request]
verify_request_components(
request: options[:request],
adapter: options[:adapter]
)
else
verify_request_components(
method: options[:method],
uri: options[:uri],
headers: options[:headers],
body: options[:body]
)
end
# body should be blank; we don't care in any case.
method = request_components[:method]
uri = request_components[:uri]
headers = request_components[:headers]
auth_hash = verify_auth_header_components headers
return false unless (
client_credential = find_client_credential auth_hash["oauth_consumer_key"]
)
return false unless (
temporary_credential = find_temporary_credential auth_hash["oauth_token"]
)
return false unless validate_nonce_timestamp(
auth_hash["oauth_nonce"], auth_hash["oauth_timestamp"]
)
computed_signature = ::Signet::OAuth1.sign_parameters(
method,
uri,
# Realm isn't used, and will throw the signature off.
auth_hash.reject { |k, _v| k == "realm" }.to_a,
client_credential.secret,
temporary_credential.secret
)
return nil unless safe_equals? computed_signature, auth_hash["oauth_signature"]
{ client_credential: client_credential,
temporary_credential: temporary_credential,
realm: auth_hash["realm"] }
end
##
# Authenticates a request for a protected resource.
# @overload authenticate_resource_request(options)
# @param [Hash] request The configuration parameters for the request.
# @param [String] method the HTTP method , defaults to GET
# @param [Addressable::URI, String] uri the URI .
# @param [Hash, Array] headers the HTTP headers.
# @param [StringIO, String] body The HTTP body.
# @param [Boolean] two_legged skip the token_credential lookup?
# @param [HTTPAdapter] adapter The HTTP adapter(optional).
#
# @return [Hash] A hash of the credentials and realm for a valid request,
# or nil
if not valid.
def authenticate_resource_request options = {}
verifications = {
client_credential: lambda do |_x|
::Signet::OAuth1::Credential.new("Client credential key",
"Client credential secret")
end
}
unless options[:two_legged] == true
verifications.update(
token_credential: lambda do |_x|
::Signet::OAuth1::Credential.new("Token credential key",
"Token credential secret")
end
)
end
# Make sure all required state is set
verifications.each do |(key, _value)|
raise ArgumentError, "#{key} was not set." unless send key
end
request_components = if options[:request]
verify_request_components(
request: options[:request],
adapter: options[:adapter]
)
else
verify_request_components(
method: options[:method],
uri: options[:uri],
headers: options[:headers],
body: options[:body]
)
end
method = request_components[:method]
uri = request_components[:uri]
headers = request_components[:headers]
body = request_components[:body]
if !body.is_a?(String) && body.respond_to?(:each)
# Just in case we get a chunked body
merged_body = StringIO.new
body.each do |chunk|
merged_body.write chunk
end
body = merged_body.string
end
raise TypeError, "Expected String, got #{body.class}." unless body.is_a? String
media_type = nil
headers.each do |(header, value)|
media_type = value.gsub(/^([^;]+)(;.*?)?$/, '\1') if header.casecmp("Content-Type").zero?
end
auth_hash = verify_auth_header_components headers
auth_token = auth_hash["oauth_token"]
unless options[:two_legged]
return nil if auth_token.nil?
return nil unless (token_credential = find_token_credential auth_token)
token_credential_secret = token_credential.secret if token_credential
end
return nil unless (client_credential =
find_client_credential auth_hash["oauth_consumer_key"])
return nil unless validate_nonce_timestamp(auth_hash["oauth_nonce"],
auth_hash["oauth_timestamp"])
if method == ("POST" || "PUT") &&
media_type == "application/x-www-form-urlencoded"
request_components[:body] = body
post_parameters = Addressable::URI.form_unencode body
post_parameters.each { |param| param[1] = "" if param[1].nil? }
# If the auth header doesn't have the same params as the body, it
# can't have been signed correctly(5849#3.4.1.3)
unless post_parameters.sort == auth_hash.reject { |k, _v| k.index "oauth_" }.to_a.sort
raise MalformedAuthorizationError, "Request is of type application/x-www-form-urlencoded " \
"but Authentication header did not include form values"
end
end
client_credential_secret = client_credential.secret if client_credential
computed_signature = ::Signet::OAuth1.sign_parameters(
method,
uri,
# Realm isn't used, and will throw the signature off.
auth_hash.reject { |k, _v| k == "realm" }.to_a,
client_credential_secret,
token_credential_secret
)
return nil unless safe_equals? computed_signature, auth_hash["oauth_signature"]
{ client_credential: client_credential,
token_credential: token_credential,
realm: auth_hash["realm"] }
end
end
end
end
signet-0.17.0/lib/signet/oauth_1/credential.rb 0000644 0000041 0000041 00000007611 14256367611 021176 0 ustar www-data www-data # Copyright (C) 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Signet # :nodoc:
module OAuth1
class Credential
##
# Creates a token object from a key and secret.
#
# @example
# Signet::OAuth1::Credential.new(
# :key => "dpf43f3p2l4k3l03",
# :secret => "kd94hf93k423kf44"
# )
#
# @example
# Signet::OAuth1::Credential.new([
# ["oauth_token", "dpf43f3p2l4k3l03"],
# ["oauth_token_secret", "kd94hf93k423kf44"]
# ])
#
# @example
# Signet::OAuth1::Credential.new(
# "dpf43f3p2l4k3l03", "kd94hf93k423kf44"
# )
def initialize *args
# We want to be particularly flexible in how we initialize a token
# object for maximum interoperability. However, this flexibility
# means we need to be careful about returning an unexpected value for
# key or secret to avoid difficult-to-debug situations. Thus lots
# of type-checking.
# This is cheaper than coercing to some kind of Hash with
# indifferent access. Also uglier.
key_from_hash = lambda do |parameters|
parameters["oauth_token"] ||
parameters[:oauth_token] ||
parameters["key"] ||
parameters[:key]
end
secret_from_hash = lambda do |parameters|
parameters["oauth_token_secret"] ||
parameters[:oauth_token_secret] ||
parameters["secret"] ||
parameters[:secret]
end
if args.first.respond_to? :to_hash
parameters = args.first.to_hash
@key = key_from_hash.call parameters
@secret = secret_from_hash.call parameters
unless @key && @secret
raise ArgumentError,
"Could not find both key and secret in #{hash.inspect}."
end
else
# Normalize to an Array
if !args.first.is_a?(String) &&
!args.first.respond_to?(:to_str) &&
args.first.is_a?(Enumerable)
# We need to special-case strings since they're technically
# Enumerable objects.
args = args.first.to_a
elsif args.first.respond_to? :to_ary
args = args.first.to_ary
end
if args.all? { |value| value.is_a? Array }
parameters = args.each_with_object({}) { |(k, v), h| h[k] = v; }
@key = key_from_hash.call parameters
@secret = secret_from_hash.call parameters
elsif args.size == 2
@key, @secret = args
else
raise ArgumentError,
"wrong number of arguments (#{args.size} for 2)"
end
end
raise TypeError, "Expected String, got #{@key.class}." unless @key.respond_to? :to_str
@key = @key.to_str
raise TypeError, "Expected String, got #{@secret.class}." unless @secret.respond_to? :to_str
@secret = @secret.to_str
end
attr_accessor :key
attr_accessor :secret
def to_hash
{
"oauth_token" => key,
"oauth_token_secret" => secret
}
end
alias to_h to_hash
def == other
if other.respond_to?(:key) && other.respond_to?(:secret)
key == other.key && secret == other.secret
else
false
end
end
end
end
end
signet-0.17.0/lib/signet/oauth_2.rb 0000644 0000041 0000041 00000012073 14256367611 017063 0 ustar www-data www-data # Copyright (C) 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "base64"
require "signet"
require "multi_json"
module Signet # :nodoc:
##
# An implementation of http://tools.ietf.org/html/draft-ietf-oauth-v2-10
#
# This module will be updated periodically to support newer drafts of the
# specification, as they become widely deployed.
module OAuth2
def self.parse_authorization_header field_value
auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
case auth_scheme
when /^Basic$/i
# HTTP Basic is allowed in OAuth 2
parse_basic_credentials(field_value[/^Basic\s+(.*)$/i, 1])
when /^OAuth$/i
# Other token types may be supported eventually
parse_bearer_credentials(field_value[/^OAuth\s+(.*)$/i, 1])
else
raise ParseError,
"Parsing non-OAuth Authorization headers is out of scope."
end
end
def self.parse_www_authenticate_header field_value
auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
case auth_scheme
when /^OAuth$/i
# Other token types may be supported eventually
parse_oauth_challenge(field_value[/^OAuth\s+(.*)$/i, 1])
else
raise ParseError,
"Parsing non-OAuth WWW-Authenticate headers is out of scope."
end
end
def self.parse_basic_credentials credential_string
decoded = Base64.decode64 credential_string
client_id, client_secret = decoded.split ":", 2
[["client_id", client_id], ["client_secret", client_secret]]
end
def self.parse_bearer_credentials credential_string
access_token = credential_string[/^([^,\s]+)(?:\s|,|$)/i, 1]
parameters = []
parameters << ["access_token", access_token]
auth_param_string = credential_string[/^(?:[^,\s]+)\s*,\s*(.*)$/i, 1]
if auth_param_string
# This code will rarely get called, but is included for completeness
parameters.concat Signet.parse_auth_param_list(auth_param_string)
end
parameters
end
def self.parse_oauth_challenge challenge_string
Signet.parse_auth_param_list challenge_string
end
def self.parse_credentials body, content_type
raise TypeError, "Expected String, got #{body.class}." unless body.is_a? String
case content_type
when %r{^application/json.*}
MultiJson.load body
when %r{^application/x-www-form-urlencoded.*}
Addressable::URI.form_unencode(body).to_h
else
raise ArgumentError, "Invalid content type '#{content_type}'"
end
end
##
# Generates a Basic Authorization header from a client identifier and a
# client password.
#
# @param [String] client_id
# The client identifier.
# @param [String] client_password
# The client password.
#
# @return [String]
# The value for the HTTP Basic Authorization header.
def self.generate_basic_authorization_header client_id, client_password
if client_id =~ /:/
raise ArgumentError,
"A client identifier may not contain a ':' character."
end
token = Base64.encode64("#{client_id}:#{client_password}").delete("\n")
"Basic #{token}"
end
##
# Generates an authorization header for an access token
#
# @param [String] access_token
# The access token.
# @param [Hash] auth_params
# Additional parameters to be encoded in the header
#
# @return [String]
# The value for the HTTP Basic Authorization header.
def self.generate_bearer_authorization_header \
access_token, auth_params = nil
# TODO: escaping?
header = "Bearer #{access_token}"
if auth_params && !auth_params.empty?
additional_headers = auth_params.map { |key, value| "#{key}=\"#{value}\"" }
header = ([header] + additional_headers).join ", "
end
header
end
##
# Appends the necessary OAuth parameters to
# the base authorization endpoint URI.
#
# @param [Addressable::URI, String, #to_str] authorization_uri
# The base authorization endpoint URI.
#
# @return [String] The authorization URI to redirect the user to.
def self.generate_authorization_uri authorization_uri, parameters = {}
parameters.each do |key, value|
parameters.delete key if value.nil?
end
parsed_uri = Addressable::URI.parse(authorization_uri).dup
query_values = parsed_uri.query_values || {}
query_values = query_values.merge parameters
parsed_uri.query_values = query_values
parsed_uri.normalize.to_s
end
end
end
signet-0.17.0/lib/signet/version.rb 0000644 0000041 0000041 00000001215 14256367611 017203 0 ustar www-data www-data # Copyright (C) 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Signet
VERSION = "0.17.0".freeze
end
signet-0.17.0/lib/signet/oauth_2/ 0000755 0000041 0000041 00000000000 14256367611 016533 5 ustar www-data www-data signet-0.17.0/lib/signet/oauth_2/client.rb 0000644 0000041 0000041 00000122625 14256367611 020346 0 ustar www-data www-data # Copyright (C) 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "faraday"
require "stringio"
require "addressable/uri"
require "signet"
require "signet/errors"
require "signet/oauth_2"
require "jwt"
require "date"
require "time"
module Signet
module OAuth2
class Client
OOB_MODES = ["urn:ietf:wg:oauth:2.0:oob:auto", "urn:ietf:wg:oauth:2.0:oob", "oob"].freeze
##
# Creates an OAuth 2.0 client.
#
# @param [Hash] options
# The configuration parameters for the client.
# - :authorization_uri
-
# The authorization server's HTTP endpoint capable of
# authenticating the end-user and obtaining authorization.
# - :token_credential_uri
-
# The authorization server's HTTP endpoint capable of issuing
# tokens and refreshing expired tokens.
# - :client_id
-
# A unique identifier issued to the client to identify itself to the
# authorization server.
# - :client_secret
-
# A shared symmetric secret issued by the authorization server,
# which is used to authenticate the client.
# - :scope
-
# The scope of the access request, expressed either as an Array
# or as a space-delimited String.
# - :target_audience
-
# The final target audience for ID tokens fetched by this client,
# as a String.
# - :state
-
# An arbitrary string designed to allow the client to maintain state.
# - :code
-
# The authorization code received from the authorization server.
# - :redirect_uri
-
# The redirection URI used in the initial request.
# - :username
-
# The resource owner's username.
# - :password
-
# The resource owner's password.
# - :issuer
-
# Issuer ID when using assertion profile
# - :person
-
# Target user for assertions
# - :expiry
-
# Number of seconds assertions are valid for
# - :signing_key
-
# Signing key when using assertion profile
# - :refresh_token
-
# The refresh token associated with the access token
# to be refreshed.
# - :access_token
-
# The current access token for this client.
# - :id_token
-
# The current ID token for this client.
# - :extension_parameters
-
# When using an extension grant type, this the set of parameters used
# by that extension.
#
# @example
# client = Signet::OAuth2::Client.new(
# :authorization_uri =>
# 'https://example.server.com/authorization',
# :token_credential_uri =>
# 'https://example.server.com/token',
# :client_id => 'anonymous',
# :client_secret => 'anonymous',
# :scope => 'example',
# :redirect_uri => 'https://example.client.com/oauth'
# )
#
# @see Signet::OAuth2::Client#update!
def initialize options = {}
@authorization_uri = nil
@token_credential_uri = nil
@client_id = nil
@client_secret = nil
@code = nil
@expires_at = nil
@issued_at = nil
@issuer = nil
@password = nil
@principal = nil
@redirect_uri = nil
@scope = nil
@target_audience = nil
@state = nil
@username = nil
@access_type = nil
update! options
end
##
# Updates an OAuth 2.0 client.
#
# @param [Hash] options
# The configuration parameters for the client.
# - :authorization_uri
-
# The authorization server's HTTP endpoint capable of
# authenticating the end-user and obtaining authorization.
# - :token_credential_uri
-
# The authorization server's HTTP endpoint capable of issuing
# tokens and refreshing expired tokens.
# - :client_id
-
# A unique identifier issued to the client to identify itself to the
# authorization server.
# - :client_secret
-
# A shared symmetric secret issued by the authorization server,
# which is used to authenticate the client.
# - :scope
-
# The scope of the access request, expressed either as an Array
# or as a space-delimited String.
# - :target_audience
-
# The final target audience for ID tokens fetched by this client,
# as a String.
# - :state
-
# An arbitrary string designed to allow the client to maintain state.
# - :code
-
# The authorization code received from the authorization server.
# - :redirect_uri
-
# The redirection URI used in the initial request.
# - :username
-
# The resource owner's username.
# - :password
-
# The resource owner's password.
# - :issuer
-
# Issuer ID when using assertion profile
# - :audience
-
# Target audience for assertions
# - :person
-
# Target user for assertions
# - :expiry
-
# Number of seconds assertions are valid for
# - :signing_key
-
# Signing key when using assertion profile
# - :refresh_token
-
# The refresh token associated with the access token
# to be refreshed.
# - :access_token
-
# The current access token for this client.
# - :access_type
-
# The current access type parameter for #authorization_uri.
# - :id_token
-
# The current ID token for this client.
# - :extension_parameters
-
# When using an extension grant type, this is the set of parameters used
# by that extension.
#
# @example
# client.update!(
# :code => 'i1WsRn1uB1',
# :access_token => 'FJQbwq9',
# :expires_in => 3600
# )
#
# @see Signet::OAuth2::Client#initialize
# @see Signet::OAuth2::Client#update_token!
def update! options = {}
# Normalize all keys to symbols to allow indifferent access.
options = deep_hash_normalize options
self.authorization_uri = options[:authorization_uri] if options.key? :authorization_uri
self.token_credential_uri = options[:token_credential_uri] if options.key? :token_credential_uri
self.client_id = options[:client_id] if options.key? :client_id
self.client_secret = options[:client_secret] if options.key? :client_secret
self.scope = options[:scope] if options.key? :scope
self.target_audience = options[:target_audience] if options.key? :target_audience
self.state = options[:state] if options.key? :state
self.code = options[:code] if options.key? :code
self.redirect_uri = options[:redirect_uri] if options.key? :redirect_uri
self.username = options[:username] if options.key? :username
self.password = options[:password] if options.key? :password
self.issuer = options[:issuer] if options.key? :issuer
self.person = options[:person] if options.key? :person
self.sub = options[:sub] if options.key? :sub
self.expiry = options[:expiry] || 60
self.audience = options[:audience] if options.key? :audience
self.signing_key = options[:signing_key] if options.key? :signing_key
self.extension_parameters = options[:extension_parameters] || {}
self.additional_parameters = options[:additional_parameters] || {}
self.access_type = options.fetch :access_type, :offline
update_token! options
self
end
##
# Updates an OAuth 2.0 client.
#
# @param [Hash] options
# The configuration parameters related to the token.
# - :refresh_token
-
# The refresh token associated with the access token
# to be refreshed.
# - :access_token
-
# The current access token for this client.
# - :id_token
-
# The current ID token for this client.
# - :expires_in
-
# The time in seconds until access token expiration.
# - :expires_at
-
# The time as an integer number of seconds since the Epoch
# - :issued_at
-
# The timestamp that the token was issued at.
#
# @example
# client.update!(
# :refresh_token => 'n4E9O119d',
# :access_token => 'FJQbwq9',
# :expires_in => 3600
# )
#
# @see Signet::OAuth2::Client#initialize
# @see Signet::OAuth2::Client#update!
def update_token! options = {}
# Normalize all keys to symbols to allow indifferent access internally
options = deep_hash_normalize options
self.expires_in = options[:expires] if options.key? :expires
self.expires_in = options[:expires_in] if options.key? :expires_in
self.expires_at = options[:expires_at] if options.key? :expires_at
# By default, the token is issued at `Time.now` when `expires_in` is
# set, but this can be used to supply a more precise time.
self.issued_at = options[:issued_at] if options.key? :issued_at
# Special case where we want expires_at to be relative to issued_at
if options.key?(:issued_at) && options.key?(:expires_in)
set_relative_expires_at options[:issued_at], options[:expires_in]
end
self.access_token = options[:access_token] if options.key? :access_token
self.refresh_token = options[:refresh_token] if options.key? :refresh_token
self.id_token = options[:id_token] if options.key? :id_token
self
end
##
# Returns the authorization URI that the user should be redirected to.
#
# @return [Addressable::URI] The authorization URI.
#
# @see Signet::OAuth2.generate_authorization_uri
def authorization_uri options = {}
# Normalize external input
options = deep_hash_normalize options
return nil if @authorization_uri.nil?
options[:response_type] = :code unless options[:response_type]
options[:access_type] = access_type if !options[:access_type] && access_type
options[:client_id] ||= client_id
options[:redirect_uri] ||= redirect_uri
if options[:prompt] && options[:approval_prompt]
raise ArgumentError, "prompt and approval_prompt are mutually exclusive parameters"
end
raise ArgumentError, "Missing required client identifier." unless options[:client_id]
raise ArgumentError, "Missing required redirect URI." unless options[:redirect_uri]
options[:scope] = scope.join " " if !options[:scope] && scope
options[:state] = state unless options[:state]
options.merge!(additional_parameters.merge(options[:additional_parameters] || {}))
options.delete :additional_parameters
options = options.transform_keys(&:to_s)
uri = Addressable::URI.parse(
::Signet::OAuth2.generate_authorization_uri(
@authorization_uri, options
)
)
if uri.normalized_scheme != "https"
raise Signet::UnsafeOperationError,
"Authorization endpoint must be protected by TLS."
end
uri
end
##
# Sets the authorization URI for this client.
#
# @param [Addressable::URI, Hash, String, #to_str] new_authorization_uri
# The authorization URI.
def authorization_uri= new_authorization_uri
@authorization_uri = coerce_uri new_authorization_uri
end
##
# Returns the token credential URI for this client.
#
# @return [Addressable::URI] The token credential URI.
def token_credential_uri
@token_credential_uri
end
##
# Sets the token credential URI for this client.
#
# @param [Addressable::URI, Hash, String, #to_str] new_token_credential_uri
# The token credential URI.
def token_credential_uri= new_token_credential_uri
@token_credential_uri = coerce_uri new_token_credential_uri
end
# Addressable expects URIs formatted as hashes to come in with symbols as keys.
# Returns nil implicitly for the nil case.
def coerce_uri incoming_uri
if incoming_uri.is_a? Hash
Addressable::URI.new deep_hash_normalize(incoming_uri)
elsif incoming_uri
Addressable::URI.parse incoming_uri
end
end
##
# Returns the current access type parameter for #authorization_uri.
#
# @return [String, Symbol] The current access type.
def access_type
@access_type
end
##
# Sets the current access type parameter for #authorization_uri.
#
# @param [String, Symbol] new_access_type
# The current access type.
def access_type= new_access_type
@access_type = new_access_type
end
##
# Returns the client identifier for this client.
#
# @return [String] The client identifier.
def client_id
@client_id
end
##
# Sets the client identifier for this client.
#
# @param [String] new_client_id
# The client identifier.
def client_id= new_client_id
@client_id = new_client_id
end
##
# Returns the client secret for this client.
#
# @return [String] The client secret.
def client_secret
@client_secret
end
##
# Sets the client secret for this client.
#
# @param [String] new_client_secret
# The client secret.
def client_secret= new_client_secret
@client_secret = new_client_secret
end
##
# Returns the scope for this client. Scope is a list of access ranges
# defined by the authorization server.
#
# @return [Array] The scope of access the client is requesting.
def scope
@scope
end
##
# Sets the scope for this client.
#
# @param [Array, String] new_scope
# The scope of access the client is requesting. This may be
# expressed as either an Array of String objects or as a
# space-delimited String.
def scope= new_scope
case new_scope
when Array
new_scope.each do |scope|
if scope.include? " "
raise ArgumentError,
"Individual scopes cannot contain the space character."
end
end
@scope = new_scope
when String
@scope = new_scope.split
when nil
@scope = nil
else
raise TypeError, "Expected Array or String, got #{new_scope.class}"
end
end
##
# Returns the final target audience for ID tokens fetched by this client.
#
# @return [String] The target audience.
def target_audience
@target_audience
end
##
# Sets the final target audience for ID tokens fetched by this client.
#
# @param [String] new_target_audience The new target audience.
def target_audience= new_target_audience
@target_audience = new_target_audience
end
##
# Returns the client's current state value.
#
# @return [String] The state value.
def state
@state
end
##
# Sets the client's current state value.
#
# @param [String] new_state
# The state value.
def state= new_state
@state = new_state
end
##
# Returns the authorization code issued to this client.
# Used only by the authorization code access grant type.
#
# @return [String] The authorization code.
def code
@code
end
##
# Sets the authorization code issued to this client.
# Used only by the authorization code access grant type.
#
# @param [String] new_code
# The authorization code.
def code= new_code
@code = new_code
end
##
# Returns the redirect URI for this client.
#
# @return [String] The redirect URI.
def redirect_uri
@redirect_uri
end
##
# Sets the redirect URI for this client.
#
# @param [String] new_redirect_uri
# The redirect URI.
def redirect_uri= new_redirect_uri
new_redirect_uri = Addressable::URI.parse new_redirect_uri
# TODO: - Better solution to allow google postmessage flow. For now, make an exception to the spec.
unless new_redirect_uri.nil? || new_redirect_uri.absolute? || uri_is_postmessage?(new_redirect_uri) ||
uri_is_oob?(new_redirect_uri)
raise ArgumentError, "Redirect URI must be an absolute URI."
end
@redirect_uri = new_redirect_uri
end
##
# Returns the username associated with this client.
# Used only by the resource owner password credential access grant type.
#
# @return [String] The username.
def username
@username
end
##
# Sets the username associated with this client.
# Used only by the resource owner password credential access grant type.
#
# @param [String] new_username
# The username.
def username= new_username
@username = new_username
end
##
# Returns the password associated with this client.
# Used only by the resource owner password credential access grant type.
#
# @return [String] The password.
def password
@password
end
##
# Sets the password associated with this client.
# Used only by the resource owner password credential access grant type.
#
# @param [String] new_password
# The password.
def password= new_password
@password = new_password
end
##
# Returns the issuer ID associated with this client.
# Used only by the assertion grant type.
#
# @return [String] Issuer id.
def issuer
@issuer
end
##
# Sets the issuer ID associated with this client.
# Used only by the assertion grant type.
#
# @param [String] new_issuer
# Issuer ID (typical in email adddress form).
def issuer= new_issuer
@issuer = new_issuer
end
##
# Returns the target audience ID when issuing assertions.
# Used only by the assertion grant type.
#
# @return [String] Target audience ID.
def audience
@audience
end
##
# Sets the target audience ID when issuing assertions.
# Used only by the assertion grant type.
#
# @param [String] new_audience
# Target audience ID
def audience= new_audience
@audience = new_audience
end
##
# Returns the target resource owner for impersonation.
# Used only by the assertion grant type.
#
# @return [String] Target user for impersonation.
def principal
@principal
end
##
# Sets the target resource owner for impersonation.
# Used only by the assertion grant type.
#
# @param [String] new_person
# Target user for impersonation
def principal= new_person
@principal = new_person
end
alias person principal
alias person= principal=
##
# The target "sub" when issuing assertions.
# Used in some Admin SDK APIs.
#
attr_accessor :sub
##
# Returns the number of seconds assertions are valid for
# Used only by the assertion grant type.
#
# @return [Integer] Assertion expiry, in seconds
def expiry
@expiry
end
##
# Sets the number of seconds assertions are valid for
# Used only by the assertion grant type.
#
# @param [Integer, String] new_expiry
# Assertion expiry, in seconds
def expiry= new_expiry
@expiry = new_expiry ? new_expiry.to_i : nil
end
##
# Returns the signing key associated with this client.
# Used only by the assertion grant type.
#
# @return [String,OpenSSL::PKey] Signing key
def signing_key
@signing_key
end
##
# Sets the signing key when issuing assertions.
# Used only by the assertion grant type.
#
# @param [String, OpenSSL::Pkey] new_key
# Signing key. Either private key for RSA or string for HMAC algorithm
def signing_key= new_key
@signing_key = new_key
end
##
# Algorithm used for signing JWTs
# @return [String] Signing algorithm
def signing_algorithm
signing_key.is_a?(String) ? "HS256" : "RS256"
end
##
# Returns the set of extension parameters used by the client.
# Used only by extension access grant types.
#
# @return [Hash] The extension parameters.
def extension_parameters
@extension_parameters ||= {}
end
##
# Sets extension parameters used by the client.
# Used only by extension access grant types.
#
# @param [Hash] new_extension_parameters
# The parameters.
def extension_parameters= new_extension_parameters
if new_extension_parameters.respond_to? :to_hash
@extension_parameters = new_extension_parameters.to_hash
else
raise TypeError,
"Expected Hash, got #{new_extension_parameters.class}."
end
end
##
# Returns the set of additional (non standard) parameters to be used by the client.
#
# @return [Hash] The pass through parameters.
def additional_parameters
@additional_parameters ||= {}
end
##
# Sets additional (non standard) parameters to be used by the client.
#
# @param [Hash] new_additional_parameters
# The parameters.
def additional_parameters= new_additional_parameters
if new_additional_parameters.respond_to? :to_hash
@additional_parameters = new_additional_parameters.to_hash
else
raise TypeError,
"Expected Hash, got #{new_additional_parameters.class}."
end
end
##
# Returns the refresh token associated with this client.
#
# @return [String] The refresh token.
def refresh_token
@refresh_token ||= nil
end
##
# Sets the refresh token associated with this client.
#
# @param [String] new_refresh_token
# The refresh token.
def refresh_token= new_refresh_token
@refresh_token = new_refresh_token
end
##
# Returns the access token associated with this client.
#
# @return [String] The access token.
def access_token
@access_token ||= nil
end
##
# Sets the access token associated with this client.
#
# @param [String] new_access_token
# The access token.
def access_token= new_access_token
@access_token = new_access_token
end
##
# Returns the ID token associated with this client.
#
# @return [String] The ID token.
def id_token
@id_token ||= nil
end
##
# Sets the ID token associated with this client.
#
# @param [String] new_id_token
# The ID token.
def id_token= new_id_token
@id_token = new_id_token
end
##
# Returns the decoded ID token associated with this client.
#
# @param [OpenSSL::PKey::RSA, Object] public_key
# The public key to use to verify the ID token. Skips verification if
# omitted.
#
# @return [String] The decoded ID token.
def decoded_id_token public_key = nil, options = {}, &keyfinder
options[:algorithm] ||= signing_algorithm
verify = !public_key.nil? || block_given?
payload, _header = JWT.decode(id_token, public_key, verify, options, &keyfinder)
raise Signet::UnsafeOperationError, "No ID token audience declared." unless payload.key? "aud"
unless Array(payload["aud"]).include?(client_id)
raise Signet::UnsafeOperationError,
"ID token audience did not match Client ID."
end
payload
end
##
# Returns the lifetime of the access token in seconds.
# Returns nil if the token does not expire.
#
# @return [Integer, nil] The access token lifetime.
def expires_in
if @expires_at.nil? || @issued_at.nil?
nil
else
(@expires_at - @issued_at).to_i
end
end
##
# Sets the lifetime of the access token in seconds. Resets the issued_at
# timestamp. Nil values will be treated as though the token does
# not expire.
#
# @param [String, Integer, nil] new_expires_in
# The access token lifetime.
def expires_in= new_expires_in
if new_expires_in.nil?
@expires_at = nil
@issued_at = nil
else
@issued_at = Time.now
@expires_at = @issued_at + new_expires_in.to_i
end
end
##
# Returns the timestamp the access token was issued at.
#
# @return [Time, nil] The access token issuance time.
def issued_at
@issued_at
end
##
# Sets the timestamp the access token was issued at.
#
# @param [String,Integer,Time] new_issued_at
# The access token issuance time.
def issued_at= new_issued_at
@issued_at = normalize_timestamp new_issued_at
end
##
# Returns the timestamp the access token will expire at.
# Returns nil if the token does not expire.
#
# @return [Time, nil] The access token lifetime.
def expires_at
@expires_at
end
##
# Limits the lifetime of the access token as number of seconds since
# the Epoch. Nil values will be treated as though the token does
# not expire.
# @param [String,Integer,Time, nil] new_expires_at
# The access token expiration time.
def expires_at= new_expires_at
@expires_at = normalize_timestamp new_expires_at
end
##
# Returns true if the access token has expired.
# Returns false if the token has not expired or has an nil @expires_at.
#
# @return [TrueClass, FalseClass]
# The expiration state of the access token.
def expired?
!expires_at.nil? && Time.now >= expires_at
end
##
# Returns true if the access token has expired or expires within
# the next n seconds. Returns false for tokens with a nil @expires_at.
#
# @param [Integer] sec
# Max number of seconds from now where a token is still considered
# expired.
# @return [TrueClass, FalseClass]
# The expiration state of the access token.
def expires_within? sec
!expires_at.nil? && Time.now >= (expires_at - sec)
end
##
# Removes all credentials from the client.
def clear_credentials!
@access_token = nil
@refresh_token = nil
@id_token = nil
@username = nil
@password = nil
@code = nil
@issued_at = nil
@expires_at = nil
end
##
# Returns the inferred grant type, based on the current state of the
# client object. Returns `"none"` if the client has insufficient
# information to make an in-band authorization request.
#
# @return [String]
# The inferred grant type.
def grant_type
@grant_type ||= nil
return @grant_type if @grant_type
if code && redirect_uri
"authorization_code"
elsif refresh_token
"refresh_token"
elsif username && password
"password"
elsif issuer && signing_key
"urn:ietf:params:oauth:grant-type:jwt-bearer"
end
end
def grant_type= new_grant_type
@grant_type =
case new_grant_type
when "authorization_code", "refresh_token", "password", "client_credentials"
new_grant_type
else
Addressable::URI.parse new_grant_type
end
end
def to_jwt options = {}
options = deep_hash_normalize options
now = Time.new
skew = options[:skew] || 60
assertion = {
"iss" => issuer,
"aud" => audience,
"exp" => (now + expiry).to_i,
"iat" => (now - skew).to_i
}
assertion["scope"] = scope.join " " unless scope.nil?
assertion["target_audience"] = target_audience unless target_audience.nil?
assertion["prn"] = person unless person.nil?
assertion["sub"] = sub unless sub.nil?
JWT.encode assertion, signing_key, signing_algorithm
end
##
# Serialize the client object to JSON.
#
# @note A serialized client contains sensitive information. Persist or transmit with care.
#
# @return [String] A serialized JSON representation of the client.
def to_json *_args
MultiJson.dump(
"authorization_uri" => authorization_uri ? authorization_uri.to_s : nil,
"token_credential_uri" => token_credential_uri ? token_credential_uri.to_s : nil,
"client_id" => client_id,
"client_secret" => client_secret,
"scope" => scope,
"target_audience" => target_audience,
"state" => state,
"code" => code,
"redirect_uri" => redirect_uri ? redirect_uri.to_s : nil,
"username" => username,
"password" => password,
"issuer" => issuer,
"audience" => audience,
"person" => person,
"expiry" => expiry,
"expires_at" => expires_at ? expires_at.to_i : nil,
"signing_key" => signing_key,
"refresh_token" => refresh_token,
"access_token" => access_token,
"id_token" => id_token,
"extension_parameters" => extension_parameters
)
end
##
# Generates a request for token credentials.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :code
-
# The authorization code.
#
# @private
# @return [Array] The request object.
def generate_access_token_request options = {}
options = deep_hash_normalize options
parameters = { "grant_type" => grant_type }
case grant_type
when "authorization_code"
parameters["code"] = code
parameters["redirect_uri"] = redirect_uri
when "password"
parameters["username"] = username
parameters["password"] = password
when "refresh_token"
parameters["refresh_token"] = refresh_token
when "urn:ietf:params:oauth:grant-type:jwt-bearer"
parameters["assertion"] = to_jwt options
else
if redirect_uri
# Grant type was intended to be `authorization_code` because of
# the presence of the redirect URI.
raise ArgumentError, "Missing authorization code."
end
parameters.merge! extension_parameters
end
parameters["client_id"] = client_id if !options[:use_basic_auth] && !client_id.nil?
parameters["client_secret"] = client_secret if !options[:use_basic_auth] && !client_secret.nil?
if options[:scope]
parameters["scope"] = options[:scope]
elsif options[:use_configured_scope] && !scope.nil?
parameters["scope"] = scope
end
additional = additional_parameters.merge(options[:additional_parameters] || {})
additional.each { |k, v| parameters[k.to_s] = v }
parameters
end
def fetch_access_token options = {}
raise ArgumentError, "Missing token endpoint URI." if token_credential_uri.nil?
options = deep_hash_normalize options
client = options[:connection] ||= Faraday.default_connection
url = Addressable::URI.parse token_credential_uri
parameters = generate_access_token_request options
if client.is_a? Faraday::Connection
if options[:use_basic_auth]
# The Basic Auth middleware usage differs before and after Faraday v2
if Gem::Version.new(Faraday::VERSION).segments.first >= 2
client.request :authorization, :basic, client_id, client_secret
else
client.request :basic_auth, client_id, client_secret
end
end
response = client.post url.normalize.to_s,
Addressable::URI.form_encode(parameters),
"Content-Type" => "application/x-www-form-urlencoded"
status = response.status.to_i
body = response.body
content_type = response.headers["Content-type"]
else
# Hurley
if options[:use_basic_auth]
url.user = client_id
url.password = client_secret
end
response = client.post url.normalize.to_s, parameters
status = response.status_code.to_i
body = response.body
content_type = response.header[:content_type]
end
return ::Signet::OAuth2.parse_credentials body, content_type if status == 200
message = " Server message:\n#{response.body.to_s.strip}" unless body.to_s.strip.empty?
if [400, 401, 403].include? status
message = "Authorization failed.#{message}"
raise ::Signet::AuthorizationError.new message, response: response
elsif status.to_s[0] == "5"
message = "Remote server error.#{message}"
raise ::Signet::RemoteServerError, message
else
message = "Unexpected status code: #{response.status}.#{message}"
raise ::Signet::UnexpectedStatusError, message
end
end
def fetch_access_token! options = {}
token_hash = fetch_access_token options
if token_hash
# No-op for grant types other than `authorization_code`.
# An authorization code is a one-time use token and is immediately
# revoked after usage.
self.code = nil
self.issued_at = Time.now
update_token! token_hash
end
token_hash
end
##
# Refresh the access token, if possible
def refresh! options = {}
fetch_access_token! options
end
##
# Generates an authenticated request for protected resources.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :request
-
# A pre-constructed request. An OAuth 2 Authorization header
# will be added to it, as well as an explicit Cache-Control
# `no-store` directive.
# - :method
-
# The HTTP method for the request. Defaults to 'GET'.
# - :uri
-
# The URI for the request.
# - :headers
-
# The HTTP headers for the request.
# - :body
-
# The HTTP body for the request.
# - :realm
-
# The Authorization realm. See RFC 2617.
# @return [Faraday::Request] The request object.
def generate_authenticated_request options = {}
options = deep_hash_normalize options
raise ArgumentError, "Missing access token." if access_token.nil?
options = {
realm: nil
}.merge(options)
if options[:request].is_a? Faraday::Request
request = options[:request]
else
if options[:request].is_a? Array
method, uri, headers, body = options[:request]
else
method = options[:method] || :get
uri = options[:uri]
headers = options[:headers] || []
body = options[:body] || ""
end
headers = headers.to_a if headers.is_a? Hash
request_components = {
method: method,
uri: uri,
headers: headers,
body: body
}
# Verify that we have all pieces required to return an HTTP request
request_components.each do |(key, value)|
raise ArgumentError, "Missing :#{key} parameter." unless value
end
method = method.to_s.downcase.to_sym
request = options[:connection].build_request method.to_s.downcase.to_sym do |req|
req.url Addressable::URI.parse(uri).normalize.to_s
req.headers = Faraday::Utils::Headers.new headers
req.body = body
end
end
request["Authorization"] = ::Signet::OAuth2.generate_bearer_authorization_header(
access_token,
options[:realm] ? [["realm", options[:realm]]] : nil
)
request["Cache-Control"] = "no-store"
request
end
##
# Transmits a request for a protected resource.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :request
-
# A pre-constructed request. An OAuth 2 Authorization header
# will be added to it, as well as an explicit Cache-Control
# `no-store` directive.
# - :method
-
# The HTTP method for the request. Defaults to 'GET'.
# - :uri
-
# The URI for the request.
# - :headers
-
# The HTTP headers for the request.
# - :body
-
# The HTTP body for the request.
# - :realm
-
# The Authorization realm. See RFC 2617.
# - :connection
-
# The HTTP connection to use.
# Must be of type Faraday::Connection
.
#
# @example
# # Using Net::HTTP
# response = client.fetch_protected_resource(
# :uri => 'http://www.example.com/protected/resource'
# )
#
# @return [Array] The response object.
def fetch_protected_resource options = {}
options = deep_hash_normalize options
options[:connection] ||= Faraday.default_connection
request = generate_authenticated_request options
request_env = request.to_env options[:connection]
request_env[:request] ||= request
response = options[:connection].app.call request_env
return response unless response.status.to_i == 401
# When accessing a protected resource, we only want to raise an
# error for 401 responses.
message = "Authorization failed."
message += " Server message:\n#{response.body.to_s.strip}" unless response.body.to_s.strip.empty?
raise ::Signet::AuthorizationError.new(
message, request: request, response: response
)
end
private
##
# Check if URI is Google's postmessage flow (not a valid redirect_uri by spec, but allowed)
# @private
def uri_is_postmessage? uri
uri.to_s.casecmp("postmessage").zero?
end
##
# Check if the URI is a out-of-band
# @private
def uri_is_oob? uri
OOB_MODES.include? uri.to_s
end
# Convert all keys in this hash (nested) to symbols for uniform retrieval
def recursive_hash_normalize_keys val
if val.is_a? Hash
deep_hash_normalize val
else
val
end
end
def deep_hash_normalize old_hash
sym_hash = {}
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
sym_hash
end
def normalize_timestamp time
case time
when NilClass
nil
when Time
time
when DateTime
time.to_time
when String
Time.parse time
when Integer
Time.at time
else
raise "Invalid time value #{time}"
end
end
def set_relative_expires_at issued_at, expires_in
self.issued_at = issued_at
# Using local expires_in because if self.expires_in is used, it returns
# the time left before the token expires
self.expires_at = self.issued_at + expires_in.to_i
end
end
end
end
signet-0.17.0/lib/signet/errors.rb 0000644 0000041 0000041 00000005612 14256367611 017037 0 ustar www-data www-data # Copyright (C) 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "addressable/uri"
module Signet
##
# An error indicating that the client has aborted an operation that
# would have been unsafe to perform.
class UnsafeOperationError < StandardError
end
##
# An error indicating the client failed to parse a value.
class ParseError < StandardError
end
##
# An error indicating that the server considers the Authorization header to
# be malformed(missing/unsupported/invalid parameters), and the request
# should be considered invalid.
class MalformedAuthorizationError < StandardError
end
##
# An error indicating that the server failed at processing the request
# due to a internal error
class RemoteServerError < StandardError
end
##
# An error indicating that the server sent an unexpected http status
class UnexpectedStatusError < StandardError
end
##
# An error indicating the remote server refused to authorize the client.
class AuthorizationError < StandardError
##
# Creates a new authentication error.
#
# @param [String] message
# A message describing the error.
# @param [Hash] options
# The configuration parameters for the request.
# - :request
-
# A Faraday::Request object. Optional.
# - :response
-
# A Faraday::Response object. Optional.
# - :code
-
# An error code.
# - :description
-
# Human-readable text intended to be used to assist in resolving the
# error condition.
# - :uri
-
# A URI identifying a human-readable web page with additional
# information about the error, indended for the resource owner.
def initialize message, options = {}
super message
@options = options
@request = options[:request]
@response = options[:response]
@code = options[:code]
@description = options[:description]
@uri = Addressable::URI.parse options[:uri]
end
##
# The HTTP request that triggered this authentication error.
#
# @return [Array] A tuple of method, uri, headers, and body.
attr_reader :request
##
# The HTTP response that triggered this authentication error.
#
# @return [Array] A tuple of status, headers, and body.
attr_reader :response
end
end
signet-0.17.0/lib/signet/oauth_1.rb 0000644 0000041 0000041 00000041537 14256367611 017071 0 ustar www-data www-data require "addressable/uri"
require "signet"
require "securerandom"
module Signet # :nodoc:
module OAuth1
OUT_OF_BAND = "oob".freeze
##
# Converts a value to a percent-encoded String
according to
# the rules given in RFC 5849. All non-unreserved characters are
# percent-encoded.
#
# @param [Symbol, #to_str] value The value to be encoded.
#
# @return [String] The percent-encoded value.
def self.encode value
value = value.to_s if value.is_a? Symbol
Addressable::URI.encode_component(
value,
Addressable::URI::CharacterClasses::UNRESERVED
)
end
##
# Converts a percent-encoded String to an unencoded value.
#
# @param [#to_str] value
# The percent-encoded String
to be unencoded.
#
# @return [String] The unencoded value.
def self.unencode value
Addressable::URI.unencode_component value
end
##
# Returns a timestamp suitable for use as an 'oauth_timestamp'
# value.
#
# @return [String] The current timestamp.
def self.generate_timestamp
Time.now.to_i.to_s
end
##
# Returns a nonce suitable for use as an 'oauth_nonce'
# value.
#
# @return [String] A random nonce.
def self.generate_nonce
SecureRandom.random_bytes(16).unpack("H*").join
end
##
# Processes an options Hash
to find a credential key value.
# Allows for greater flexibility in configuration.
#
# @param [Symbol] credential_type
# One of :client
, :temporary
,
# :token
, :consumer
, :request
,
# or :access
.
#
# @return [String] The credential key value.
def self.extract_credential_key_option credential_type, options
# Normalize key to String to allow indifferent access.
options = options.to_h.transform_keys(&:to_s)
credential_key = "#{credential_type}_credential_key"
credential = "#{credential_type}_credential"
if options[credential_key]
credential_key = options[credential_key]
elsif options[credential]
require "signet/oauth_1/credential"
unless options[credential].respond_to? :key
raise TypeError,
"Expected Signet::OAuth1::Credential, " \
"got #{options[credential].class}."
end
credential_key = options[credential].key
elsif options["client"]
require "signet/oauth_1/client"
unless options["client"].is_a? ::Signet::OAuth1::Client
raise TypeError,
"Expected Signet::OAuth1::Client, got #{options['client'].class}."
end
credential_key = options["client"].send credential_key
else
credential_key = nil
end
if !credential_key.nil? && !credential_key.is_a?(String)
raise TypeError,
"Expected String, got #{credential_key.class}."
end
credential_key
end
##
# Processes an options Hash
to find a credential secret value.
# Allows for greater flexibility in configuration.
#
# @param [Symbol] credential_type
# One of :client
, :temporary
,
# :token
, :consumer
, :request
,
# or :access
.
#
# @return [String] The credential secret value.
def self.extract_credential_secret_option credential_type, options
# Normalize key to String to allow indifferent access.
options = options.to_h.transform_keys(&:to_s)
credential_secret = "#{credential_type}_credential_secret"
credential = "#{credential_type}_credential"
if options[credential_secret]
credential_secret = options[credential_secret]
elsif options[credential]
require "signet/oauth_1/credential"
unless options[credential].respond_to? :secret
raise TypeError,
"Expected Signet::OAuth1::Credential, " \
"got #{options[credential].class}."
end
credential_secret = options[credential].secret
elsif options["client"]
require "signet/oauth_1/client"
unless options["client"].is_a? ::Signet::OAuth1::Client
raise TypeError,
"Expected Signet::OAuth1::Client, got #{options['client'].class}."
end
credential_secret = options["client"].send credential_secret
else
credential_secret = nil
end
if !credential_secret.nil? && !credential_secret.is_a?(String)
raise TypeError,
"Expected String, got #{credential_secret.class}."
end
credential_secret
end
##
# Normalizes a set of OAuth parameters according to the algorithm given
# in RFC 5849. Sorts key/value pairs lexically by byte order, first by
# key, then by value, joins key/value pairs with the '=' character, then
# joins the entire parameter list with '&' characters.
#
# @param [Enumerable] parameters The OAuth parameter list.
#
# @return [String] The normalized parameter list.
def self.normalize_parameters parameters
raise TypeError, "Expected Enumerable, got #{parameters.class}." unless parameters.is_a? Enumerable
parameter_list = parameters.map do |k, v|
next if k == "oauth_signature"
# This is probably the wrong place to try to exclude the realm
"#{encode k}=#{encode v}"
end
parameter_list.compact.sort.join "&"
end
##
# Generates a signature base string according to the algorithm given in
# RFC 5849. Joins the method, URI, and normalized parameter string with
# '&' characters.
#
# @param [String] method The HTTP method.
# @param [Addressable::URI, String, #to_str] uri The URI.
# @param [Enumerable] parameters The OAuth parameter list.
#
# @return [String] The signature base string.
def self.generate_base_string method, uri, parameters
raise TypeError, "Expected Enumerable, got #{parameters.class}." unless parameters.is_a? Enumerable
method = method.to_s.upcase
parsed_uri = Addressable::URI.parse uri
uri = Addressable::URI.new(
scheme: parsed_uri.normalized_scheme,
authority: parsed_uri.normalized_authority,
path: parsed_uri.path,
query: parsed_uri.query,
fragment: parsed_uri.fragment
)
uri_parameters = uri.query_values(Array) || []
uri = uri.omit(:query, :fragment).to_s
merged_parameters =
uri_parameters.concat(parameters.map { |k, v| [k, v] })
parameter_string = normalize_parameters merged_parameters
[
encode(method),
encode(uri),
encode(parameter_string)
].join("&")
end
##
# Generates an Authorization
header from a parameter list
# according to the rules given in RFC 5849.
#
# @param [Enumerable] parameters The OAuth parameter list.
# @param [String] realm
# The Authorization
realm. See RFC 2617.
#
# @return [String] The Authorization
header.
def self.generate_authorization_header parameters, realm = nil
if !parameters.is_a?(Enumerable) || parameters.is_a?(String)
raise TypeError, "Expected Enumerable, got #{parameters.class}."
end
parameter_list = parameters.map do |k, v|
if k == "realm"
raise ArgumentError,
'The "realm" parameter must be specified as a separate argument.'
end
"#{encode k}=\"#{encode v}\""
end
if realm
realm = realm.gsub '"', '\"'
parameter_list.unshift "realm=\"#{realm}\""
end
"OAuth #{parameter_list.join ', '}"
end
##
# Parses an Authorization
header into its component
# parameters. Parameter keys and values are decoded according to the
# rules given in RFC 5849.
def self.parse_authorization_header field_value
raise TypeError, "Expected String, got #{field_value.class}." unless field_value.is_a? String
auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
case auth_scheme
when /^OAuth$/i
# Other token types may be supported eventually
pairs = Signet.parse_auth_param_list(field_value[/^OAuth\s+(.*)$/i, 1])
(pairs.each_with_object [] do |(k, v), accu|
if k != "realm"
k = unencode k
v = unencode v
end
accu << [k, v]
end)
else
raise ParseError,
"Parsing non-OAuth Authorization headers is out of scope."
end
end
##
# Parses an application/x-www-form-urlencoded
HTTP response
# body into an OAuth key/secret pair.
#
# @param [String] body The response body.
#
# @return [Signet::OAuth1::Credential] The OAuth credentials.
def self.parse_form_encoded_credentials body
raise TypeError, "Expected String, got #{body.class}." unless body.is_a? String
Signet::OAuth1::Credential.new(
Addressable::URI.form_unencode(body)
)
end
##
# Generates an OAuth signature using the signature method indicated in the
# parameter list. Unsupported signature methods will result in a
# NotImplementedError
exception being raised.
#
# @param [String] method The HTTP method.
# @param [Addressable::URI, String, #to_str] uri The URI.
# @param [Enumerable] parameters The OAuth parameter list.
# @param [String] client_credential_secret The client credential secret.
# @param [String] token_credential_secret
# The token credential secret. Omitted when unavailable.
#
# @return [String] The signature.
def self.sign_parameters method, uri, parameters,
client_credential_secret, token_credential_secret = nil
# Technically, the token_credential_secret parameter here may actually
# be a temporary credential secret when obtaining a token credential
# for the first time
base_string = generate_base_string method, uri, parameters
parameters = parameters.to_h.transform_keys(&:to_s)
signature_method = parameters["oauth_signature_method"]
case signature_method
when "HMAC-SHA1"
require "signet/oauth_1/signature_methods/hmac_sha1"
Signet::OAuth1::HMACSHA1.generate_signature base_string, client_credential_secret, token_credential_secret
when "RSA-SHA1"
require "signet/oauth_1/signature_methods/rsa_sha1"
Signet::OAuth1::RSASHA1.generate_signature base_string, client_credential_secret, token_credential_secret
when "PLAINTEXT"
require "signet/oauth_1/signature_methods/plaintext"
Signet::OAuth1::PLAINTEXT.generate_signature base_string, client_credential_secret, token_credential_secret
else
raise NotImplementedError,
"Unsupported signature method: #{signature_method}"
end
end
##
# Generates an OAuth parameter list to be used when obtaining a set of
# temporary credentials.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :client_credential_key
-
# The client credential key.
# - :callback
-
# The OAuth callback. Defaults to {Signet::OAuth1::OUT_OF_BAND}.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :additional_parameters
-
# Non-standard additional parameters.
#
# @return [Array]
# The parameter list as an Array
of key/value pairs.
def self.unsigned_temporary_credential_parameters options = {}
options = {
callback: ::Signet::OAuth1::OUT_OF_BAND,
signature_method: "HMAC-SHA1",
additional_parameters: []
}.merge(options)
client_credential_key =
extract_credential_key_option :client, options
raise ArgumentError, "Missing :client_credential_key parameter." if client_credential_key.nil?
parameters = [
["oauth_consumer_key", client_credential_key],
["oauth_signature_method", options[:signature_method]],
["oauth_timestamp", generate_timestamp],
["oauth_nonce", generate_nonce],
["oauth_version", "1.0"],
["oauth_callback", options[:callback]]
]
# Works for any Enumerable
options[:additional_parameters].each do |key, value|
parameters << [key, value]
end
parameters
end
##
# Appends the optional 'oauth_token' and 'oauth_callback' parameters to
# the base authorization URI.
#
# @param [Addressable::URI, String, #to_str] authorization_uri
# The base authorization URI.
#
# @return [String] The authorization URI to redirect the user to.
def self.generate_authorization_uri authorization_uri, options = {}
options = {
callback: nil,
additional_parameters: {}
}.merge(options)
temporary_credential_key =
extract_credential_key_option :temporary, options
parsed_uri = Addressable::URI.parse(authorization_uri).dup
query_values = parsed_uri.query_values || {}
if options[:additional_parameters]
query_values = query_values.merge(
options[:additional_parameters].each_with_object({}) { |(k, v), h| h[k] = v; }
)
end
query_values["oauth_token"] = temporary_credential_key if temporary_credential_key
query_values["oauth_callback"] = options[:callback] if options[:callback]
parsed_uri.query_values = query_values
parsed_uri.normalize.to_s
end
##
# Generates an OAuth parameter list to be used when obtaining a set of
# token credentials.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :client_credential_key
-
# The client credential key.
# - :temporary_credential_key
-
# The temporary credential key.
# - :verifier
-
# The OAuth verifier.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
#
# @return [Array]
# The parameter list as an Array
of key/value pairs.
def self.unsigned_token_credential_parameters options = {}
options = {
signature_method: "HMAC-SHA1",
verifier: nil
}.merge(options)
client_credential_key =
extract_credential_key_option :client, options
temporary_credential_key =
extract_credential_key_option :temporary, options
raise ArgumentError, "Missing :client_credential_key parameter." if client_credential_key.nil?
raise ArgumentError, "Missing :temporary_credential_key parameter." if temporary_credential_key.nil?
raise ArgumentError, "Missing :verifier parameter." if options[:verifier].nil?
[
["oauth_consumer_key", client_credential_key],
["oauth_token", temporary_credential_key],
["oauth_signature_method", options[:signature_method]],
["oauth_timestamp", generate_timestamp],
["oauth_nonce", generate_nonce],
["oauth_verifier", options[:verifier]],
["oauth_version", "1.0"]
]
end
##
# Generates an OAuth parameter list to be used when requesting a
# protected resource.
#
# @param [Hash] options
# The configuration parameters for the request.
# - :client_credential_key
-
# The client credential key.
# - :token_credential_key
-
# The token credential key.
# - :signature_method
-
# The signature method. Defaults to 'HMAC-SHA1'
.
# - :two_legged
-
# A switch for two-legged OAuth. Defaults to false
.
#
# @return [Array]
# The parameter list as an Array
of key/value pairs.
def self.unsigned_resource_parameters options = {}
options = {
signature_method: "HMAC-SHA1",
two_legged: false
}.merge(options)
client_credential_key =
extract_credential_key_option :client, options
raise ArgumentError, "Missing :client_credential_key parameter." if client_credential_key.nil?
unless options[:two_legged]
token_credential_key =
extract_credential_key_option :token, options
raise ArgumentError, "Missing :token_credential_key parameter." if token_credential_key.nil?
end
parameters = [
["oauth_consumer_key", client_credential_key],
["oauth_signature_method", options[:signature_method]],
["oauth_timestamp", generate_timestamp],
["oauth_nonce", generate_nonce],
["oauth_version", "1.0"]
]
parameters << ["oauth_token", token_credential_key] unless options[:two_legged]
# No additional parameters allowed here
parameters
end
end
end
signet-0.17.0/.yardopts 0000644 0000041 0000041 00000000215 14256367611 014777 0 ustar www-data www-data --no-private
--title=Signet
--markup markdown
--markup-provider redcarpet
./lib/**/*.rb
-
README.md
CHANGELOG.md
CODE_OF_CONDUCT.md
LICENSE
signet-0.17.0/SECURITY.md 0000644 0000041 0000041 00000000511 14256367611 014721 0 ustar www-data www-data # Security Policy
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.