signet-0.17.0/0000755000004100000410000000000014256367611013133 5ustar www-datawww-datasignet-0.17.0/README.md0000644000004100000410000000350314256367611014413 0ustar www-datawww-data# Signet
Homepage
https://github.com/googleapis/signet/
Author
Bob Aman
Copyright
Copyright © 2010 Google, Inc.
License
Apache 2.0
[![Gem Version](https://badge.fury.io/rb/signet.svg)](https://badge.fury.io/rb/signet) ## Description Signet is an OAuth 1.0 / OAuth 2.0 implementation. ## Reference - {Signet::OAuth1} - {Signet::OAuth1::Client} - {Signet::OAuth1::Credential} - {Signet::OAuth1::Server} - {Signet::OAuth2} - {Signet::OAuth2::Client} ## Example Usage for Google # Initialize the client ``` ruby require 'signet/oauth_2/client' client = Signet::OAuth2::Client.new( :authorization_uri => 'https://accounts.google.com/o/oauth2/auth', :token_credential_uri => 'https://oauth2.googleapis.com/token', :client_id => "#{YOUR_CLIENT_ID}.apps.googleusercontent.com", :client_secret => YOUR_CLIENT_SECRET, :scope => 'email profile', :redirect_uri => 'https://example.client.com/oauth' ) ``` # Request an authorization code ``` redirect_to(client.authorization_uri) ``` # Obtain an access token ``` client.code = request.query['code'] client.fetch_access_token! ``` ## Install `gem install signet` Be sure `https://rubygems.org` is in your gem sources. ## Supported Ruby Versions This library is supported on Ruby 2.5+. Google provides official support for Ruby versions that are actively supported by Ruby Core—that is, Ruby versions that are either in normal maintenance or in security maintenance, and not end of life. Currently, this means Ruby 2.5 and later. Older versions of Ruby _may_ still work, but are unsupported and not recommended. See https://www.ruby-lang.org/en/downloads/branches/ for details about the Ruby support schedule. signet-0.17.0/CHANGELOG.md0000644000004100000410000001244214256367611014747 0ustar www-datawww-data# Release History ### 0.17.0 (2022-06-23) * Updated minimum Ruby version to 2.6 ### 0.16.1 (2022-02-24) #### Bug Fixes * Support Faraday 2 ### 0.16.0 (2021-09-03) #### Features * Support for fetching an access token with basic auth #### Bug Fixes * Remove extraneous files from the gem * Require addressable 2.8 to remediate vulnerability ### 0.15.0 (2021-03-04) * Drop support for Ruby 2.4 and add support for Ruby 3.0 ### 0.14.1 / 2021-01-27 * Fix OAuth1 signature with duplicate query param names ### 0.14.0 / 2020-03-31 * Support for fetching ID tokens from google oauth2 endpoint. ### 0.13.2 / 2020-03-25 Rerelease of 0.13.1. ### 0.13.1 / 2020-03-24 * Update github url ### 0.13.0 / 2020-02-24 * Support Faraday 1.x ### 0.12.0 / 2019-10-08 * This version now requires Ruby 2.4. * Support array values of the "aud" field. * Normalize the version constant to match related gems. ### 0.11.0 / 2018-10-08 * Add constant time comparison for oauth signatures. ### 0.10.0 / 2018-09-21 * Add UnexpectedStatusError class for http status errors that are not handled. ### 0.9.2 / 2018-09-12 * Update issued_at correctly when it is set simultaneously with expires_in. ### 0.9.1 / 2018-08-29 * Warn on EOL ruby versions. * Fix DateTime normalization. ### 0.9.0 / 2018-08-20 * Add RemoteServerError class for 5xx level errors. * Allow to_json to be called with arguments * Expires_in now sets and reflects current expires_at value * Expires_within(0) now returns false when expires_at is nil. ### 0.8.1 / 2017-10-13 * Restore support for Ruby 1.9.3 ### 0.8.0 / 2017-10-12 * Ensure the "expires_at" attribute is recalculated on refresh (chutzimir) * Fix warnings on Ruby 2.4 (koic) * Allow DateTime objects to be passed into attributes (foxtacles) * Provide signature verification algorithm for compatibility with ruby-jwt 2.0 (jurriaan) * Signet::OAuth2::Client#decoded_id_token can take a keyfinder block (mvastola) ### 0.7.3 / 2016-06-20 * Fix timestamp parsing on 32-bit systems * Fix expiration check when issue/expiry times are nil ### 0.7.2 / 2015-12-21 * Don't assume Faraday form encoding middleware is present ### 0.7.1 / 2015-12-17 * Fix an issue with date parsing ### 0.7 / 2015-12-06 * No longer overwrite SSL environment variables. * Tighten up date & URL (de)serialization for OAuth2 client * Allow Hurley as a connection * Allow scope as an option in `fetch_access_token!` to request downscoped access tokens * Add expires_within(sec) method to oauth2 client to facilitate proactive refreshes ### 0.6.1 / 2015-06-08 * Fix language warnings for unused & shadowed variables ((@blowmage)[]) * Update SSL cert path for OSX ((@gambaroff)[]) * Update JWT library and fix broken tests * Fix incorrect parameter name in OAuth2 client docs ((@samuelreh)[]) * Fix symbolization of URL parameter keys ((@swifthand)[]) ### 0.6.0 / 2014-12-05 * Drop support for ruby versions < 1.9.3 * Update gem dependencies and lock down versions tighter * Allow form encoded responses when exchanging OAuth 2 authorization codes * Normalize options keys for indifferent access ### 0.5.1 / 2014-06-08 * Allow Hash objects to be used to initialize authorization URI * Added PLAINTEXT and RSA-SHA1 signature methods to OAuth 1 support * Added client object serialization * The `approval_prompt` option no longer defaults to `:force` * The `approval_prompt` and `prompt` are now mutually exclusive. ### 0.5.0 / 2013-05-31 * Switched to faraday 0.9.0 * Added `expires_at` option ### 0.4.5 * Minor documentation fixes * Allow postmessage as a valid redirect_uri in OAuth 2 ### 0.4.4 * Add support for assertion profile ### 0.4.3 * Added method to clear credentials ### 0.4.2 * Backwards compatibility for MultiJson ### 0.4.1 * Updated Launchy dependency ### 0.4.0 * Added OAuth 1 server implementation * Updated Faraday dependency ### 0.3.4 * Attempts to auto-detect CA cert location ### 0.3.3 * Request objects no longer recreated during processing * Faraday middleware now supported * Streamed requests now supported * Fixed assertion profiles; client ID/secret omission no longer an error ### 0.3.2 * Added audience security check for ID tokens ### 0.3.1 * Fixed a warning while determining grant type * Removed requirement that a connection be supplied when authorizing requests * Updated addressable dependency to avoid minor bug * Fixed some documentation stuff around markdown formatting * Added support for Google Code wiki format output when generating docs ### 0.3.0 * Replaced httpadapter gem dependency with faraday * Replaced json gem dependency with multi_json * Updated to OAuth 2.0 draft 22 * Complete test coverage ### 0.2.4 * Updated to incorporate changes to the Google OAuth endpoints ### 0.2.3 * Added support for JWT-formatted ID tokens. * Added :issued_at option to #update_token! method. ### 0.2.2 * Lowered requirements for json gem ### 0.2.1 * Updated to keep in sync with the new httpadapter changes ### 0.2.0 * Added support for OAuth 2.0 draft 10 ### 0.1.4 * Added support for a two-legged authorization flow ### 0.1.3 * Fixed issue with headers passed in as a Hash * Fixed incompatibilities with Ruby 1.8.6 ### 0.1.2 * Fixed bug with overzealous normalization ### 0.1.1 * Fixed bug with missing StringIO require * Fixed issue with dependency on unreleased features of addressable ### 0.1.0 * Initial release signet-0.17.0/signet.gemspec0000644000004100000410000000657414256367611016005 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: signet 0.17.0 ruby lib Gem::Specification.new do |s| s.name = "signet".freeze s.version = "0.17.0" s.required_rubygems_version = Gem::Requirement.new(">= 1.3.5".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/googleapis/signet/issues", "changelog_uri" => "https://github.com/googleapis/signet/blob/main/CHANGELOG.md", "source_code_uri" => "https://github.com/googleapis/signet" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Bob Aman".freeze, "Steven Bazyl".freeze] s.date = "2022-06-23" s.description = "Signet is an OAuth 1.0 / OAuth 2.0 implementation.\n".freeze s.email = "sbazyl@google.com".freeze s.extra_rdoc_files = ["README.md".freeze] s.files = [".yardopts".freeze, "CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "LICENSE".freeze, "README.md".freeze, "SECURITY.md".freeze, "lib/signet.rb".freeze, "lib/signet/errors.rb".freeze, "lib/signet/oauth_1.rb".freeze, "lib/signet/oauth_1/client.rb".freeze, "lib/signet/oauth_1/credential.rb".freeze, "lib/signet/oauth_1/server.rb".freeze, "lib/signet/oauth_1/signature_methods/hmac_sha1.rb".freeze, "lib/signet/oauth_1/signature_methods/plaintext.rb".freeze, "lib/signet/oauth_1/signature_methods/rsa_sha1.rb".freeze, "lib/signet/oauth_2.rb".freeze, "lib/signet/oauth_2/client.rb".freeze, "lib/signet/version.rb".freeze] s.homepage = "https://github.com/googleapis/signet".freeze s.licenses = ["Apache-2.0".freeze] s.rdoc_options = ["--main".freeze, "README.md".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.6".freeze) s.rubygems_version = "3.2.5".freeze s.summary = "Signet is an OAuth 1.0 / OAuth 2.0 implementation.".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_runtime_dependency(%q.freeze, ["~> 2.8"]) s.add_runtime_dependency(%q.freeze, [">= 0.17.5", "< 3.a"]) s.add_development_dependency(%q.freeze, ["~> 1.26.0"]) s.add_runtime_dependency(%q.freeze, [">= 1.5", "< 3.0"]) s.add_development_dependency(%q.freeze, ["~> 1.5"]) s.add_development_dependency(%q.freeze, ["~> 2.4"]) s.add_runtime_dependency(%q.freeze, ["~> 1.10"]) s.add_development_dependency(%q.freeze, ["~> 13.0"]) s.add_development_dependency(%q.freeze, ["~> 3.0"]) s.add_development_dependency(%q.freeze, ["~> 3.1"]) s.add_development_dependency(%q.freeze, ["~> 0.9", ">= 0.9.12"]) else s.add_dependency(%q.freeze, ["~> 2.8"]) s.add_dependency(%q.freeze, [">= 0.17.5", "< 3.a"]) s.add_dependency(%q.freeze, ["~> 1.26.0"]) s.add_dependency(%q.freeze, [">= 1.5", "< 3.0"]) s.add_dependency(%q.freeze, ["~> 1.5"]) s.add_dependency(%q.freeze, ["~> 2.4"]) s.add_dependency(%q.freeze, ["~> 1.10"]) s.add_dependency(%q.freeze, ["~> 13.0"]) s.add_dependency(%q.freeze, ["~> 3.0"]) s.add_dependency(%q.freeze, ["~> 3.1"]) s.add_dependency(%q.freeze, ["~> 0.9", ">= 0.9.12"]) end end signet-0.17.0/CODE_OF_CONDUCT.md0000644000004100000410000000367514256367611015745 0ustar www-datawww-data# Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) signet-0.17.0/LICENSE0000644000004100000410000002514314256367611014145 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. signet-0.17.0/lib/0000755000004100000410000000000014256367611013701 5ustar www-datawww-datasignet-0.17.0/lib/signet.rb0000644000004100000410000000546214256367611015526 0ustar www-datawww-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 "signet/version" module Signet # :nodoc: def self.parse_auth_param_list auth_param_string # Production rules from: # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-12 token = /[-!#{$OUTPUT_RECORD_SEPARATOR}%&'*+.^_`|~0-9a-zA-Z]+/ d_qdtext = /[\s\x21\x23-\x5B\x5D-\x7E\x80-\xFF]/n d_quoted_pair = /\\[\s\x21-\x7E\x80-\xFF]/n d_qs = /"(?:#{d_qdtext}|#{d_quoted_pair})*"/ # Production rules that allow for more liberal parsing, i.e. single quotes s_qdtext = /[\s\x21-\x26\x28-\x5B\x5D-\x7E\x80-\xFF]/n s_quoted_pair = /\\[\s\x21-\x7E\x80-\xFF]/n s_qs = /'(?:#{s_qdtext}|#{s_quoted_pair})*'/ # Combine the above production rules to find valid auth-param pairs. auth_param = /((?:#{token})\s*=\s*(?:#{d_qs}|#{s_qs}|#{token}))/ auth_param_pairs = [] last_match = nil remainder = auth_param_string # Iterate over the string, consuming pair matches as we go. Verify that # pre-matches and post-matches contain only allowable characters. # # This would be way easier in Ruby 1.9, but we want backwards # compatibility. while (match = remainder.match auth_param) if match.pre_match && match.pre_match !~ /^[\s,]*$/ raise ParseError, "Unexpected auth param format: '#{auth_param_string}'." end auth_param_pairs << match.captures[0] # Appending pair remainder = match.post_match last_match = match end if last_match.post_match && last_match.post_match !~ /^[\s,]*$/ raise ParseError, "Unexpected auth param format: '#{auth_param_string}'." end # Now parse the auth-param pair strings & turn them into key-value pairs. (auth_param_pairs.each_with_object [] do |pair, accu| name, value = pair.split "=", 2 case value when /^".*"$/ value = value.gsub(/^"(.*)"$/, '\1').gsub(/\\(.)/, '\1') when /^'.*'$/ value = value.gsub(/^'(.*)'$/, '\1').gsub(/\\(.)/, '\1') when %r{[()<>@,;:\\"/\[\]?={}]} # Certain special characters are not allowed raise ParseError, "Unexpected characters in auth param " \ "list: '#{auth_param_string}'." end accu << [name, value] end) end end signet-0.17.0/lib/signet/0000755000004100000410000000000014256367611015172 5ustar www-datawww-datasignet-0.17.0/lib/signet/oauth_1/0000755000004100000410000000000014256367611016532 5ustar www-datawww-datasignet-0.17.0/lib/signet/oauth_1/signature_methods/0000755000004100000410000000000014256367611022256 5ustar www-datawww-datasignet-0.17.0/lib/signet/oauth_1/signature_methods/rsa_sha1.rb0000644000004100000410000000107614256367611024310 0ustar www-datawww-datarequire "digest/sha1" require "base64" require "openssl" require "signet" module Signet # :nodoc: module OAuth1 module RSASHA1 def self.generate_signature \ base_string, client_credential_secret, _token_credential_secret private_key = OpenSSL::PKey::RSA.new client_credential_secret signature = private_key.sign OpenSSL::Digest.new("SHA1"), base_string # using strict_encode64 because the encode64 method adds newline characters after ever 60 chars Base64.strict_encode64(signature).strip end end end end signet-0.17.0/lib/signet/oauth_1/signature_methods/hmac_sha1.rb0000644000004100000410000000160414256367611024430 0ustar www-datawww-datarequire "openssl" require "signet" module Signet # :nodoc: module OAuth1 module HMACSHA1 def self.generate_signature \ base_string, client_credential_secret, token_credential_secret # Both the client secret and token secret must be escaped client_credential_secret = Signet::OAuth1.encode client_credential_secret token_credential_secret = Signet::OAuth1.encode token_credential_secret # The key for the signature is just the client secret and token # secret joined by the '&' character. If the token secret is omitted, # the '&' must still be present. key = [client_credential_secret, token_credential_secret].join "&" Base64.encode64(OpenSSL::HMAC.digest( OpenSSL::Digest.new("sha1"), key, base_string )).strip end end end end signet-0.17.0/lib/signet/oauth_1/signature_methods/plaintext.rb0000644000004100000410000000140614256367611024614 0ustar www-datawww-datarequire "signet" module Signet # :nodoc: module OAuth1 module PLAINTEXT def self.generate_signature \ _base_string, client_credential_secret, token_credential_secret # Both the client secret and token secret must be escaped client_credential_secret = Signet::OAuth1.encode client_credential_secret token_credential_secret = Signet::OAuth1.encode token_credential_secret # The key for the signature is just the client secret and token # secret joined by the '&' character. If the token secret is omitted, # the '&' must still be present. key = [client_credential_secret, token_credential_secret].join "&" Signet::OAuth1.encode(key).strip end end end end signet-0.17.0/lib/signet/oauth_1/client.rb0000644000004100000410000011323214256367611020337 0ustar www-datawww-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 'faraday/utils' require "stringio" require "addressable/uri" require "signet" require "signet/errors" require "signet/oauth_1" require "signet/oauth_1/credential" module Signet module OAuth1 class Client ## # Creates 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 = 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.rb0000644000004100000410000004777414256367611020410 0ustar www-datawww-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.rb0000644000004100000410000000761114256367611021176 0ustar www-datawww-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.rb0000644000004100000410000001207314256367611017063 0ustar www-datawww-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.rb0000644000004100000410000000121514256367611017203 0ustar www-datawww-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/0000755000004100000410000000000014256367611016533 5ustar www-datawww-datasignet-0.17.0/lib/signet/oauth_2/client.rb0000644000004100000410000012262514256367611020346 0ustar www-datawww-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.rb0000644000004100000410000000561214256367611017037 0ustar www-datawww-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.rb0000644000004100000410000004153714256367611017071 0ustar www-datawww-datarequire "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/.yardopts0000644000004100000410000000021514256367611014777 0ustar www-datawww-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.md0000644000004100000410000000051114256367611014721 0ustar www-datawww-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.