googleauth-1.16.1/0000755000004100000410000000000015135672542014000 5ustar www-datawww-datagoogleauth-1.16.1/SECURITY.md0000644000004100000410000000051115135672542015566 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. googleauth-1.16.1/lib/0000755000004100000410000000000015135672542014546 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/0000755000004100000410000000000015135672542016704 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/external_account/0000755000004100000410000000000015135672542022242 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/external_account/external_account_utils.rb0000644000004100000410000001100715135672542027344 0ustar www-datawww-data# Copyright 2023 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 "time" require "googleauth/base_client" require "googleauth/errors" require "googleauth/helpers/connection" require "googleauth/oauth2/sts_client" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth module ExternalAccount # Authenticates requests using External Account credentials, such # as those provided by the AWS provider or OIDC provider like Azure, etc. module ExternalAccountUtils # Cloud resource manager URL used to retrieve project information. CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze ## # Retrieves the project ID corresponding to the workload identity or workforce pool. # For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project. # When not determinable, None is returned. # # The resource may not have permission (resourcemanager.projects.get) to # call this API or the required scopes may not be selected: # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes # # @return [String, nil] The project ID corresponding to the workload identity # pool or workforce pool if determinable # def project_id return @project_id unless @project_id.nil? project_number = self.project_number || @workforce_pool_user_project # if we missing either project number or scope, we won't retrieve project_id return nil if project_number.nil? || @scope.nil? url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}" response = connection.get url do |req| req.headers["Authorization"] = "Bearer #{@access_token}" req.headers["Content-Type"] = "application/json" end if response.status == 200 response_data = MultiJson.load response.body, symbolize_names: true @project_id = response_data[:projectId] end @project_id end ## # Retrieve the project number corresponding to workload identity pool # STS audience pattern: # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...` # # @return [String, nil] The project number extracted from the audience string, # or nil if it cannot be determined # def project_number segments = @audience.split "/" idx = segments.index "projects" return nil if idx.nil? || idx + 1 == segments.size segments[idx + 1] end # Normalizes a timestamp value to a Time object # # @param time [Time, String, nil] The timestamp to normalize # @return [Time, nil] The normalized timestamp or nil if input is nil # @raise [Google::Auth::CredentialsError] If the time value is not nil, Time, or String def normalize_timestamp time case time when NilClass nil when Time time when String Time.parse time else raise CredentialsError, "Invalid time value #{time}" end end # Extracts the service account email from the impersonation URL # # @return [String, nil] The service account email extracted from the # service_account_impersonation_url, or nil if it cannot be determined def service_account_email return nil if @service_account_impersonation_url.nil? start_idx = @service_account_impersonation_url.rindex "/" end_idx = @service_account_impersonation_url.index ":generateAccessToken" if start_idx != -1 && end_idx != -1 && start_idx < end_idx start_idx += 1 return @service_account_impersonation_url[start_idx..end_idx] end nil end end end end end googleauth-1.16.1/lib/googleauth/external_account/base_credentials.rb0000644000004100000410000002132215135672542026056 0ustar www-datawww-data# Copyright 2023 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 "time" require "googleauth/base_client" require "googleauth/errors" require "googleauth/helpers/connection" require "googleauth/oauth2/sts_client" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth module ExternalAccount # Authenticates requests using External Account credentials, such # as those provided by the AWS provider or OIDC provider like Azure, etc. module BaseCredentials # Contains all methods needed for all external account credentials. # Other credentials should call `base_setup` during initialization # And should define the :retrieve_subject_token! method # External account JSON type identifier. EXTERNAL_ACCOUNT_JSON_TYPE = "external_account".freeze # The token exchange grant_type used for exchanging credentials. STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze # The token exchange requested_token_type. This is always an access_token. STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze # Default IAM_SCOPE IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze include Google::Auth::BaseClient include Helpers::Connection attr_reader :expires_at attr_accessor :access_token attr_accessor :universe_domain def expires_within? seconds # This method is needed for BaseClient @expires_at && @expires_at - Time.now.utc < seconds end def expires_at= new_expires_at @expires_at = normalize_timestamp new_expires_at end def fetch_access_token! _options = {} # This method is needed for BaseClient response = exchange_token if @service_account_impersonation_url impersonated_response = get_impersonated_access_token response["access_token"] self.expires_at = impersonated_response["expireTime"] self.access_token = impersonated_response["accessToken"] else # Extract the expiration time in seconds from the response and calculate the actual expiration time # and then save that to the expiry variable. self.expires_at = Time.now.utc + response["expires_in"].to_i self.access_token = response["access_token"] end notify_refresh_listeners end # Retrieves the subject token using the credential_source object. # @return [string] # The retrieved subject token. # def retrieve_subject_token! raise NoMethodError, "retrieve_subject_token! not implemented" end # Returns whether the credentials represent a workforce pool (True) or # workload (False) based on the credentials' audience. # # @return [bool] # true if the credentials represent a workforce pool. # false if they represent a workload. def is_workforce_pool? %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "") end # For external account credentials, the principal is # represented by the audience, such as a workforce pool # @private # @return [String] the GCP principal, e.g. a workforce pool def principal @audience end private def token_type # This method is needed for BaseClient :access_token end # A common method for Other credentials to call during initialization # @raise [Google::Auth::InitializationError] If workforce_pool_user_project is incorrectly set def base_setup options self.default_connection = options[:connection] @audience = options[:audience] @scope = options[:scope] || IAM_SCOPE @subject_token_type = options[:subject_token_type] @token_url = options[:token_url] @token_info_url = options[:token_info_url] @service_account_impersonation_url = options[:service_account_impersonation_url] @service_account_impersonation_options = options[:service_account_impersonation_options] || {} @client_id = options[:client_id] @client_secret = options[:client_secret] @quota_project_id = options[:quota_project_id] @project_id = nil @workforce_pool_user_project = options[:workforce_pool_user_project] @universe_domain = options[:universe_domain] || "googleapis.com" @expires_at = nil @access_token = nil @sts_client = Google::Auth::OAuth2::STSClient.new( token_exchange_endpoint: @token_url, connection: default_connection ) return unless @workforce_pool_user_project && !is_workforce_pool? raise InitializationError, "workforce_pool_user_project should not be set for non-workforce pool credentials." end # Exchange tokens at STS endpoint # @raise [Google::Auth::AuthorizationError] If the token exchange request fails def exchange_token additional_options = nil if @client_id.nil? && @workforce_pool_user_project additional_options = { userProject: @workforce_pool_user_project } end token_request = { audience: @audience, grant_type: STS_GRANT_TYPE, subject_token: retrieve_subject_token!, subject_token_type: @subject_token_type, scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope, requested_token_type: STS_REQUESTED_TOKEN_TYPE, additional_options: additional_options } log_token_request token_request @sts_client.exchange_token token_request rescue Google::Auth::AuthorizationError => e raise Google::Auth::AuthorizationError.with_details( e.message, credential_type_name: self.class.name, principal: principal ) end def log_token_request token_request logger&.info do Google::Logging::Message.from( message: "Requesting access token from #{token_request[:grant_type]}", "credentialsId" => object_id ) end logger&.debug do digest = Digest::SHA256.hexdigest token_request[:subject_token].to_s loggable_request = token_request.merge subject_token: "(sha256:#{digest})" Google::Logging::Message.from( message: "Request data", "request" => loggable_request, "credentialsId" => object_id ) end end # Exchanges a token for an impersonated service account access token # # @param [String] token The token to exchange # @param [Hash] _options Additional options (not used) # @return [Hash] The response containing the impersonated access token # @raise [Google::Auth::CredentialsError] If the impersonation request fails def get_impersonated_access_token token, _options = {} log_impersonated_token_request token response = connection.post @service_account_impersonation_url do |req| req.headers["Authorization"] = "Bearer #{token}" req.headers["Content-Type"] = "application/json" req.body = MultiJson.dump({ scope: @scope }) end if response.status != 200 raise CredentialsError.with_details( "Service account impersonation failed with status #{response.status}", credential_type_name: self.class.name, principal: principal ) end MultiJson.load response.body end def log_impersonated_token_request original_token logger&.info do digest = Digest::SHA256.hexdigest original_token Google::Logging::Message.from( message: "Requesting impersonated access token with original token (sha256:#{digest})", "credentialsId" => object_id ) end end end end end end googleauth-1.16.1/lib/googleauth/external_account/pluggable_credentials.rb0000644000004100000410000002000715135672542027105 0ustar www-datawww-data# Copyright 2023 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 "open3" require "time" require "googleauth/errors" require "googleauth/external_account/base_credentials" require "googleauth/external_account/external_account_utils" module Google # Module Auth provides classes that provide Google-specific authorization used to access Google APIs. module Auth module ExternalAccount # This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI # provider then exchanging the credentials for a short-lived Google Cloud access token. class PluggableAuthCredentials # constant for pluggable auth enablement in environment variable. ENABLE_PLUGGABLE_ENV = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES".freeze EXECUTABLE_SUPPORTED_MAX_VERSION = 1 EXECUTABLE_TIMEOUT_MILLIS_DEFAULT = 30 * 1000 EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000 EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000 ID_TOKEN_TYPE = ["urn:ietf:params:oauth:token-type:jwt", "urn:ietf:params:oauth:token-type:id_token"].freeze include Google::Auth::ExternalAccount::BaseCredentials include Google::Auth::ExternalAccount::ExternalAccountUtils extend CredentialsLoader # Will always be nil, but method still gets used. attr_reader :client_id # Initialize from options map. # # @param [Hash] options Configuration options # @option options [String] :audience Audience for the token # @option options [Hash] :credential_source Credential source configuration that contains executable # configuration # @raise [Google::Auth::InitializationError] If executable source, command is missing, or timeout is invalid def initialize options = {} base_setup options @audience = options[:audience] @credential_source = options[:credential_source] || {} @credential_source_executable = @credential_source[:executable] if @credential_source_executable.nil? raise InitializationError, "Missing excutable source. An 'executable' must be provided" end @credential_source_executable_command = @credential_source_executable[:command] if @credential_source_executable_command.nil? raise InitializationError, "Missing command field. Executable command must be provided." end @credential_source_executable_timeout_millis = @credential_source_executable[:timeout_millis] || EXECUTABLE_TIMEOUT_MILLIS_DEFAULT if @credential_source_executable_timeout_millis < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND || @credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND raise InitializationError, "Timeout must be between 5 and 120 seconds." end @credential_source_executable_output_file = @credential_source_executable[:output_file] end # Retrieves the subject token using the credential_source object. # # @return [String] The retrieved subject token # @raise [Google::Auth::CredentialsError] If executables are not allowed, if token retrieval fails, # or if the token is invalid def retrieve_subject_token! unless ENV[ENABLE_PLUGGABLE_ENV] == "1" raise CredentialsError, "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \ "to run." end # check output file first subject_token = load_subject_token_from_output_file return subject_token unless subject_token.nil? # environment variable injection env = inject_environment_variables output = subprocess_with_timeout env, @credential_source_executable_command, @credential_source_executable_timeout_millis response = MultiJson.load output, symbolize_keys: true parse_subject_token response end private def load_subject_token_from_output_file return nil if @credential_source_executable_output_file.nil? return nil unless File.exist? @credential_source_executable_output_file begin content = File.read @credential_source_executable_output_file, encoding: "utf-8" response = MultiJson.load content, symbolize_keys: true rescue StandardError return nil end begin subject_token = parse_subject_token response rescue StandardError => e return nil if e.message.match(/The token returned by the executable is expired/) raise CredentialsError, e.message end subject_token end def parse_subject_token response validate_response_schema response unless response[:success] if response[:code].nil? || response[:message].nil? raise CredentialsError, "Error code and message fields are required in the response." end raise CredentialsError, "Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}." end if response[:expiration_time] && response[:expiration_time] < Time.now.to_i raise CredentialsError, "The token returned by the executable is expired." end if response[:token_type].nil? raise CredentialsError, "The executable response is missing the token_type field." end return response[:id_token] if ID_TOKEN_TYPE.include? response[:token_type] return response[:saml_response] if response[:token_type] == "urn:ietf:params:oauth:token-type:saml2" raise CredentialsError, "Executable returned unsupported token type." end def validate_response_schema response raise CredentialsError, "The executable response is missing the version field." if response[:version].nil? if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION raise CredentialsError, "Executable returned unsupported version #{response[:version]}." end raise CredentialsError, "The executable response is missing the success field." if response[:success].nil? end def inject_environment_variables env = ENV.to_h env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = @audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = @subject_token_type env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0" # only non-interactive mode we support. unless @service_account_impersonation_url.nil? env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = service_account_email end unless @credential_source_executable_output_file.nil? env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = @credential_source_executable_output_file end env end def subprocess_with_timeout environment_vars, command, timeout_seconds Timeout.timeout timeout_seconds do output, error, status = Open3.capture3 environment_vars, command unless status.success? raise CredentialsError, "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}" end output end end end end end end googleauth-1.16.1/lib/googleauth/external_account/aws_credentials.rb0000644000004100000410000004416315135672542025746 0ustar www-datawww-data# Copyright 2023 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 "time" require "googleauth/errors" require "googleauth/external_account/base_credentials" require "googleauth/external_account/external_account_utils" module Google # Module Auth provides classes that provide Google-specific authorization used to access Google APIs. module Auth # Authenticates requests using External Account credentials, such as those provided by the AWS provider. module ExternalAccount # This module handles the retrieval of credentials from Google Cloud by utilizing the AWS EC2 metadata service and # then exchanging the credentials for a short-lived Google Cloud access token. class AwsCredentials # Constant for imdsv2 session token expiration in seconds IMDSV2_TOKEN_EXPIRATION_IN_SECONDS = 300 include Google::Auth::ExternalAccount::BaseCredentials include Google::Auth::ExternalAccount::ExternalAccountUtils extend CredentialsLoader # Will always be nil, but method still gets used. attr_reader :client_id def initialize options = {} base_setup options @audience = options[:audience] @credential_source = options[:credential_source] || {} @environment_id = @credential_source[:environment_id] @region_url = @credential_source[:region_url] @credential_verification_url = @credential_source[:url] @regional_cred_verification_url = @credential_source[:regional_cred_verification_url] @imdsv2_session_token_url = @credential_source[:imdsv2_session_token_url] # These will be lazily loaded when needed, or will raise an error if not provided @region = nil @request_signer = nil @imdsv2_session_token = nil @imdsv2_session_token_expiry = nil end # Retrieves the subject token using the credential_source object. # The subject token is a serialized [AWS GetCallerIdentity signed request]( # https://cloud.google.com/iam/docs/access-resources-aws#exchange-token). # # The logic is summarized as: # # Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION environment variable or from the AWS # metadata server availability-zone if not found in the environment variable. # # Check AWS credentials in environment variables. If not found, retrieve from the AWS metadata server # security-credentials endpoint. # # When retrieving AWS credentials from the metadata server security-credentials endpoint, the AWS role needs to # be determined by # calling the security-credentials endpoint without any argument. # Then the credentials can be retrieved via: security-credentials/role_name # # Generate the signed request to AWS STS GetCallerIdentity action. # # Inject x-goog-cloud-target-resource into header and serialize the signed request. # This will be the subject-token to pass to GCP STS. # # @return [string] The retrieved subject token. # def retrieve_subject_token! if @request_signer.nil? @region = region @request_signer = AwsRequestSigner.new @region end request = { method: "POST", url: @regional_cred_verification_url.sub("{region}", @region) } request_options = @request_signer.generate_signed_request fetch_security_credentials, request request_headers = request_options[:headers] request_headers["x-goog-cloud-target-resource"] = @audience aws_signed_request = { headers: [], method: request_options[:method], url: request_options[:url] } aws_signed_request[:headers] = request_headers.keys.sort.map do |key| { key: key, value: request_headers[key] } end uri_escape aws_signed_request.to_json end private # Retrieves an IMDSv2 session token or returns a cached token if valid # # @return [String] The IMDSv2 session token # @raise [Google::Auth::CredentialsError] If the token URL is missing or there's an error retrieving the token def imdsv2_session_token return @imdsv2_session_token unless imdsv2_session_token_invalid? if @imdsv2_session_token_url.nil? raise CredentialsError.with_details( "IMDSV2 token url must be provided", credential_type_name: self.class.name, principal: principal ) end begin response = connection.put @imdsv2_session_token_url do |req| req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s end raise Faraday::Error unless response.success? rescue Faraday::Error => e raise CredentialsError.with_details( "Fetching AWS IMDSV2 token error: #{e}", credential_type_name: self.class.name, principal: principal ) end @imdsv2_session_token = response.body @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS @imdsv2_session_token end def imdsv2_session_token_invalid? return true if @imdsv2_session_token.nil? @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now end # Makes a request to an AWS resource endpoint # # @param [String] url The AWS endpoint URL # @param [String] name Resource name for error messages # @param [Hash, nil] data Optional data to send in POST requests # @param [Hash] headers Optional request headers # @return [Faraday::Response] The successful response # @raise [Google::Auth::CredentialsError] If the request fails def get_aws_resource url, name, data: nil, headers: {} begin headers["x-aws-ec2-metadata-token"] = imdsv2_session_token response = if data headers["Content-Type"] = "application/json" connection.post url, data, headers else connection.get url, nil, headers end raise Faraday::Error unless response.success? response rescue Faraday::Error raise CredentialsError.with_details( "Failed to retrieve AWS #{name}.", credential_type_name: self.class.name, principal: principal ) end end def uri_escape string if string.nil? nil else CGI.escape(string.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~") end end # Retrieves the AWS security credentials required for signing AWS requests from either the AWS security # credentials environment variables or from the AWS metadata server. def fetch_security_credentials env_aws_access_key_id = ENV[CredentialsLoader::AWS_ACCESS_KEY_ID_VAR] env_aws_secret_access_key = ENV[CredentialsLoader::AWS_SECRET_ACCESS_KEY_VAR] # This is normally not available for permanent credentials. env_aws_session_token = ENV[CredentialsLoader::AWS_SESSION_TOKEN_VAR] if env_aws_access_key_id && env_aws_secret_access_key return { access_key_id: env_aws_access_key_id, secret_access_key: env_aws_secret_access_key, session_token: env_aws_session_token } end role_name = fetch_metadata_role_name credentials = fetch_metadata_security_credentials role_name { access_key_id: credentials["AccessKeyId"], secret_access_key: credentials["SecretAccessKey"], session_token: credentials["Token"] } end # Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server. # This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security # credentials needed to sign requests to AWS APIs. # # @return [String] The AWS role name # @raise [Google::Auth::CredentialsError] If the credential verification URL is not set or if the request fails def fetch_metadata_role_name unless @credential_verification_url raise CredentialsError.with_details( "Unable to determine the AWS metadata server security credentials endpoint", credential_type_name: self.class.name, principal: principal ) end get_aws_resource(@credential_verification_url, "IAM Role").body end # Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server. def fetch_metadata_security_credentials role_name response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials" MultiJson.load response.body end # Reads the name of the AWS region from the environment # # @return [String] The name of the AWS region # @raise [Google::Auth::CredentialsError] If the region is not set in the environment # and the region_url was not set in credentials source def region @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR] unless @region unless @region_url raise CredentialsError.with_details( "region_url or region must be set for external account credentials", credential_type_name: self.class.name, principal: principal ) end @region ||= get_aws_resource(@region_url, "region").body[0..-2] end @region end end # Implements an AWS request signer based on the AWS Signature Version 4 signing process. # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html class AwsRequestSigner # Instantiates an AWS request signer used to compute authenticated signed requests to AWS APIs based on the AWS # Signature Version 4 signing process. # # @param [string] region_name # The AWS region to use. def initialize region_name @region_name = region_name end # Generates an AWS signature version 4 signed request. # # Creates a signed request following the AWS Signature Version 4 process, which # provides secure authentication for AWS API calls. The process includes creating # canonical request strings, calculating signatures using the AWS credentials, and # building proper authorization headers. # # For detailed information on the signing process, see: # https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html # # @param [Hash] aws_credentials The AWS security credentials with the following keys: # @option aws_credentials [String] :access_key_id The AWS access key ID # @option aws_credentials [String] :secret_access_key The AWS secret access key # @option aws_credentials [String, nil] :session_token Optional AWS session token # @param [Hash] original_request The request to sign with the following keys: # @option original_request [String] :url The AWS service URL (must be HTTPS) # @option original_request [String] :method The HTTP method (GET, POST, etc.) # @option original_request [Hash, nil] :headers Optional request headers # @option original_request [String, nil] :data Optional request payload # # @return [Hash] The signed request with the following keys: # * :url - The original URL as a string # * :headers - A hash of headers with the authorization header added # * :method - The HTTP method # * :data - The request payload (if present) # # @raise [Google::Auth::CredentialsError] If the AWS service URL is invalid # def generate_signed_request aws_credentials, original_request uri = Addressable::URI.parse original_request[:url] unless uri.hostname && uri.scheme == "https" # NOTE: We use AwsCredentials name but can't access its principal since AwsRequestSigner # is a separate class and not a credential object with access to the audience raise CredentialsError.with_details( "Invalid AWS service URL", credential_type_name: AwsCredentials.name, principal: "aws" ) end service_name = uri.host.split(".").first datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ" date = datetime[0, 8] headers = aws_headers aws_credentials, original_request, datetime request_payload = original_request[:data] || "" content_sha256 = sha256_hexdigest request_payload canonical_req = canonical_request original_request[:method], uri, headers, content_sha256 sts = string_to_sign datetime, canonical_req, service_name # Authorization header requires everything else to be properly setup in order to be properly # calculated. headers["Authorization"] = build_authorization_header headers, sts, aws_credentials, service_name, date { url: uri.to_s, headers: headers, method: original_request[:method], data: (request_payload unless request_payload.empty?) }.compact end private def aws_headers aws_credentials, original_request, datetime uri = Addressable::URI.parse original_request[:url] temp_headers = original_request[:headers] || {} headers = {} temp_headers.each_key { |k| headers[k.to_s] = temp_headers[k] } headers["host"] = uri.host headers["x-amz-date"] = datetime headers["x-amz-security-token"] = aws_credentials[:session_token] if aws_credentials[:session_token] headers end def build_authorization_header headers, sts, aws_credentials, service_name, date [ "AWS4-HMAC-SHA256", "Credential=#{credential aws_credentials[:access_key_id], date, service_name},", "SignedHeaders=#{headers.keys.sort.join ';'},", "Signature=#{signature aws_credentials[:secret_access_key], date, sts, service_name}" ].join(" ") end def signature secret_access_key, date, string_to_sign, service k_date = hmac "AWS4#{secret_access_key}", date k_region = hmac k_date, @region_name k_service = hmac k_region, service k_credentials = hmac k_service, "aws4_request" hexhmac k_credentials, string_to_sign end def hmac key, value OpenSSL::HMAC.digest OpenSSL::Digest.new("sha256"), key, value end def hexhmac key, value OpenSSL::HMAC.hexdigest OpenSSL::Digest.new("sha256"), key, value end def credential access_key_id, date, service "#{access_key_id}/#{credential_scope date, service}" end def credential_scope date, service [ date, @region_name, service, "aws4_request" ].join("/") end def string_to_sign datetime, canonical_request, service [ "AWS4-HMAC-SHA256", datetime, credential_scope(datetime[0, 8], service), sha256_hexdigest(canonical_request) ].join("\n") end def host uri # Handles known and unknown URI schemes; default_port nil when unknown. if uri.default_port == uri.port uri.host else "#{uri.host}:#{uri.port}" end end def canonical_request http_method, uri, headers, content_sha256 headers = headers.sort_by(&:first) # transforms to a sorted array of [key, value] [ http_method, uri.path.empty? ? "/" : uri.path, build_canonical_querystring(uri.query || ""), headers.map { |k, v| "#{k}:#{v}\n" }.join, # Canonical headers headers.map(&:first).join(";"), # Signed headers content_sha256 ].join("\n") end def sha256_hexdigest string OpenSSL::Digest::SHA256.hexdigest string end # Generates the canonical query string given a raw query string. # Logic is based on # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html # Code is from the AWS SDK for Ruby # https://github.com/aws/aws-sdk-ruby/blob/0ac3d0a393ed216290bfb5f0383380376f6fb1f1/gems/aws-sigv4/lib/aws-sigv4/signer.rb#L532 def build_canonical_querystring query params = query.split "&" params = params.map { |p| p.include?("=") ? p : "#{p}=" } params.each.with_index.sort do |(a, a_offset), (b, b_offset)| a_name, a_value = a.split "=" b_name, b_value = b.split "=" if a_name == b_name if a_value == b_value a_offset <=> b_offset else a_value <=> b_value end else a_name <=> b_name end end.map(&:first).join("&") end end end end end googleauth-1.16.1/lib/googleauth/external_account/identity_pool_credentials.rb0000644000004100000410000001467215135672542030040 0ustar www-datawww-data# Copyright 2023 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 "time" require "googleauth/errors" require "googleauth/external_account/base_credentials" require "googleauth/external_account/external_account_utils" module Google # Module Auth provides classes that provide Google-specific authorization used to access Google APIs. module Auth module ExternalAccount # This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI # provider then exchanging the credentials for a short-lived Google Cloud access token. class IdentityPoolCredentials include Google::Auth::ExternalAccount::BaseCredentials include Google::Auth::ExternalAccount::ExternalAccountUtils extend CredentialsLoader # Will always be nil, but method still gets used. attr_reader :client_id # Initialize from options map. # # @param [Hash] options Configuration options # @option options [String] :audience The audience for the token # @option options [Hash{Symbol => Object}] :credential_source A hash containing either source file or url. # credential_source_format is either text or json to define how to parse the credential response. # @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing, # contains ambiguous sources, or is missing required fields # def initialize options = {} base_setup options @audience = options[:audience] @credential_source = options[:credential_source] || {} @credential_source_file = @credential_source[:file] @credential_source_url = @credential_source[:url] @credential_source_headers = @credential_source[:headers] || {} @credential_source_format = @credential_source[:format] || {} @credential_source_format_type = @credential_source_format[:type] || "text" validate_credential_source end # Implementation of BaseCredentials retrieve_subject_token! # # @return [String] The subject token # @raise [Google::Auth::CredentialsError] If the token can't be parsed from JSON or is missing def retrieve_subject_token! content, resource_name = token_data if @credential_source_format_type == "text" token = content else begin response_data = MultiJson.load content, symbolize_keys: true token = response_data[@credential_source_field_name.to_sym] rescue StandardError raise CredentialsError, "Unable to parse subject_token from JSON resource #{resource_name} " \ "using key #{@credential_source_field_name}" end end raise CredentialsError, "Missing subject_token in the credential_source file/response." unless token token end private # Validates input # # @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing, # contains ambiguous sources, or is missing required fields def validate_credential_source # `environment_id` is only supported in AWS or dedicated future external account credentials. unless @credential_source[:environment_id].nil? raise InitializationError, "Invalid Identity Pool credential_source field 'environment_id'" end unless ["json", "text"].include? @credential_source_format_type raise InitializationError, "Invalid credential_source format #{@credential_source_format_type}" end # for JSON types, get the required subject_token field name. @credential_source_field_name = @credential_source_format[:subject_token_field_name] if @credential_source_format_type == "json" && @credential_source_field_name.nil? raise InitializationError, "Missing subject_token_field_name for JSON credential_source format" end # check file or url must be fulfilled and mutually exclusiveness. if @credential_source_file && @credential_source_url raise InitializationError, "Ambiguous credential_source. 'file' is mutually exclusive with 'url'." end return unless (@credential_source_file || @credential_source_url).nil? raise InitializationError, "Missing credential_source. A 'file' or 'url' must be provided." end def token_data @credential_source_file.nil? ? url_data : file_data end # Reads data from a file source # # @return [Array(String, String)] The file content and file path # @raise [Google::Auth::CredentialsError] If the source file doesn't exist def file_data unless File.exist? @credential_source_file raise CredentialsError, "File #{@credential_source_file} was not found." end content = File.read @credential_source_file, encoding: "utf-8" [content, @credential_source_file] end # Fetches data from a URL source # # @return [Array(String, String)] The response body and URL # @raise [Google::Auth::CredentialsError] If there's an error retrieving data from the URL # or if the response is not successful def url_data begin response = connection.get @credential_source_url do |req| req.headers.merge! @credential_source_headers end rescue Faraday::Error => e raise CredentialsError, "Error retrieving from credential url: #{e}" end unless response.success? raise CredentialsError, "Unable to retrieve Identity Pool subject token #{response.body}" end [response.body, @credential_source_url] end end end end end googleauth-1.16.1/lib/googleauth/id_tokens.rb0000644000004100000410000002122615135672542021213 0ustar www-datawww-data# frozen_string_literal: true # Copyright 2020 Google LLC # # 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 "googleauth/id_tokens/errors" require "googleauth/id_tokens/key_sources" require "googleauth/id_tokens/verifier" module Google module Auth ## # ## Verifying Google ID tokens # # This module verifies ID tokens issued by Google. This can be used to # authenticate signed-in users using OpenID Connect. See # https://developers.google.com/identity/sign-in/web/backend-auth for more # information. # # ### Basic usage # # To verify an ID token issued by Google accounts: # # payload = Google::Auth::IDTokens.verify_oidc the_token, # aud: "my-app-client-id" # # If verification succeeds, you will receive the token's payload as a hash. # If verification fails, an exception (normally a subclass of # {Google::Auth::IDTokens::VerificationError}) will be raised. # # To verify an ID token issued by the Google identity-aware proxy (IAP): # # payload = Google::Auth::IDTokens.verify_iap the_token, # aud: "my-app-client-id" # # These methods will automatically download and cache the Google public # keys necessary to verify these tokens. They will also automatically # verify the issuer (`iss`) field for their respective types of ID tokens. # # ### Advanced usage # # If you want to provide your own public keys, either by pointing at a # custom URI or by providing the key data directly, use the Verifier class # and pass in a key source. # # To point to a custom URI that returns a JWK set: # # source = Google::Auth::IDTokens::JwkHttpKeySource.new "https://example.com/jwk" # verifier = Google::Auth::IDTokens::Verifier.new key_source: source # payload = verifier.verify the_token, aud: "my-app-client-id" # # To provide key data directly: # # jwk_data = { # keys: [ # { # alg: "ES256", # crv: "P-256", # kid: "LYyP2g", # kty: "EC", # use: "sig", # x: "SlXFFkJ3JxMsXyXNrqzE3ozl_0913PmNbccLLWfeQFU", # y: "GLSahrZfBErmMUcHP0MGaeVnJdBwquhrhQ8eP05NfCI" # } # ] # } # source = Google::Auth::IDTokens::StaticKeySource.from_jwk_set jwk_data # verifier = Google::Auth::IDTokens::Verifier key_source: source # payload = verifier.verify the_token, aud: "my-app-client-id" # module IDTokens ## # A list of issuers expected for Google OIDC-issued tokens. # # @return [Array] # OIDC_ISSUERS = ["accounts.google.com", "https://accounts.google.com"].freeze ## # A list of issuers expected for Google IAP-issued tokens. # # @return [Array] # IAP_ISSUERS = ["https://cloud.google.com/iap"].freeze ## # The URL for Google OAuth2 V3 public certs # # @return [String] # OAUTH2_V3_CERTS_URL = "https://www.googleapis.com/oauth2/v3/certs" ## # The URL for Google IAP public keys # # @return [String] # IAP_JWK_URL = "https://www.gstatic.com/iap/verify/public_key-jwk" class << self ## # The key source providing public keys that can be used to verify # ID tokens issued by Google OIDC. # # @return [Google::Auth::IDTokens::JwkHttpKeySource] # def oidc_key_source @oidc_key_source ||= JwkHttpKeySource.new OAUTH2_V3_CERTS_URL end ## # The key source providing public keys that can be used to verify # ID tokens issued by Google IAP. # # @return [Google::Auth::IDTokens::JwkHttpKeySource] # def iap_key_source @iap_key_source ||= JwkHttpKeySource.new IAP_JWK_URL end ## # Reset all convenience key sources. Used for testing. # @private # def forget_sources! @oidc_key_source = @iap_key_source = nil self end ## # A convenience method that verifies a token allegedly issued by Google # OIDC. # # @param token [String] The ID token to verify # @param aud [String,Array,nil] The expected audience. At least # one `aud` field in the token must match at least one of the # provided audiences, or the verification will fail with # {Google::Auth::IDToken::AudienceMismatchError}. If `nil` (the # default), no audience checking is performed. # @param azp [String,Array,nil] The expected authorized party # (azp). At least one `azp` field in the token must match at least # one of the provided values, or the verification will fail with # {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil` # (the default), no azp checking is performed. # @param iss [String,Array,nil] The expected issuer. At least # one `iss` field in the token must match at least one of the # provided issuers, or the verification will fail with # {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer # checking is performed. Default is to check against {OIDC_ISSUERS}. # # @return [Hash] The decoded token payload. # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed. # Additional data may be available in the error subclass and message. # def verify_oidc token, aud: nil, azp: nil, iss: OIDC_ISSUERS verifier = Verifier.new key_source: oidc_key_source, aud: aud, azp: azp, iss: iss verifier.verify token end ## # A convenience method that verifies a token allegedly issued by Google # IAP. # # @param token [String] The ID token to verify # @param aud [String,Array,nil] The expected audience. At least # one `aud` field in the token must match at least one of the # provided audiences, or the verification will fail with # {Google::Auth::IDToken::AudienceMismatchError}. If `nil` (the # default), no audience checking is performed. # @param azp [String,Array,nil] The expected authorized party # (azp). At least one `azp` field in the token must match at least # one of the provided values, or the verification will fail with # {Google::Auth::IDToken::AuthorizedPartyMismatchError}. If `nil` # (the default), no azp checking is performed. # @param iss [String,Array,nil] The expected issuer. At least # one `iss` field in the token must match at least one of the # provided issuers, or the verification will fail with # {Google::Auth::IDToken::IssuerMismatchError}. If `nil`, no issuer # checking is performed. Default is to check against {IAP_ISSUERS}. # # @return [Hash] The decoded token payload. # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed. # Additional data may be available in the error subclass and message. # def verify_iap token, aud: nil, azp: nil, iss: IAP_ISSUERS verifier = Verifier.new key_source: iap_key_source, aud: aud, azp: azp, iss: iss verifier.verify token end end end end end googleauth-1.16.1/lib/googleauth/iam.rb0000644000004100000410000000465015135672542020004 0ustar www-datawww-data# Copyright 2015 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 "googleauth/signet" require "googleauth/credentials_loader" require "multi_json" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # Authenticates requests using IAM credentials. class IAMCredentials SELECTOR_KEY = "x-goog-iam-authority-selector".freeze TOKEN_KEY = "x-goog-iam-authorization-token".freeze # Initializes an IAMCredentials. # # @param selector [String] The IAM selector. # @param token [String] The IAM token. # @raise [TypeError] If selector or token is not a String def initialize selector, token raise TypeError unless selector.is_a? String raise TypeError unless token.is_a? String @selector = selector @token = token end # Adds the credential fields to the hash. # # @param a_hash [Hash] The hash to update with credentials # @return [Hash] The updated hash with credentials def apply! a_hash a_hash[SELECTOR_KEY] = @selector a_hash[TOKEN_KEY] = @token a_hash end # Returns a clone of a_hash updated with the authorization header # # @param a_hash [Hash] The hash to clone and update with credentials # @return [Hash] A new hash with credentials def apply a_hash a_copy = a_hash.clone apply! a_copy a_copy end # Returns a reference to the #apply method, suitable for passing as # a closure # # @return [Proc] A procedure that updates a hash with credentials def updater_proc proc { |a_hash, _opts = {}| apply a_hash } end # Returns the IAM authority selector as the principal # @private # @return [String] the IAM authoirty selector def principal @selector end end end end googleauth-1.16.1/lib/googleauth/web_user_authorizer.rb0000644000004100000410000003042115135672542023320 0ustar www-datawww-data# Copyright 2014 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 "multi_json" require "googleauth/errors" require "googleauth/signet" require "googleauth/user_authorizer" require "googleauth/user_refresh" require "securerandom" module Google module Auth # Varation on {Google::Auth::UserAuthorizer} adapted for Rack based # web applications. # # Example usage: # # get('/') do # user_id = request.session['user_email'] # credentials = authorizer.get_credentials(user_id, request) # if credentials.nil? # redirect authorizer.get_authorization_url(user_id: user_id, # request: request) # end # # Credentials are valid, can call APIs # ... # end # # get('/oauth2callback') do # url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred( # request) # redirect url # end # # Instead of implementing the callback directly, applications are # encouraged to use {Google::Auth::WebUserAuthorizer::CallbackApp} instead. # # @see CallbackApp # @note Requires sessions are enabled class WebUserAuthorizer < Google::Auth::UserAuthorizer STATE_PARAM = "state".freeze AUTH_CODE_KEY = "code".freeze ERROR_CODE_KEY = "error".freeze SESSION_ID_KEY = "session_id".freeze CALLBACK_STATE_KEY = "g-auth-callback".freeze CURRENT_URI_KEY = "current_uri".freeze XSRF_KEY = "g-xsrf-token".freeze SCOPE_KEY = "scope".freeze NIL_REQUEST_ERROR = "Request is required.".freeze NIL_SESSION_ERROR = "Sessions must be enabled".freeze MISSING_AUTH_CODE_ERROR = "Missing authorization code in request".freeze AUTHORIZATION_ERROR = "Authorization error: %s".freeze INVALID_STATE_TOKEN_ERROR = "State token does not match expected value".freeze class << self attr_accessor :default end # Handle the result of the oauth callback. This version defers the # exchange of the code by temporarily stashing the results in the user's # session. This allows apps to use the generic # {Google::Auth::WebUserAuthorizer::CallbackApp} handler for the callback # without any additional customization. # # Apps that wish to handle the callback directly should use # {#handle_auth_callback} instead. # # @param [Rack::Request] request # Current request # @return [String, nil] # Redirect URI if successfully extracted, nil otherwise def self.handle_auth_callback_deferred request callback_state, redirect_uri = extract_callback_state request request.session[CALLBACK_STATE_KEY] = MultiJson.dump callback_state redirect_uri end # Initialize the authorizer # # @param [Google::Auth::ClientID] client_id # Configured ID & secret for this application # @param [String, Array] scope # Authorization scope to request # @param [Google::Auth::Stores::TokenStore] token_store # Backing storage for persisting user credentials # @param [String] legacy_callback_uri # URL (either absolute or relative) of the auth callback. Defaults # to '/oauth2callback'. # @deprecated This field is deprecated. Instead, use the keyword # argument callback_uri. # @param [String] code_verifier # Random string of 43-128 chars used to verify the key exchange using # PKCE. def initialize client_id, scope, token_store, legacy_callback_uri = nil, callback_uri: nil, code_verifier: nil super client_id, scope, token_store, legacy_callback_uri, code_verifier: code_verifier, callback_uri: callback_uri end # Handle the result of the oauth callback. Exchanges the authorization # code from the request and persists to storage. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [Rack::Request] request # Current request # @return (Google::Auth::UserRefreshCredentials, String) # credentials & next URL to redirect to def handle_auth_callback user_id, request callback_state, redirect_uri = WebUserAuthorizer.extract_callback_state( request ) WebUserAuthorizer.validate_callback_state callback_state, request credentials = get_and_store_credentials_from_code( user_id: user_id, code: callback_state[AUTH_CODE_KEY], scope: callback_state[SCOPE_KEY], base_url: request.url ) [credentials, redirect_uri] end # Build the URL for requesting authorization. # # @param [String] login_hint # Login hint if need to authorize a specific account. Should be a # user's email address or unique profile ID. # @param [Rack::Request] request # Current request # @param [String] redirect_to # Optional URL to proceed to after authorization complete. Defaults to # the current URL. # @param [String, Array] scope # Authorization scope to request. Overrides the instance scopes if # not nil. # @param [Hash] state # Optional key-values to be returned to the oauth callback. # @return [String] # Authorization url # @raise [Google::Auth::InitializationError] # If request is nil or request.session is nil def get_authorization_url options = {} options = options.dup request = options[:request] raise InitializationError, NIL_REQUEST_ERROR if request.nil? raise InitializationError, NIL_SESSION_ERROR if request.session.nil? state = options[:state] || {} redirect_to = options[:redirect_to] || request.url request.session[XSRF_KEY] = SecureRandom.base64 options[:state] = MultiJson.dump(state.merge( SESSION_ID_KEY => request.session[XSRF_KEY], CURRENT_URI_KEY => redirect_to )) options[:base_url] = request.url super options end # Fetch stored credentials for the user from the given request session. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [Rack::Request] request # Current request. Optional. If omitted, this will attempt to fall back # on the base class behavior of reading from the token store. # @param [Array, String] scope # If specified, only returns credentials that have all the \ # requested scopes # @return [Google::Auth::UserRefreshCredentials] # Stored credentials, nil if none present # @raise [Google::Auth::AuthorizationError] # If the authorization code is missing, there's an error in the request, # or the state token doesn't match def get_credentials user_id, request = nil, scope = nil if request&.session&.key? CALLBACK_STATE_KEY # Note - in theory, no need to check required scope as this is # expected to be called immediately after a return from authorization state_json = request.session.delete CALLBACK_STATE_KEY callback_state = MultiJson.load state_json WebUserAuthorizer.validate_callback_state callback_state, request get_and_store_credentials_from_code( user_id: user_id, code: callback_state[AUTH_CODE_KEY], scope: callback_state[SCOPE_KEY], base_url: request.url ) else super user_id, scope end end # Extract the callback state from the request # # @param [Rack::Request] request # Current request # @return [Array] # Callback state and redirect URI def self.extract_callback_state request state = MultiJson.load(request.params[STATE_PARAM] || "{}") redirect_uri = state[CURRENT_URI_KEY] callback_state = { AUTH_CODE_KEY => request.params[AUTH_CODE_KEY], ERROR_CODE_KEY => request.params[ERROR_CODE_KEY], SESSION_ID_KEY => state[SESSION_ID_KEY], SCOPE_KEY => request.params[SCOPE_KEY] } [callback_state, redirect_uri] end # Returns the principal identifier for this web authorizer # This is a class method that returns a symbol since # we might not have a client_id in the static callback context # # @return [Symbol] The symbol for web user authorization def self.principal :web_user_authorization end # Verifies the results of an authorization callback # # @param [Hash] state # Callback state # @option state [String] AUTH_CODE_KEY # The authorization code # @option state [String] ERROR_CODE_KEY # Error message if failed # @param [Rack::Request] request # Current request # @raise [Google::Auth::AuthorizationError] # If the authorization code is missing, there's an error in the callback state, # or the state token doesn't match def self.validate_callback_state state, request if state[AUTH_CODE_KEY].nil? raise AuthorizationError.with_details( MISSING_AUTH_CODE_ERROR, credential_type_name: name, principal: principal ) end if state[ERROR_CODE_KEY] raise AuthorizationError.with_details( format(AUTHORIZATION_ERROR, state[ERROR_CODE_KEY]), credential_type_name: name, principal: principal ) elsif request.session[XSRF_KEY] != state[SESSION_ID_KEY] raise AuthorizationError.with_details( INVALID_STATE_TOKEN_ERROR, credential_type_name: name, principal: principal ) end end # Small Rack app which acts as the default callback handler for the app. # # To configure in Rails, add to routes.rb: # # match '/oauth2callback', # to: Google::Auth::WebUserAuthorizer::CallbackApp, # via: :all # # With Rackup, add to config.ru: # # map '/oauth2callback' do # run Google::Auth::WebUserAuthorizer::CallbackApp # end # # Or in a classic Sinatra app: # # get('/oauth2callback') do # Google::Auth::WebUserAuthorizer::CallbackApp.call(env) # end # # @see Google::Auth::WebUserAuthorizer class CallbackApp LOCATION_HEADER = "Location".freeze REDIR_STATUS = 302 ERROR_STATUS = 500 # Handle a rack request. Simply stores the results the authorization # in the session temporarily and redirects back to to the previously # saved redirect URL. Credentials can be later retrieved by calling. # {Google::Auth::Web::WebUserAuthorizer#get_credentials} # # See {Google::Auth::Web::WebUserAuthorizer#get_authorization_uri} # for how to initiate authorization requests. # # @param [Hash] env # Rack environment # @return [Array] # HTTP response def self.call env request = Rack::Request.new env return_url = WebUserAuthorizer.handle_auth_callback_deferred request if return_url [REDIR_STATUS, { LOCATION_HEADER => return_url }, []] else [ERROR_STATUS, {}, ["No return URL is present in the request."]] end end def call env self.class.call env end end end end end googleauth-1.16.1/lib/googleauth/stores/0000755000004100000410000000000015135672542020223 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/stores/file_token_store.rb0000644000004100000410000000276315135672542024113 0ustar www-datawww-data# Copyright 2014 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 "yaml/store" require "googleauth/token_store" module Google module Auth module Stores # Implementation of user token storage backed by a local YAML file class FileTokenStore < Google::Auth::TokenStore # Create a new store with the supplied file. # # @param [String, File] file # Path to storage file def initialize options = {} super() path = options[:file] @store = YAML::Store.new path end # (see Google::Auth::Stores::TokenStore#load) def load id @store.transaction { @store[id] } end # (see Google::Auth::Stores::TokenStore#store) def store id, token @store.transaction { @store[id] = token } end # (see Google::Auth::Stores::TokenStore#delete) def delete id @store.transaction { @store.delete id } end end end end end googleauth-1.16.1/lib/googleauth/stores/redis_token_store.rb0000644000004100000410000000462415135672542024300 0ustar www-datawww-data# Copyright 2014 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 "redis" require "googleauth/token_store" module Google module Auth module Stores # Implementation of user token storage backed by Redis. Tokens # are stored as JSON using the supplied key, prefixed with # `g-user-token:` class RedisTokenStore < Google::Auth::TokenStore DEFAULT_KEY_PREFIX = "g-user-token:".freeze # Create a new store with the supplied redis client. # # @param [::Redis, String] redis # Initialized redis client to connect to. # @param [String] prefix # Prefix for keys in redis. Defaults to 'g-user-token:' # @note If no redis instance is provided, a new one is created and # the options passed through. You may include any other keys accepted # by `Redis.new` def initialize options = {} super() redis = options.delete :redis prefix = options.delete :prefix @redis = case redis when Redis redis else Redis.new options end @prefix = prefix || DEFAULT_KEY_PREFIX end # (see Google::Auth::Stores::TokenStore#load) def load id key = key_for id @redis.get key end # (see Google::Auth::Stores::TokenStore#store) def store id, token key = key_for id @redis.set key, token end # (see Google::Auth::Stores::TokenStore#delete) def delete id key = key_for id @redis.del key end private # Generate a redis key from a token ID # # @param [String] id # ID of the token # @return [String] # Redis key def key_for id @prefix + id end end end end end googleauth-1.16.1/lib/googleauth/base_client.rb0000644000004100000410000000617615135672542021513 0ustar www-datawww-data# Copyright 2023 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 "google/logging/message" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # BaseClient is a class used to contain common methods that are required by any # Credentials Client, including AwsCredentials, ServiceAccountCredentials, # and UserRefreshCredentials. This is a superclass of Signet::OAuth2::Client # and has been created to create a generic interface for all credentials clients # to use, including ones which do not inherit from Signet::OAuth2::Client. module BaseClient AUTH_METADATA_KEY = :authorization # Updates a_hash updated with the authentication token def apply! a_hash, opts = {} # fetch the access token there is currently not one, or if the client # has expired fetch_access_token! opts if needs_access_token? token = send token_type a_hash[AUTH_METADATA_KEY] = "Bearer #{token}" logger&.debug do hash = Digest::SHA256.hexdigest token Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})" end a_hash[AUTH_METADATA_KEY] end # Returns a clone of a_hash updated with the authentication token def apply a_hash, opts = {} a_copy = a_hash.clone apply! a_copy, opts a_copy end # Whether the id_token or access_token is missing or about to expire. def needs_access_token? send(token_type).nil? || expires_within?(60) end # Returns a reference to the #apply method, suitable for passing as # a closure def updater_proc proc { |a_hash, opts = {}| apply a_hash, opts } end def on_refresh &block @refresh_listeners = [] unless defined? @refresh_listeners @refresh_listeners << block end def notify_refresh_listeners listeners = defined?(@refresh_listeners) ? @refresh_listeners : [] listeners.each do |block| block.call self end end def expires_within? raise NoMethodError, "expires_within? not implemented" end # The logger used to log operations on this client, such as token refresh. attr_accessor :logger # @private def principal raise NoMethodError, "principal not implemented" end private def token_type raise NoMethodError, "token_type not implemented" end def fetch_access_token! raise NoMethodError, "fetch_access_token! not implemented" end end end end googleauth-1.16.1/lib/googleauth/api_key.rb0000644000004100000410000001340715135672542020657 0ustar www-datawww-data# Copyright 2025 Google LLC # # 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 "googleauth/base_client" require "googleauth/credentials_loader" module Google module Auth ## # Implementation of Google API Key authentication. # # API Keys are text strings. They don't have an associated JSON file. # # The end-user is managing their API Keys directly, not via # an authentication library. # # API Keys provide project information for an API request. # API Keys don't reference an IAM principal, they do not expire, # and cannot be refreshed. # class APIKeyCredentials include Google::Auth::BaseClient # @private Authorization header key API_KEY_HEADER = "x-goog-api-key".freeze # @private Environment variable containing API key API_KEY_VAR = "GOOGLE_API_KEY".freeze # @return [String] The API key attr_reader :api_key # @return [String] The universe domain of the universe # this API key is for attr_accessor :universe_domain class << self # Creates an APIKeyCredentials from the environment. # Checks the ENV['GOOGLE_API_KEY'] variable. # # @param [String] _scope # The scope to use for OAuth. Not used by API key auth. # @param [Hash] options # The options to pass to the credentials instance # # @return [Google::Auth::APIKeyCredentials, nil] # Credentials if the API key environment variable is present, # nil otherwise def from_env _scope = nil, options = {} api_key = ENV[API_KEY_VAR] return nil if api_key.nil? || api_key.empty? new options.merge(api_key: api_key) end # Create the APIKeyCredentials. # # @param [Hash] options The credentials options # @option options [String] :api_key # The API key to use for authentication # @option options [String] :universe_domain # The universe domain of the universe this API key # belongs to (defaults to googleapis.com) # @return [Google::Auth::APIKeyCredentials] def make_creds options = {} new options end end # Initialize the APIKeyCredentials. # # @param [Hash] options The credentials options # @option options [String] :api_key # The API key to use for authentication # @option options [String] :universe_domain # The universe domain of the universe this API key # belongs to (defaults to googleapis.com) # @raise [ArgumentError] If the API key is nil or empty def initialize options = {} raise ArgumentError, "API key must be provided" if options[:api_key].nil? || options[:api_key].empty? @api_key = options[:api_key] @universe_domain = options[:universe_domain] || "googleapis.com" end # Determines if the credentials object has expired. # Since API keys don't expire, this always returns false. # # @param [Fixnum] _seconds # The optional timeout in seconds since the last refresh # @return [Boolean] # True if the token has expired, false otherwise. def expires_within? _seconds false end # Creates a duplicate of these credentials. # # @param [Hash] options Additional options for configuring the credentials # @return [Google::Auth::APIKeyCredentials] def duplicate options = {} self.class.new( api_key: options[:api_key] || @api_key, universe_domain: options[:universe_domain] || @universe_domain ) end # Updates the provided hash with the API Key header. # # The `apply!` method modifies the provided hash in place, adding the # `x-goog-api-key` header with the API Key value. # # The API Key is hashed before being logged for security purposes. # # NB: this method typically would be called through `updater_proc`. # Some older clients call it directly though, so it has to be public. # # @param [Hash] a_hash The hash to which the API Key header should be added. # This is typically a hash representing the request headers. This hash # will be modified in place. # @param [Hash] _opts Additional options (currently not used). Included # for consistency with the `BaseClient` interface. # @return [Hash] The modified hash (the same hash passed as the `a_hash` # argument). def apply! a_hash, _opts = {} a_hash[API_KEY_HEADER] = @api_key logger&.debug do hash = Digest::SHA256.hexdigest @api_key Google::Logging::Message.from message: "Sending API key auth token. (sha256:#{hash})" end a_hash end # For credentials that are initialized with a token without a principal, # the type of that token should be returned as a principal instead # @private # @return [Symbol] the token type in lieu of the principal def principal token_type end protected # The token type should be :api_key def token_type :api_key end # We don't need to fetch access tokens for API key auth def fetch_access_token! _options = {} nil end end end end googleauth-1.16.1/lib/googleauth/scope_util.rb0000644000004100000410000000523515135672542021404 0ustar www-datawww-data# Copyright 2015 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 "googleauth/signet" require "googleauth/credentials_loader" require "multi_json" module Google module Auth ## # Small utility for normalizing scopes into canonical form. # # The canonical form of scopes is as an array of strings, each in the form # of a full URL. This utility converts space-delimited scope strings into # this form, and handles a small number of common aliases. # # This is used by UserRefreshCredentials to verify that a credential grants # a requested scope. # module ScopeUtil ## # Aliases understood by this utility # ALIASES = { "email" => "https://www.googleapis.com/auth/userinfo.email", "profile" => "https://www.googleapis.com/auth/userinfo.profile", "openid" => "https://www.googleapis.com/auth/plus.me" }.freeze ## # Normalize the input, which may be an array of scopes or a whitespace- # delimited scope string. The output is always an array, even if a single # scope is input. # # @param scope [String,Array] Input scope(s) # @return [Array] An array of scopes in canonical form. # def self.normalize scope list = as_array scope list.map { |item| ALIASES[item] || item } end ## # Ensure the input is an array. If a single string is passed in, splits # it via whitespace. Does not interpret aliases. # # @param scope [String,Array] Input scope(s) # @return [Array] Always an array of strings # @raise [ArgumentError] If the input is not a string or array of strings # def self.as_array scope case scope when Array scope.each do |item| unless item.is_a? String raise ArgumentError, "Invalid scope value: #{item.inspect}. Must be string or array" end end scope when String scope.split else raise ArgumentError, "Invalid scope value: #{scope.inspect}. Must be string or array" end end end end end googleauth-1.16.1/lib/googleauth/client_id.rb0000644000004100000410000000743215135672542021171 0ustar www-datawww-data# Copyright 2014 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 "multi_json" require "googleauth/credentials_loader" require "googleauth/errors" module Google module Auth ## # Representation of an application's identity for user authorization flows. # class ClientId # Toplevel JSON key for the an installed app configuration. # Must include client_id and client_secret subkeys if present. INSTALLED_APP = "installed".freeze # Toplevel JSON key for the a webapp configuration. # Must include client_id and client_secret subkeys if present. WEB_APP = "web".freeze # JSON key for the client ID within an app configuration. CLIENT_ID = "client_id".freeze # JSON key for the client secret within an app configuration. CLIENT_SECRET = "client_secret".freeze # An error message raised when none of the expected toplevel properties # can be found. MISSING_TOP_LEVEL_ELEMENT_ERROR = "Expected top level property 'installed' or 'web' to be present.".freeze ## # Text identifier of the client ID # @return [String] # attr_reader :id ## # Secret associated with the client ID # @return [String] # attr_reader :secret class << self attr_accessor :default end ## # Initialize the Client ID. Both id and secret must be non-nil. # # @param [String] id # Text identifier of the client ID # @param [String] secret # Secret associated with the client ID # @note Direct instantiation is discouraged to avoid embedding IDs # and secrets in source. See {#from_file} to load from # `client_secrets.json` files. # @raise [Google::Auth::InitializationError] If id or secret is nil # def initialize id, secret raise InitializationError, "Client id can not be nil" if id.nil? raise InitializationError, "Client secret can not be nil" if secret.nil? @id = id @secret = secret end ## # Constructs a Client ID from a JSON file downloaded from the # Google Developers Console. # # @param [String, File] file # Path of file to read from # @return [Google::Auth::ClientID] # @raise [Google::Auth::InitializationError] If file is nil # def self.from_file file raise InitializationError, "File can not be nil." if file.nil? File.open file.to_s do |f| json = f.read config = MultiJson.load json from_hash config end end ## # Constructs a Client ID from a previously loaded JSON file. The hash # structure should match the expected JSON format. # # @param [hash] config # Parsed contents of the JSON file # @return [Google::Auth::ClientID] # @raise [Google::Auth::InitializationError] If config is nil or missing required elements # def self.from_hash config raise InitializationError, "Hash can not be nil." if config.nil? raw_detail = config[INSTALLED_APP] || config[WEB_APP] raise InitializationError, MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil? ClientId.new raw_detail[CLIENT_ID], raw_detail[CLIENT_SECRET] end end end end googleauth-1.16.1/lib/googleauth/user_refresh.rb0000644000004100000410000001743115135672542021733 0ustar www-datawww-data# Copyright 2015 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 "googleauth/credentials_loader" require "googleauth/errors" require "googleauth/scope_util" require "googleauth/signet" require "multi_json" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # Authenticates requests using User Refresh credentials. # # This class allows authorizing requests from user refresh tokens. # # This the end of the result of a 3LO flow. E.g, the end result of # 'gcloud auth login' saves a file with these contents in well known # location # # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production) class UserRefreshCredentials < Signet::OAuth2::Client TOKEN_CRED_URI = "https://oauth2.googleapis.com/token".freeze AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/auth".freeze REVOKE_TOKEN_URI = "https://oauth2.googleapis.com/revoke".freeze extend CredentialsLoader attr_reader :project_id attr_reader :quota_project_id # @private # @type [::String] The type name for this credential. CREDENTIAL_TYPE_NAME = "authorized_user".freeze # Create a UserRefreshCredentials. # # @param json_key_io [IO] An IO object containing the JSON key # @param scope [string|array|nil] the scope(s) to access def self.make_creds options = {} # rubocop:disable Metrics/MethodLength json_key_io, scope = options.values_at :json_key_io, :scope user_creds = if json_key_io json_key = MultiJson.load json_key_io.read if json_key.key? "type" json_key_io.rewind else # Defaults to class credential 'type' if missing. json_key["type"] = CREDENTIAL_TYPE_NAME json_key_io = StringIO.new MultiJson.dump(json_key) end CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME read_json_key json_key_io else { "client_id" => ENV[CredentialsLoader::CLIENT_ID_VAR], "client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR], "refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR], "project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR], "quota_project_id" => nil, "universe_domain" => nil } end new(token_credential_uri: TOKEN_CRED_URI, client_id: user_creds["client_id"], client_secret: user_creds["client_secret"], refresh_token: user_creds["refresh_token"], project_id: user_creds["project_id"], quota_project_id: user_creds["quota_project_id"], scope: scope, universe_domain: user_creds["universe_domain"] || "googleapis.com") .configure_connection(options) end # Reads a JSON key from an IO object and extracts required fields. # # @param [IO] json_key_io An IO object containing the JSON key # @return [Hash] The parsed JSON key # @raise [Google::Auth::InitializationError] If the JSON is missing required fields def self.read_json_key json_key_io json_key = MultiJson.load json_key_io.read wanted = ["client_id", "client_secret", "refresh_token"] wanted.each do |key| raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key end json_key end def initialize options = {} options ||= {} options[:token_credential_uri] ||= TOKEN_CRED_URI options[:authorization_uri] ||= AUTHORIZATION_URI @project_id = options[:project_id] @project_id ||= CredentialsLoader.load_gcloud_project_id @quota_project_id = options[:quota_project_id] super options end # Creates a duplicate of these credentials # without the Signet::OAuth2::Client-specific # transient state (e.g. cached tokens) # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized in addition to keys in the # Signet::OAuth2::Client # * `project_id` the project id to use during the authentication # * `quota_project_id` the quota project id to use # during the authentication def duplicate options = {} options = deep_hash_normalize options super( { project_id: @project_id, quota_project_id: @quota_project_id }.merge(options) ) end # Revokes the credential # # @param [Hash] options Options for revoking the credential # @option options [Faraday::Connection] :connection The connection to use # @raise [Google::Auth::AuthorizationError] If the revocation request fails def revoke! options = {} c = options[:connection] || Faraday.default_connection retry_with_error do resp = c.post(REVOKE_TOKEN_URI, token: refresh_token || access_token) case resp.status when 200 self.access_token = nil self.refresh_token = nil self.expires_at = 0 else raise AuthorizationError.with_details( "Unexpected error code #{resp.status}", credential_type_name: self.class.name, principal: principal ) end end end # Verifies that a credential grants the requested scope # # @param [Array, String] required_scope # Scope to verify # @return [Boolean] # True if scope is granted def includes_scope? required_scope missing_scope = Google::Auth::ScopeUtil.normalize(required_scope) - Google::Auth::ScopeUtil.normalize(scope) missing_scope.empty? end # Destructively updates these credentials # # This method is called by `Signet::OAuth2::Client`'s constructor # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized in addition to keys in the # Signet::OAuth2::Client # * `project_id` the project id to use during the authentication # * `quota_project_id` the quota project id to use # during the authentication # @return [Google::Auth::UserRefreshCredentials] def update! options = {} # Normalize all keys to symbols to allow indifferent access. options = deep_hash_normalize options @project_id = options[:project_id] if options.key? :project_id @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id super(options) self end # Returns the client ID as the principal for user refresh credentials # @private # @return [String, Symbol] the client ID or :user_refresh if not available def principal @client_id || :user_refresh end end end end googleauth-1.16.1/lib/googleauth/compute_engine.rb0000644000004100000410000002727415135672542022246 0ustar www-datawww-data# Copyright 2015 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 "google-cloud-env" require "googleauth/errors" require "googleauth/signet" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth NO_METADATA_SERVER_ERROR = <<~ERROR.freeze Error code 404 trying to get security access token from Compute Engine metadata for the default service account. This may be because the virtual machine instance does not have permission scopes specified. ERROR UNEXPECTED_ERROR_SUFFIX = <<~ERROR.freeze trying to get security access token from Compute Engine metadata for the default service account ERROR # Extends Signet::OAuth2::Client so that the auth token is obtained from # the GCE metadata server. class GCECredentials < Signet::OAuth2::Client # @private Unused and deprecated but retained to prevent breaking changes DEFAULT_METADATA_HOST = "169.254.169.254".freeze # @private Unused and deprecated but retained to prevent breaking changes COMPUTE_AUTH_TOKEN_URI = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze # @private Unused and deprecated but retained to prevent breaking changes COMPUTE_ID_TOKEN_URI = "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze # @private Unused and deprecated but retained to prevent breaking changes COMPUTE_CHECK_URI = "http://169.254.169.254".freeze class << self # @private Unused and deprecated def metadata_host ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST end # @private Unused and deprecated def compute_check_uri "http://#{metadata_host}".freeze end # @private Unused and deprecated def compute_auth_token_uri "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze end # @private Unused and deprecated def compute_id_token_uri "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze end # Detect if this appear to be a GCE instance, by checking if metadata # is available. # The parameters are deprecated and unused. def on_gce? _options = {}, _reload = false # rubocop:disable Style/OptionalBooleanParameter Google::Cloud.env.metadata? end def reset_cache Google::Cloud.env.compute_metadata.reset_existence! Google::Cloud.env.compute_metadata.cache.expire_all! end alias unmemoize_all reset_cache end # @private Temporary; remove when universe domain metadata endpoint is stable (see b/349488459). attr_accessor :disable_universe_domain_check # Construct a GCECredentials def initialize options = {} # Override the constructor to remember whether the universe domain was # overridden by a constructor argument. @universe_domain_overridden = options["universe_domain"] || options[:universe_domain] # TODO: Remove when universe domain metadata endpoint is stable (see b/349488459). @disable_universe_domain_check = true super options end # Creates a duplicate of these credentials # without the Signet::OAuth2::Client-specific # transient state (e.g. cached tokens) # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized in addition to keys in the # Signet::OAuth2::Client # * `:universe_domain_overridden` Whether the universe domain was # overriden during credentials creation def duplicate options = {} options = deep_hash_normalize options super( { universe_domain_overridden: @universe_domain_overridden }.merge(options) ) end # @private # Overrides universe_domain getter to fetch lazily if it hasn't been # fetched yet. This is necessary specifically for Compute Engine because # the universe comes from the metadata service, and isn't known # immediately on credential construction. All other credential types read # the universe from their json key or other immediate input. def universe_domain value = super return value unless value.nil? fetch_access_token! super end # Overrides the super class method to change how access tokens are # fetched. # # @param [Hash] _options Options for token fetch (not used) # @return [Hash] The token data hash # @raise [Google::Auth::UnexpectedStatusError] On unexpected HTTP status codes # @raise [Google::Auth::AuthorizationError] If metadata server is unavailable or returns error def fetch_access_token _options = {} query, entry = build_metadata_request_params begin log_fetch_query resp = Google::Cloud.env.lookup_metadata_response "instance", entry, query: query log_fetch_resp resp handle_metadata_response resp rescue Google::Cloud::Env::MetadataServerNotResponding => e log_fetch_err e raise AuthorizationError.with_details( e.message, credential_type_name: self.class.name, principal: principal ) end end # Destructively updates these credentials. # # This method is called by `Signet::OAuth2::Client`'s constructor # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized in addition to keys in the # Signet::OAuth2::Client # * `:universe_domain_overridden` Whether the universe domain was # overriden during credentials creation # @return [Google::Auth::GCECredentials] def update! options = {} # Normalize all keys to symbols to allow indifferent access. options = deep_hash_normalize options @universe_domain_overridden = options[:universe_domain_overridden] if options.key? :universe_domain_overridden super(options) self end # Returns the principal identifier for GCE credentials # @private # @return [Symbol] :gce to represent Google Compute Engine identity def principal :gce_metadata end private # @private # Builds query parameters and endpoint for metadata request # @return [Array] The query parameters and endpoint path def build_metadata_request_params query, entry = if token_type == :id_token [{ "audience" => target_audience, "format" => "full" }, "service-accounts/default/identity"] else [{}, "service-accounts/default/token"] end query[:scopes] = Array(scope).join "," if scope [query, entry] end # @private # Handles the response from the metadata server # @param [Google::Cloud::Env::MetadataResponse] resp The metadata server response # @return [Hash] The token hash on success # @raise [Google::Auth::UnexpectedStatusError, Google::Auth::AuthorizationError] On error def handle_metadata_response resp case resp.status when 200 build_token_hash resp.body, resp.headers["content-type"], resp.retrieval_monotonic_time when 403, 500 raise Signet::UnexpectedStatusError, "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}" when 404 raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR else raise Signet::AuthorizationError, "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}" end end def log_fetch_query if token_type == :id_token logger&.info do Google::Logging::Message.from( message: "Requesting id token from MDS with aud=#{target_audience}", "credentialsId" => object_id ) end else logger&.info do Google::Logging::Message.from( message: "Requesting access token from MDS", "credentialsId" => object_id ) end end end def log_fetch_resp resp logger&.info do Google::Logging::Message.from( message: "Received #{resp.status} from MDS", "credentialsId" => object_id ) end end def log_fetch_err _err logger&.info do Google::Logging::Message.from( message: "MDS did not respond to token request", "credentialsId" => object_id ) end end # Constructs a token hash from the metadata server response # # @private # @param [String] body The response body from the metadata server # @param [String] content_type The content type of the response # @param [Float] retrieval_time The monotonic time when the response was retrieved # # @return [Hash] A hash containing: # - access_token/id_token: The actual token depending on what was requested # - token_type: The type of token (usually "Bearer") # - expires_in: Seconds until token expiration (adjusted for freshness) # - universe_domain: The universe domain for the token (if not overridden) def build_token_hash body, content_type, retrieval_time hash = if ["text/html", "application/text"].include? content_type parse_encoded_token body else Signet::OAuth2.parse_credentials body, content_type end add_universe_domain_to hash adjust_for_stale_expires_in hash, retrieval_time hash end def parse_encoded_token body hash = { token_type.to_s => body } if token_type == :id_token expires_at = expires_at_from_id_token body hash["expires_at"] = expires_at if expires_at end hash end def add_universe_domain_to hash return if @universe_domain_overridden universe_domain = if disable_universe_domain_check # TODO: Remove when universe domain metadata endpoint is stable (see b/349488459). "googleapis.com" else Google::Cloud.env.lookup_metadata "universe", "universe-domain" end universe_domain = "googleapis.com" if !universe_domain || universe_domain.empty? hash["universe_domain"] = universe_domain.strip end # The response might have been cached, which means expires_in might be # stale. Update it based on the time since the data was retrieved. # We also ensure expires_in is conservative; subtracting at least 1 # second to offset any skew from metadata server latency. def adjust_for_stale_expires_in hash, retrieval_time return unless hash["expires_in"].is_a? Numeric offset = 1 + (Process.clock_gettime(Process::CLOCK_MONOTONIC) - retrieval_time).round hash["expires_in"] -= offset if offset.positive? hash["expires_in"] = 0 if hash["expires_in"].negative? end end end end googleauth-1.16.1/lib/googleauth/application_default.rb0000644000004100000410000000552615135672542023250 0ustar www-datawww-data# Copyright 2015 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 "googleauth/compute_engine" require "googleauth/default_credentials" require "googleauth/errors" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth NOT_FOUND_ERROR = <<~ERROR_MESSAGE.freeze Your credentials were not found. To set up Application Default Credentials for your environment, see https://cloud.google.com/docs/authentication/external/set-up-adc ERROR_MESSAGE module_function # Obtains the default credentials implementation to use in this # environment. # # Use this to obtain the Application Default Credentials for accessing # Google APIs. Application Default Credentials are described in detail # at https://cloud.google.com/docs/authentication/production. # # If supplied, scope is used to create the credentials instance, when it can # be applied. E.g, on google compute engine and for user credentials the # scope is ignored. # # @param scope [string|array|nil] the scope(s) to access # @param options [Hash] Connection options. These may be used to configure # the `Faraday::Connection` used for outgoing HTTP requests. For # example, if a connection proxy must be used in the current network, # you may provide a connection with with the needed proxy options. # The following keys are recognized: # * `:default_connection` The connection object to use for token # refresh requests. # * `:connection_builder` A `Proc` that creates and returns a # connection to use for token refresh requests. # * `:connection` The connection to use to determine whether GCE # metadata credentials are available. # @raise [Google::Auth::InitializationError] If the credentials cannot be found def get_application_default scope = nil, options = {} creds = DefaultCredentials.from_env(scope, options) || DefaultCredentials.from_well_known_path(scope, options) || DefaultCredentials.from_system_default_path(scope, options) return creds unless creds.nil? raise InitializationError, NOT_FOUND_ERROR unless GCECredentials.on_gce? options GCECredentials.new options.merge(scope: scope) end end end googleauth-1.16.1/lib/googleauth/external_account.rb0000644000004100000410000001517115135672542022574 0ustar www-datawww-data# Copyright 2022 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 "time" require "uri" require "googleauth/credentials_loader" require "googleauth/errors" require "googleauth/external_account/aws_credentials" require "googleauth/external_account/identity_pool_credentials" require "googleauth/external_account/pluggable_credentials" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # Authenticates requests using External Account credentials, such # as those provided by the AWS provider. module ExternalAccount # Provides an entrypoint for all Exernal Account credential classes. class Credentials # The subject token type used for AWS external_account credentials. AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".freeze MISSING_CREDENTIAL_SOURCE = "missing credential source for external account".freeze INVALID_EXTERNAL_ACCOUNT_TYPE = "credential source is not supported external account type".freeze # @private # @type [::String] The type name for this credential. CREDENTIAL_TYPE_NAME = "external_account".freeze # Create a ExternalAccount::Credentials # # @note Warning: # This method does not validate the credential configuration. A security # risk occurs when a credential configuration configured with malicious urls # is used. # When the credential configuration is accepted from an # untrusted source, you should validate it before using with this method. # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials # for more details. # # @param options [Hash] Options for creating credentials # @option options [IO] :json_key_io (required) An IO object containing the JSON key # @option options [String,Array,nil] :scope The scope(s) to access # @return [Google::Auth::ExternalAccount::AwsCredentials, # Google::Auth::ExternalAccount::IdentityPoolCredentials, # Google::Auth::ExternalAccount::PluggableAuthCredentials] # The appropriate external account credentials based on the credential source # @raise [Google::Auth::InitializationError] If the json file is missing, lacks required fields, # or does not contain a supported credential source def self.make_creds options = {} json_key_io, scope = options.values_at :json_key_io, :scope raise InitializationError, "A json file is required for external account credentials." unless json_key_io json_key = MultiJson.load json_key_io.read, symbolize_keys: true if json_key.key? :type json_key_io.rewind else # Defaults to class credential 'type' if missing. json_key[:type] = CREDENTIAL_TYPE_NAME json_key_io = StringIO.new MultiJson.dump(json_key) end CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME user_creds = read_json_key json_key_io # AWS credentials is determined by aws subject token type return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE raise InitializationError, MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil? user_creds[:scope] = scope make_external_account_credentials user_creds end # Reads the required fields from the JSON. # # @param json_key_io [IO] An IO object containing the JSON key # @return [Hash] The parsed JSON key # @raise [Google::Auth::InitializationError] If the JSON is missing required fields def self.read_json_key json_key_io json_key = MultiJson.load json_key_io.read, symbolize_keys: true wanted = [ :audience, :subject_token_type, :token_url, :credential_source ] wanted.each do |key| raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key end json_key end class << self private # Creates AWS credentials from the provided user credentials # # @param user_creds [Hash] The user credentials containing AWS credential source information # @param scope [String,Array,nil] The scope(s) to access # @return [Google::Auth::ExternalAccount::AwsCredentials] The AWS credentials def make_aws_credentials user_creds, scope Google::Auth::ExternalAccount::AwsCredentials.new( audience: user_creds[:audience], scope: scope, subject_token_type: user_creds[:subject_token_type], token_url: user_creds[:token_url], credential_source: user_creds[:credential_source], service_account_impersonation_url: user_creds[:service_account_impersonation_url], universe_domain: user_creds[:universe_domain] ) end # Creates the appropriate external account credentials based on the credential source type # # @param user_creds [Hash] The user credentials containing credential source information # @return [Google::Auth::ExternalAccount::IdentityPoolCredentials, # Google::Auth::ExternalAccount::PluggableAuthCredentials] # The appropriate external account credentials # @raise [Google::Auth::InitializationError] If the credential source is not a supported type def make_external_account_credentials user_creds unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil? return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds end unless user_creds[:credential_source][:executable].nil? return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds end raise InitializationError, INVALID_EXTERNAL_ACCOUNT_TYPE end end end end end end googleauth-1.16.1/lib/googleauth/json_key_reader.rb0000644000004100000410000000345715135672542022405 0ustar www-datawww-data# Copyright 2015 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 "googleauth/errors" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # JsonKeyReader contains the behaviour used to read private key and # client email fields from the service account module JsonKeyReader # Reads a JSON key from an IO object and extracts common fields. # # @param json_key_io [IO] An IO object containing the JSON key # @return [Array(String, String, String, String, String)] An array containing: # private_key, client_email, project_id, quota_project_id, and universe_domain # @raise [Google::Auth::InitializationError] If client_email or private_key # fields are missing from the JSON def read_json_key json_key_io json_key = MultiJson.load json_key_io.read raise InitializationError, "missing client_email" unless json_key.key? "client_email" raise InitializationError, "missing private_key" unless json_key.key? "private_key" [ json_key["private_key"], json_key["client_email"], json_key["project_id"], json_key["quota_project_id"], json_key["universe_domain"] ] end end end end googleauth-1.16.1/lib/googleauth/helpers/0000755000004100000410000000000015135672542020346 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/helpers/connection.rb0000644000004100000410000000230115135672542023026 0ustar www-datawww-data# Copyright 2023 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" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # Helpers provides utility methods for Google::Auth. module Helpers # Connection provides a Faraday connection for use with Google::Auth. module Connection module_function def default_connection @default_connection end def default_connection= conn @default_connection = conn end def connection @default_connection || Faraday.default_connection end end end end end googleauth-1.16.1/lib/googleauth/service_account.rb0000644000004100000410000002113515135672542022407 0ustar www-datawww-data# Copyright 2015 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 "jwt" require "multi_json" require "stringio" require "google/logging/message" require "googleauth/signet" require "googleauth/credentials_loader" require "googleauth/json_key_reader" require "googleauth/service_account_jwt_header" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # Authenticates requests using Google's Service Account credentials via an # OAuth access token. # # This class allows authorizing requests for service accounts directly # from credentials from a json key file downloaded from the developer # console (via 'Generate new Json Key'). # # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production) class ServiceAccountCredentials < Signet::OAuth2::Client TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze extend CredentialsLoader extend JsonKeyReader attr_reader :project_id attr_reader :quota_project_id # @private # @type [::String] The type name for this credential. CREDENTIAL_TYPE_NAME = "service_account".freeze def enable_self_signed_jwt? # Use a self-singed JWT if there's no information that can be used to # obtain an OAuth token, OR if there are scopes but also an assertion # that they are default scopes that shouldn't be used to fetch a token, # OR we are not in the default universe and thus OAuth isn't supported. target_audience.nil? && (scope.nil? || @enable_self_signed_jwt || universe_domain != "googleapis.com") end # Creates a ServiceAccountCredentials. # # @param json_key_io [IO] An IO object containing the JSON key # @param scope [string|array|nil] the scope(s) to access # @raise [ArgumentError] If both scope and target_audience are specified def self.make_creds options = {} # rubocop:disable Metrics/MethodLength json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri = options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience, :audience, :token_credential_uri raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience private_key, client_email, project_id, quota_project_id, universe_domain = if json_key_io json_key = MultiJson.load json_key_io.read if json_key.key? "type" json_key_io.rewind else # Defaults to class credential 'type' if missing. json_key["type"] = CREDENTIAL_TYPE_NAME json_key_io = StringIO.new MultiJson.dump(json_key) end CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME read_json_key json_key_io else creds_from_env end project_id ||= CredentialsLoader.load_gcloud_project_id new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI, audience: audience || TOKEN_CRED_URI, scope: scope, enable_self_signed_jwt: enable_self_signed_jwt, target_audience: target_audience, issuer: client_email, signing_key: OpenSSL::PKey::RSA.new(private_key), project_id: project_id, quota_project_id: quota_project_id, universe_domain: universe_domain || "googleapis.com") .configure_connection(options) end # Creates a duplicate of these credentials # without the Signet::OAuth2::Client-specific # transient state (e.g. cached tokens) # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized in addition to keys in the # Signet::OAuth2::Client # * `:enable_self_signed_jwt` Whether the self-signed JWT should # be used for the authentication # * `project_id` the project id to use during the authentication # * `quota_project_id` the quota project id to use # during the authentication def duplicate options = {} options = deep_hash_normalize options super( { enable_self_signed_jwt: @enable_self_signed_jwt, project_id: project_id, quota_project_id: quota_project_id, logger: logger }.merge(options) ) end # Handles certain escape sequences that sometimes appear in input. # Specifically, interprets the "\n" sequence for newline, and removes # enclosing quotes. # # @param str [String] The string to unescape # @return [String] The unescaped string def self.unescape str str = str.gsub '\n', "\n" str = str[1..-2] if str.start_with?('"') && str.end_with?('"') str end def initialize options = {} @project_id = options[:project_id] @quota_project_id = options[:quota_project_id] @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false super options end # Extends the base class to use a transient # ServiceAccountJwtHeaderCredentials for certain cases. def apply! a_hash, opts = {} if enable_self_signed_jwt? apply_self_signed_jwt! a_hash else super end end # Modifies this logic so it also requires self-signed-jwt to be disabled def needs_access_token? super && !enable_self_signed_jwt? end # Destructively updates these credentials # # This method is called by `Signet::OAuth2::Client`'s constructor # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized in addition to keys in the # Signet::OAuth2::Client # * `:enable_self_signed_jwt` Whether the self-signed JWT should # be used for the authentication # * `project_id` the project id to use during the authentication # * `quota_project_id` the quota project id to use # during the authentication # @return [Google::Auth::ServiceAccountCredentials] def update! options = {} # Normalize all keys to symbols to allow indifferent access. options = deep_hash_normalize options @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false @project_id = options[:project_id] if options.key? :project_id @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id super(options) self end # Returns the client email as the principal for service account credentials # @private # @return [String] the email address of the service account def principal @issuer end private def apply_self_signed_jwt! a_hash # Use the ServiceAccountJwtHeaderCredentials using the same cred values cred_json = { private_key: @signing_key.to_s, client_email: @issuer, project_id: @project_id, quota_project_id: @quota_project_id } key_io = StringIO.new MultiJson.dump(cred_json) alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io, scope: scope alt.logger = logger alt.apply! a_hash end # @private # Loads service account credential details from environment variables. # # @return [Array] An array containing private_key, # client_email, project_id, quota_project_id, and universe_domain. def self.creds_from_env private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR] client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR] project_id = ENV[CredentialsLoader::PROJECT_ID_VAR] [private_key, client_email, project_id, nil, nil] end private_class_method :creds_from_env end end end googleauth-1.16.1/lib/googleauth/signet.rb0000644000004100000410000002041315135672542020522 0ustar www-datawww-data# Copyright 2015 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 "json" require "signet/oauth_2/client" require "googleauth/base_client" require "googleauth/errors" module Signet # OAuth2 supports OAuth2 authentication. module OAuth2 # Signet::OAuth2::Client creates an OAuth2 client # # This reopens Client to add #apply and #apply! methods which update a # hash with the fetched authentication token. class Client include Google::Auth::BaseClient alias update_token_signet_base update_token! def update_token! options = {} options = deep_hash_normalize options id_token_expires_at = expires_at_from_id_token options[:id_token] options[:expires_at] = id_token_expires_at if id_token_expires_at update_token_signet_base options self.universe_domain = options[:universe_domain] if options.key? :universe_domain self end alias update_signet_base update! def update! options = {} # Normalize all keys to symbols to allow indifferent access. options = deep_hash_normalize options # This `update!` method "overide" adds the `@logger`` update and # the `universe_domain` update. # # The `universe_domain` is also updated in `update_token!` but is # included here for completeness self.universe_domain = options[:universe_domain] if options.key? :universe_domain @logger = options[:logger] if options.key? :logger update_signet_base options end def configure_connection options @connection_info = options[:connection_builder] || options[:default_connection] self end # The token type as symbol, either :id_token or :access_token def token_type target_audience ? :id_token : :access_token end # Set the universe domain attr_accessor :universe_domain alias orig_fetch_access_token! fetch_access_token! def fetch_access_token! options = {} unless options[:connection] connection = build_default_connection options = options.merge connection: connection if connection end info = retry_with_error do orig_fetch_access_token! options end notify_refresh_listeners info end alias googleauth_orig_generate_access_token_request generate_access_token_request def generate_access_token_request options = {} parameters = googleauth_orig_generate_access_token_request options logger&.info do Google::Logging::Message.from( message: "Requesting access token from #{parameters['grant_type']}", "credentialsId" => object_id ) end logger&.debug do Google::Logging::Message.from( message: "Token fetch params: #{parameters}", "credentialsId" => object_id ) end parameters end def build_default_connection if !defined?(@connection_info) nil elsif @connection_info.respond_to? :call @connection_info.call else @connection_info end end # rubocop:disable Metrics/MethodLength # Retries the provided block with exponential backoff, handling and wrapping errors. # # @param [Integer] max_retry_count The maximum number of retries before giving up # @yield The block to execute and potentially retry # @return [Object] The result of the block if successful # @raise [Google::Auth::AuthorizationError] If a Signet::AuthorizationError occurs or if retries are exhausted # @raise [Google::Auth::ParseError] If a Signet::ParseError occurs during token parsing def retry_with_error max_retry_count = 5 retry_count = 0 begin yield.tap { |resp| log_response resp } rescue Signet::AuthorizationError, Signet::ParseError => e log_auth_error e error_class = e.is_a?(Signet::ParseError) ? Google::Auth::ParseError : Google::Auth::AuthorizationError raise error_class.with_details( e.message, credential_type_name: self.class.name, principal: respond_to?(:principal) ? principal : :signet_client ) rescue StandardError => e if retry_count < max_retry_count log_transient_error e retry_count += 1 sleep retry_count * 0.3 retry else log_retries_exhausted e msg = "Unexpected error: #{e.inspect}" raise Google::Auth::AuthorizationError.with_details( msg, credential_type_name: self.class.name, principal: respond_to?(:principal) ? principal : :signet_client ) end end end # rubocop:enable Metrics/MethodLength # Creates a duplicate of these credentials # without the Signet::OAuth2::Client-specific # transient state (e.g. cached tokens) # # @param options [Hash] Overrides for the credentials parameters. # @see Signet::OAuth2::Client#update! def duplicate options = {} options = deep_hash_normalize options opts = { authorization_uri: @authorization_uri, token_credential_uri: @token_credential_uri, client_id: @client_id, client_secret: @client_secret, scope: @scope, target_audience: @target_audience, redirect_uri: @redirect_uri, username: @username, password: @password, issuer: @issuer, person: @person, sub: @sub, audience: @audience, signing_key: @signing_key, extension_parameters: @extension_parameters, additional_parameters: @additional_parameters, access_type: @access_type, universe_domain: @universe_domain, logger: @logger }.merge(options) new_client = self.class.new opts new_client.configure_connection options end private def expires_at_from_id_token id_token match = /^[\w=-]+\.([\w=-]+)\.[\w=-]+$/.match id_token.to_s return unless match json = JSON.parse Base64.urlsafe_decode64 match[1] return unless json.key? "exp" Time.at json["exp"].to_i rescue StandardError # Shouldn't happen unless we get a garbled ID token nil end def log_response token_response response_hash = JSON.parse token_response rescue {} if response_hash["access_token"] digest = Digest::SHA256.hexdigest response_hash["access_token"] response_hash["access_token"] = "(sha256:#{digest})" end if response_hash["id_token"] digest = Digest::SHA256.hexdigest response_hash["id_token"] response_hash["id_token"] = "(sha256:#{digest})" end Google::Logging::Message.from( message: "Received auth token response: #{response_hash}", "credentialsId" => object_id ) end def log_auth_error err logger&.info do Google::Logging::Message.from( message: "Auth error when fetching auth token: #{err}", "credentialsId" => object_id ) end end def log_transient_error err logger&.info do Google::Logging::Message.from( message: "Transient error when fetching auth token: #{err}", "credentialsId" => object_id ) end end def log_retries_exhausted err logger&.info do Google::Logging::Message.from( message: "Exhausted retries when fetching auth token: #{err}", "credentialsId" => object_id ) end end end end end googleauth-1.16.1/lib/googleauth/user_authorizer.rb0000644000004100000410000003320415135672542022465 0ustar www-datawww-data# Copyright 2014 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 "uri" require "multi_json" require "googleauth/signet" require "googleauth/user_refresh" require "securerandom" module Google module Auth # Handles an interactive 3-Legged-OAuth2 (3LO) user consent authorization. # # Example usage for a simple command line app: # # credentials = authorizer.get_credentials(user_id) # if credentials.nil? # url = authorizer.get_authorization_url( # base_url: OOB_URI) # puts "Open the following URL in the browser and enter the " + # "resulting code after authorization" # puts url # code = gets # credentials = authorizer.get_and_store_credentials_from_code( # user_id: user_id, code: code, base_url: OOB_URI) # end # # Credentials ready to use, call APIs # ... class UserAuthorizer MISMATCHED_CLIENT_ID_ERROR = "Token client ID of %s does not match configured client id %s".freeze NIL_CLIENT_ID_ERROR = "Client id can not be nil.".freeze NIL_SCOPE_ERROR = "Scope can not be nil.".freeze NIL_USER_ID_ERROR = "User ID can not be nil.".freeze NIL_TOKEN_STORE_ERROR = "Can not call method if token store is nil".freeze MISSING_ABSOLUTE_URL_ERROR = 'Absolute base url required for relative callback url "%s"'.freeze # Initialize the authorizer # # @param [Google::Auth::ClientID] client_id # Configured ID & secret for this application # @param [String, Array] scope # Authorization scope to request # @param [Google::Auth::Stores::TokenStore] token_store # Backing storage for persisting user credentials # @param [String] legacy_callback_uri # URL (either absolute or relative) of the auth callback. # Defaults to '/oauth2callback'. # @deprecated This field is deprecated. Instead, use the keyword # argument callback_uri. # @param [String] code_verifier # Random string of 43-128 chars used to verify the key exchange using # PKCE. # @raise [Google::Auth::InitializationError] # If client_id is nil or scope is nil def initialize client_id, scope, token_store, legacy_callback_uri = nil, callback_uri: nil, code_verifier: nil raise InitializationError, NIL_CLIENT_ID_ERROR if client_id.nil? raise InitializationError, NIL_SCOPE_ERROR if scope.nil? @client_id = client_id @scope = Array(scope) @token_store = token_store @callback_uri = legacy_callback_uri || callback_uri || "/oauth2callback" @code_verifier = code_verifier end # Build the URL for requesting authorization. # # @param [String] login_hint # Login hint if need to authorize a specific account. Should be a # user's email address or unique profile ID. # @param [String] state # Opaque state value to be returned to the oauth callback. # @param [String] base_url # Absolute URL to resolve the configured callback uri against. Required # if the configured callback uri is a relative. # @param [String, Array] scope # Authorization scope to request. Overrides the instance scopes if not # nil. # @param [Hash] additional_parameters # Additional query parameters to be added to the authorization URL. # @return [String] # Authorization url def get_authorization_url options = {} scope = options[:scope] || @scope options[:additional_parameters] ||= {} if @code_verifier options[:additional_parameters].merge!( { code_challenge: generate_code_challenge(@code_verifier), code_challenge_method: code_challenge_method } ) end credentials = UserRefreshCredentials.new( client_id: @client_id.id, client_secret: @client_id.secret, scope: scope, additional_parameters: options[:additional_parameters] ) redirect_uri = redirect_uri_for options[:base_url] url = credentials.authorization_uri(access_type: "offline", redirect_uri: redirect_uri, approval_prompt: "force", state: options[:state], include_granted_scopes: true, login_hint: options[:login_hint]) url.to_s end # Fetch stored credentials for the user. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [Array, String] scope # If specified, only returns credentials that have all # the requested scopes # @return [Google::Auth::UserRefreshCredentials] # Stored credentials, nil if none present # @raise [Google::Auth::CredentialsError] # If the client ID in the stored token doesn't match the configured client ID def get_credentials user_id, scope = nil saved_token = stored_token user_id return nil if saved_token.nil? data = MultiJson.load saved_token if data.fetch("client_id", @client_id.id) != @client_id.id raise CredentialsError.with_details( format(MISMATCHED_CLIENT_ID_ERROR, data["client_id"], @client_id.id), credential_type_name: self.class.name, principal: principal ) end credentials = UserRefreshCredentials.new( client_id: @client_id.id, client_secret: @client_id.secret, scope: data["scope"] || @scope, access_token: data["access_token"], refresh_token: data["refresh_token"], expires_at: data.fetch("expiration_time_millis", 0) / 1000 ) scope ||= @scope return monitor_credentials user_id, credentials if credentials.includes_scope? scope nil end # Exchanges an authorization code returned in the oauth callback # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [String] code # The authorization code from the OAuth callback # @param [String, Array] scope # Authorization scope requested. Overrides the instance # scopes if not nil. # @param [String] base_url # Absolute URL to resolve the configured callback uri against. # Required if the configured # callback uri is a relative. # @param [Hash] additional_parameters # Additional parameters to be added to the post body of token # endpoint request. # @return [Google::Auth::UserRefreshCredentials] # Credentials if exchange is successful def get_credentials_from_code options = {} user_id = options[:user_id] code = options[:code] scope = options[:scope] || @scope base_url = options[:base_url] options[:additional_parameters] ||= {} options[:additional_parameters].merge!({ code_verifier: @code_verifier }) credentials = UserRefreshCredentials.new( client_id: @client_id.id, client_secret: @client_id.secret, redirect_uri: redirect_uri_for(base_url), scope: scope, additional_parameters: options[:additional_parameters] ) credentials.code = code credentials.fetch_access_token!({}) monitor_credentials user_id, credentials end # Exchanges an authorization code returned in the oauth callback. # Additionally, stores the resulting credentials in the token store if # the exchange is successful. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [String] code # The authorization code from the OAuth callback # @param [String, Array] scope # Authorization scope requested. Overrides the instance # scopes if not nil. # @param [String] base_url # Absolute URL to resolve the configured callback uri against. # Required if the configured # callback uri is a relative. # @return [Google::Auth::UserRefreshCredentials] # Credentials if exchange is successful def get_and_store_credentials_from_code options = {} credentials = get_credentials_from_code options store_credentials options[:user_id], credentials end # Revokes a user's credentials. This both revokes the actual # grant as well as removes the token from the token store. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. def revoke_authorization user_id credentials = get_credentials user_id if credentials begin @token_store.delete user_id ensure credentials.revoke! end end nil end # Store credentials for a user. Generally not required to be # called directly, but may be used to migrate tokens from one # store to another. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [Google::Auth::UserRefreshCredentials] credentials # Credentials to store. # @return [Google::Auth::UserRefreshCredentials] # The stored credentials def store_credentials user_id, credentials json = MultiJson.dump( client_id: credentials.client_id, access_token: credentials.access_token, refresh_token: credentials.refresh_token, scope: credentials.scope, expiration_time_millis: credentials.expires_at.to_i * 1000 ) @token_store.store user_id, json credentials end # The code verifier for PKCE for OAuth 2.0. When set, the # authorization URI will contain the Code Challenge and Code # Challenge Method querystring parameters, and the token URI will # contain the Code Verifier parameter. # # @param [String|nil] new_code_erifier def code_verifier= new_code_verifier @code_verifier = new_code_verifier end # Generate the code verifier needed to be sent while fetching # authorization URL. def self.generate_code_verifier random_number = rand 32..96 SecureRandom.alphanumeric random_number end # Returns the principal identifier for this authorizer # The client ID is used as the principal for user authorizers # # @private # @return [String] The client ID associated with this authorizer def principal @client_id.id end private # @private Fetch stored token with given user_id # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @return [String] The saved token from @token_store # @raise [Google::Auth::InitializationError] # If user_id is nil or token_store is nil def stored_token user_id raise InitializationError, NIL_USER_ID_ERROR if user_id.nil? raise InitializationError, NIL_TOKEN_STORE_ERROR if @token_store.nil? @token_store.load user_id end # Begin watching a credential for refreshes so the access token can be # saved. # # @param [String] user_id # Unique ID of the user for loading/storing credentials. # @param [Google::Auth::UserRefreshCredentials] credentials # Credentials to store. def monitor_credentials user_id, credentials credentials.on_refresh do |cred| store_credentials user_id, cred end credentials end # Resolve the redirect uri against a base. # # @param [String] base_url # Absolute URL to resolve the callback against if necessary. # @return [String] # Redirect URI # @raise [Google::Auth::CredentialsError] # If the callback URI is relative and base_url is nil or not absolute def redirect_uri_for base_url return @callback_uri if uri_is_postmessage?(@callback_uri) || !URI(@callback_uri).scheme.nil? if base_url.nil? || URI(base_url).scheme.nil? raise CredentialsError.with_details( format(MISSING_ABSOLUTE_URL_ERROR, @callback_uri), credential_type_name: self.class.name, principal: principal ) end URI.join(base_url, @callback_uri).to_s end # Check if URI is Google's postmessage flow (not a valid redirect_uri by spec, but allowed) def uri_is_postmessage? uri uri.to_s.casecmp("postmessage").zero? end def generate_code_challenge code_verifier digest = Digest::SHA256.digest code_verifier Base64.urlsafe_encode64 digest, padding: false end def code_challenge_method "S256" end end end end googleauth-1.16.1/lib/googleauth/default_credentials.rb0000644000004100000410000001353215135672542023236 0ustar www-datawww-data# Copyright 2015 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 "multi_json" require "stringio" require "googleauth/credentials_loader" require "googleauth/errors" require "googleauth/external_account" require "googleauth/service_account" require "googleauth/service_account_jwt_header" require "googleauth/user_refresh" require "googleauth/impersonated_service_account" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # DefaultCredentials is used to preload the credentials file, to determine # which type of credentials should be loaded. class DefaultCredentials extend CredentialsLoader ## # Override CredentialsLoader#make_creds to use the class determined by # loading the json. # # **Important:** If you accept a credential configuration (credential # JSON/File/Stream) from an external source for authentication to Google # Cloud, you must validate it before providing it to any Google API or # library. Providing an unvalidated credential configuration to Google # APIs can compromise the security of your systems and data. For more # information, refer to [Validate credential configurations from external # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). # # @deprecated This method is deprecated and will be removed in a future version. # Please use the `make_creds` method on the specific credential class you intend to load, # e.g., `Google::Auth::ServiceAccountCredentials.make_creds`. # # This method does not validate the credential configuration. The security # risk occurs when a credential configuration is accepted from a source that # is not under your control and used without validation on your side. # # If you know that you will be loading credential configurations of a # specific type, it is recommended to use a credential-type-specific # `make_creds` method. # This will ensure that an unexpected credential type with potential for # malicious intent is not loaded unintentionally. You might still have to do # validation for certain credential types. Please follow the recommendation # for that method. For example, if you want to load only service accounts, # you can use: # ``` # creds = Google::Auth::ServiceAccountCredentials.make_creds # ``` # @see Google::Auth::ServiceAccountCredentials.make_creds # # If you are loading your credential configuration from an untrusted source and have # not mitigated the risks (e.g. by validating the configuration yourself), make # these changes as soon as possible to prevent security risks to your environment. # # Regardless of the method used, it is always your responsibility to validate # configurations received from external sources. # # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials for more details. # # @param options [Hash] Options for creating the credentials # @return [Google::Auth::Credentials] The credentials instance # @raise [Google::Auth::InitializationError] If the credentials cannot be determined def self.make_creds options = {} json_key_io = options[:json_key_io] json_key, clz = determine_creds_class json_key_io if json_key io = StringIO.new MultiJson.dump(json_key) clz.make_creds options.merge(json_key_io: io) else clz.make_creds options end end # Reads the input json and determines which creds class to use. # # @param json_key_io [IO, nil] An optional IO object containing the JSON key. # If nil, the credential type is determined from environment variables. # @return [Array(Hash, Class)] The JSON key (or nil if from environment) and the credential class to use # @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type, # or if the environment variable is undefined or unsupported. def self.determine_creds_class json_key_io = nil if json_key_io json_key = MultiJson.load json_key_io.read key = "type" raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key type = json_key[key] else env_var = CredentialsLoader::ACCOUNT_TYPE_VAR type = ENV[env_var] raise InitializationError, "#{env_var} is undefined in env" unless type json_key = nil end clz = case type when ServiceAccountCredentials::CREDENTIAL_TYPE_NAME ServiceAccountCredentials when UserRefreshCredentials::CREDENTIAL_TYPE_NAME UserRefreshCredentials when ExternalAccount::Credentials::CREDENTIAL_TYPE_NAME ExternalAccount::Credentials when ImpersonatedServiceAccountCredentials::CREDENTIAL_TYPE_NAME ImpersonatedServiceAccountCredentials else raise InitializationError, "credentials type '#{type}' is not supported" end [json_key, clz] end end end end googleauth-1.16.1/lib/googleauth/version.rb0000644000004100000410000000136115135672542020717 0ustar www-datawww-data# Copyright 2014 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 Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth VERSION = "1.16.1".freeze end end googleauth-1.16.1/lib/googleauth/token_store.rb0000644000004100000410000000321415135672542021565 0ustar www-datawww-data# Copyright 2014 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 Google module Auth # Interface definition for token stores. It is not required that # implementations inherit from this class. It is provided for documentation # purposes to illustrate the API contract. class TokenStore class << self attr_accessor :default end # Load the token data from storage for the given ID. # # @param [String] id # ID of token data to load. # @return [String] # The loaded token data. def load _id raise NoMethodError, "load not implemented" end # Put the token data into storage for the given ID. # # @param [String] id # ID of token data to store. # @param [String] token # The token data to store. def store _id, _token raise NoMethodError, "store not implemented" end # Remove the token data from storage for the given ID. # # @param [String] id # ID of the token data to delete def delete _id raise NoMethodError, "delete not implemented" end end end end googleauth-1.16.1/lib/googleauth/id_tokens/0000755000004100000410000000000015135672542020663 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/id_tokens/key_sources.rb0000644000004100000410000003076115135672542023552 0ustar www-datawww-data# frozen_string_literal: true # Copyright 2020 Google LLC # # 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 "json" require "monitor" require "net/http" require "openssl" require "jwt" module Google module Auth module IDTokens ## # A public key used for verifying ID tokens. # # This includes the public key data, ID, and the algorithm used for # signature verification. RSA and Elliptical Curve (EC) keys are # supported. # class KeyInfo ## # Create a public key info structure. # # @param id [String] The key ID. # @param key [OpenSSL::PKey::RSA,OpenSSL::PKey::EC] The key itself. # @param algorithm [String] The algorithm (normally `RS256` or `ES256`) # def initialize id: nil, key: nil, algorithm: nil @id = id @key = key @algorithm = algorithm end ## # The key ID. # @return [String] # attr_reader :id ## # The key itself. # @return [OpenSSL::PKey::RSA,OpenSSL::PKey::EC] # attr_reader :key ## # The signature algorithm. (normally `RS256` or `ES256`) # @return [String] # attr_reader :algorithm class << self ## # Create a KeyInfo from a single JWK, which may be given as either a # hash or an unparsed JSON string. # # @param jwk [Hash,String] The JWK specification. # @return [KeyInfo] # @raise [Google::Auth::IDTokens::KeySourceError] If the key could not be extracted from the # JWK due to invalid type, malformed JSON, or invalid key data. # def from_jwk jwk jwk = symbolize_keys ensure_json_parsed jwk key = case jwk[:kty] when "RSA" extract_rsa_key jwk when "EC" extract_ec_key jwk when nil raise KeySourceError, "Key type not found" else raise KeySourceError, "Cannot use key type #{jwk[:kty]}" end new id: jwk[:kid], key: key, algorithm: jwk[:alg] end ## # Create an array of KeyInfo from a JWK Set, which may be given as # either a hash or an unparsed JSON string. # # @param jwk_set [Hash,String] The JWK Set specification. # @return [Array] # @raise [Google::Auth::IDTokens::KeySourceError] If a key could not be extracted from the # JWK Set, or if the set contains no keys. # def from_jwk_set jwk_set jwk_set = symbolize_keys ensure_json_parsed jwk_set jwks = jwk_set[:keys] raise KeySourceError, "No keys found in jwk set" unless jwks jwks.map { |jwk| from_jwk jwk } end private def ensure_json_parsed input return input unless input.is_a? String JSON.parse input rescue JSON::ParserError raise KeySourceError, "Unable to parse JSON" end def symbolize_keys hash result = {} hash.each { |key, val| result[key.to_sym] = val } result end def extract_rsa_key jwk begin n_data = Base64.urlsafe_decode64 jwk[:n] e_data = Base64.urlsafe_decode64 jwk[:e] rescue ArgumentError raise KeySourceError, "Badly formatted key data" end n_bn = OpenSSL::BN.new n_data, 2 e_bn = OpenSSL::BN.new e_data, 2 sequence = [OpenSSL::ASN1::Integer.new(n_bn), OpenSSL::ASN1::Integer.new(e_bn)] rsa_key = OpenSSL::PKey::RSA.new OpenSSL::ASN1::Sequence(sequence).to_der rsa_key.public_key end # @private CURVE_NAME_MAP = { "P-256" => "prime256v1", "P-384" => "secp384r1", "P-521" => "secp521r1", "secp256k1" => "secp256k1" }.freeze def extract_ec_key jwk begin x_data = Base64.urlsafe_decode64 jwk[:x] y_data = Base64.urlsafe_decode64 jwk[:y] rescue ArgumentError raise KeySourceError, "Badly formatted key data" end curve_name = CURVE_NAME_MAP[jwk[:crv]] raise KeySourceError, "Unsupported EC curve #{jwk[:crv]}" unless curve_name group = OpenSSL::PKey::EC::Group.new curve_name x_hex = x_data.unpack1 "H*" y_hex = y_data.unpack1 "H*" bn = OpenSSL::BN.new ["04#{x_hex}#{y_hex}"].pack("H*"), 2 point = OpenSSL::PKey::EC::Point.new group, bn sequence = OpenSSL::ASN1::Sequence([ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId("id-ecPublicKey"), OpenSSL::ASN1::ObjectId(curve_name)]), OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) ]) OpenSSL::PKey::EC.new sequence.to_der end end end ## # A key source that contains a static set of keys. # class StaticKeySource ## # Create a static key source with the given keys. # # @param keys [Array] The keys # def initialize keys @current_keys = Array(keys) end ## # Return the current keys. Does not perform any refresh. # # @return [Array] # attr_reader :current_keys alias refresh_keys current_keys class << self ## # Create a static key source containing a single key parsed from a # single JWK, which may be given as either a hash or an unparsed # JSON string. # # @param jwk [Hash,String] The JWK specification. # @return [StaticKeySource] # def from_jwk jwk new KeyInfo.from_jwk jwk end ## # Create a static key source containing multiple keys parsed from a # JWK Set, which may be given as either a hash or an unparsed JSON # string. # # @param jwk_set [Hash,String] The JWK Set specification. # @return [StaticKeySource] # def from_jwk_set jwk_set new KeyInfo.from_jwk_set jwk_set end end end ## # A base key source that downloads keys from a URI. Subclasses should # override {HttpKeySource#interpret_json} to parse the response. # class HttpKeySource ## # The default interval between retries in seconds (3600s = 1hr). # # @return [Integer] # DEFAULT_RETRY_INTERVAL = 3600 ## # Create an HTTP key source. # # @param uri [String,URI] The URI from which to download keys. # @param retry_interval [Integer,nil] Override the retry interval in # seconds. This is the minimum time between retries of failed key # downloads. # def initialize uri, retry_interval: nil @uri = URI uri @retry_interval = retry_interval || DEFAULT_RETRY_INTERVAL @allow_refresh_at = Time.now @current_keys = [] @monitor = Monitor.new end ## # The URI from which to download keys. # @return [Array] # attr_reader :uri ## # Return the current keys, without attempting to re-download. # # @return [Array] # attr_reader :current_keys ## # Attempt to re-download keys (if the retry interval has expired) and # return the new keys. # # @return [Array] # @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval fails, JSON parsing # fails, or the data cannot be interpreted as keys # def refresh_keys @monitor.synchronize do return @current_keys if Time.now < @allow_refresh_at @allow_refresh_at = Time.now + @retry_interval response = Net::HTTP.get_response uri raise KeySourceError, "Unable to retrieve data from #{uri}" unless response.is_a? Net::HTTPSuccess data = begin JSON.parse response.body rescue JSON::ParserError raise KeySourceError, "Unable to parse JSON" end @current_keys = Array(interpret_json(data)) end end protected def interpret_json _data nil end end ## # A key source that downloads X509 certificates. # Used by the legacy OAuth V1 public certs endpoint. # class X509CertHttpKeySource < HttpKeySource ## # Create a key source that downloads X509 certificates. # # @param uri [String,URI] The URI from which to download keys. # @param algorithm [String] The algorithm to use for signature # verification. Defaults to "`RS256`". # @param retry_interval [Integer,nil] Override the retry interval in # seconds. This is the minimum time between retries of failed key # downloads. # def initialize uri, algorithm: "RS256", retry_interval: nil super uri, retry_interval: retry_interval @algorithm = algorithm end protected # Interpret JSON data as X509 certificates # # @param data [Hash] The JSON data containing certificate strings # @return [Array] Array of key info objects # @raise [Google::Auth::IDTokens::KeySourceError] If X509 certificates cannot be parsed def interpret_json data data.map do |id, cert_str| key = OpenSSL::X509::Certificate.new(cert_str).public_key KeyInfo.new id: id, key: key, algorithm: @algorithm end rescue OpenSSL::X509::CertificateError raise KeySourceError, "Unable to parse X509 certificates" end end ## # A key source that downloads a JWK set. # class JwkHttpKeySource < HttpKeySource ## # Create a key source that downloads a JWT Set. # # @param uri [String,URI] The URI from which to download keys. # @param retry_interval [Integer,nil] Override the retry interval in # seconds. This is the minimum time between retries of failed key # downloads. # def initialize uri, retry_interval: nil super uri, retry_interval: retry_interval end protected def interpret_json data KeyInfo.from_jwk_set data end end ## # A key source that aggregates other key sources. This means it will # aggregate the keys provided by its constituent sources. Additionally, # when asked to refresh, it will refresh all its constituent sources. # class AggregateKeySource ## # Create a key source that aggregates other key sources. # # @param sources [Array] The key sources to aggregate. # def initialize sources @sources = Array(sources) end ## # Return the current keys, without attempting to refresh. # # @return [Array] # def current_keys @sources.flat_map(&:current_keys) end ## # Attempt to refresh keys and return the new keys. # # @return [Array] # @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval failed for any source. # def refresh_keys @sources.flat_map(&:refresh_keys) end end end end end googleauth-1.16.1/lib/googleauth/id_tokens/errors.rb0000644000004100000410000000325715135672542022533 0ustar www-datawww-data# frozen_string_literal: true # Copyright 2020 Google LLC # # 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 "googleauth/errors" module Google module Auth module IDTokens ## # Failed to obtain keys from the key source. # class KeySourceError < StandardError include Google::Auth::Error end ## # Failed to verify a token. # class VerificationError < StandardError include Google::Auth::Error end ## # Failed to verify token because it is expired. # class ExpiredTokenError < VerificationError; end ## # Failed to verify token because its signature did not match. # class SignatureError < VerificationError; end ## # Failed to verify token because its issuer did not match. # class IssuerMismatchError < VerificationError; end ## # Failed to verify token because its audience did not match. # class AudienceMismatchError < VerificationError; end ## # Failed to verify token because its authorized party did not match. # class AuthorizedPartyMismatchError < VerificationError; end end end end googleauth-1.16.1/lib/googleauth/id_tokens/verifier.rb0000644000004100000410000001127215135672542023026 0ustar www-datawww-data# frozen_string_literal: true # Copyright 2020 Google LLC # # 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 "jwt" module Google module Auth module IDTokens ## # An object that can verify ID tokens. # # A verifier maintains a set of default settings, including the key # source and fields to verify. However, individual verification calls can # override any of these settings. # class Verifier ## # Create a verifier. # # @param key_source [key source] The default key source to use. All # verification calls must have a key source, so if no default key # source is provided here, then calls to {#verify} _must_ provide # a key source. # @param aud [String,nil] The default audience (`aud`) check, or `nil` # for no check. # @param azp [String,nil] The default authorized party (`azp`) check, # or `nil` for no check. # @param iss [String,nil] The default issuer (`iss`) check, or `nil` # for no check. # def initialize key_source: nil, aud: nil, azp: nil, iss: nil @key_source = key_source @aud = aud @azp = azp @iss = iss end ## # Verify the given token. # # @param token [String] the ID token to verify. # @param key_source [key source] If given, override the key source. # @param aud [String,nil] If given, override the `aud` check. # @param azp [String,nil] If given, override the `azp` check. # @param iss [String,nil] If given, override the `iss` check. # # @return [Hash] the decoded payload, if verification succeeded. # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed. # Additional data may be available in the error subclass and message. def verify token, key_source: :default, aud: :default, azp: :default, iss: :default key_source = @key_source if key_source == :default aud = @aud if aud == :default azp = @azp if azp == :default iss = @iss if iss == :default raise KeySourceError, "No key sources" unless key_source keys = key_source.current_keys payload = decode_token token, keys, aud, azp, iss unless payload keys = key_source.refresh_keys payload = decode_token token, keys, aud, azp, iss end raise SignatureError, "Token not verified as issued by Google" unless payload payload end private def decode_token token, keys, aud, azp, iss payload = nil keys.find do |key| options = { algorithms: key.algorithm } decoded_token = JWT.decode token, key.key, true, options payload = decoded_token.first rescue JWT::ExpiredSignature raise ExpiredTokenError, "Token signature is expired" rescue JWT::DecodeError nil # Try the next key end normalize_and_verify_payload payload, aud, azp, iss end def normalize_and_verify_payload payload, aud, azp, iss return nil unless payload # Map the legacy "cid" claim to the canonical "azp" payload["azp"] ||= payload["cid"] if payload.key? "cid" # Payload content validation if aud && (Array(aud) & Array(payload["aud"])).empty? raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}" end if azp && (Array(azp) & Array(payload["azp"])).empty? raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}" end if iss && (Array(iss) & Array(payload["iss"])).empty? raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}" end payload end end end end end googleauth-1.16.1/lib/googleauth/service_account_jwt_header.rb0000644000004100000410000001510515135672542024603 0ustar www-datawww-data# Copyright 2025 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 "google/logging/message" require "googleauth/credentials_loader" require "googleauth/json_key_reader" require "jwt" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # Authenticates requests using Google's Service Account credentials via # JWT Header. # # This class allows authorizing requests for service accounts directly # from credentials from a json key file downloaded from the developer # console (via 'Generate new Json Key'). It is not part of any OAuth2 # flow, rather it creates a JWT and sends that as a credential. # # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production) class ServiceAccountJwtHeaderCredentials JWT_AUD_URI_KEY = :jwt_aud_uri AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze SIGNING_ALGORITHM = "RS256".freeze EXPIRY = 60 extend CredentialsLoader extend JsonKeyReader attr_reader :project_id attr_reader :quota_project_id attr_accessor :universe_domain attr_accessor :logger # Create a ServiceAccountJwtHeaderCredentials. # # @param json_key_io [IO] An IO object containing the JSON key # @param scope [string|array|nil] the scope(s) to access def self.make_creds options = {} json_key_io, scope = options.values_at :json_key_io, :scope new json_key_io: json_key_io, scope: scope end # Initializes a ServiceAccountJwtHeaderCredentials. # # @param json_key_io [IO] An IO object containing the JSON key def initialize options = {} json_key_io = options[:json_key_io] if json_key_io @private_key, @issuer, @project_id, @quota_project_id, @universe_domain = self.class.read_json_key json_key_io else @private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR] @issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR] @project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR] @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id @universe_domain = options[:universe_domain] if options.key? :universe_domain end @universe_domain ||= "googleapis.com" @project_id ||= CredentialsLoader.load_gcloud_project_id @signing_key = OpenSSL::PKey::RSA.new @private_key @scope = options[:scope] if options.key? :scope @logger = options[:logger] if options.key? :logger end # Creates a duplicate of these credentials # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized # * `private key` the private key in string form # * `issuer` the SA issuer # * `scope` the scope(s) to access # * `project_id` the project id to use during the authentication # * `quota_project_id` the quota project id to use # * `universe_domain` the universe domain of the credentials def duplicate options = {} options = deep_hash_normalize options options = { private_key: @private_key, issuer: @issuer, scope: @scope, project_id: project_id, quota_project_id: quota_project_id, universe_domain: universe_domain, logger: logger }.merge(options) self.class.new options end # Construct a jwt token if the JWT_AUD_URI key is present in the input # hash. # # The jwt token is used as the value of a 'Bearer '. def apply! a_hash, opts = {} jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY return a_hash if jwt_aud_uri.nil? && @scope.nil? jwt_token = new_jwt_token jwt_aud_uri, opts a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}" logger&.debug do hash = Digest::SHA256.hexdigest jwt_token Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})" end a_hash end # Returns a clone of a_hash updated with the authorization header def apply a_hash, opts = {} a_copy = a_hash.clone apply! a_copy, opts a_copy end # Returns a reference to the #apply method, suitable for passing as # a closure def updater_proc proc { |a_hash, opts = {}| apply a_hash, opts } end # Creates a jwt uri token. def new_jwt_token jwt_aud_uri = nil, options = {} now = Time.new skew = options[:skew] || 60 assertion = { "iss" => @issuer, "sub" => @issuer, "exp" => (now + EXPIRY).to_i, "iat" => (now - skew).to_i } jwt_aud_uri = nil if @scope assertion["scope"] = Array(@scope).join " " if @scope assertion["aud"] = jwt_aud_uri if jwt_aud_uri logger&.debug do Google::Logging::Message.from message: "JWT assertion: #{assertion}" end JWT.encode assertion, @signing_key, SIGNING_ALGORITHM end # Duck-types the corresponding method from BaseClient def needs_access_token? false end # Returns the client email as the principal for service account JWT header credentials # @private # @return [String] the email address of the service account def principal @issuer end private 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 # 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 end end end googleauth-1.16.1/lib/googleauth/credentials.rb0000644000004100000410000006405615135672542021541 0ustar www-datawww-data# Copyright 2017 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 "forwardable" require "json" require "pathname" require "signet/oauth_2/client" require "multi_json" require "googleauth/credentials_loader" require "googleauth/errors" module Google module Auth ## # Credentials is a high-level base class used by Google's API client # libraries to represent the authentication when connecting to an API. # In most cases, it is subclassed by API-specific credential classes that # can be instantiated by clients. # # **Important:** If you accept a credential configuration (credential # JSON/File/Stream) from an external source for authentication to Google # Cloud, you must validate it before providing it to any Google API or # library. Providing an unvalidated credential configuration to Google APIs # can compromise the security of your systems and data. For more # information, refer to [Validate credential configurations from external # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). # # ## Options # # Credentials classes are configured with options that dictate default # values for parameters such as scope and audience. These defaults are # expressed as class attributes, and may differ from endpoint to endpoint. # Normally, an API client will provide subclasses specific to each # endpoint, configured with appropriate values. # # Note that these options inherit up the class hierarchy. If a particular # options is not set for a subclass, its superclass is queried. # # Some older users of this class set options via constants. This usage is # deprecated. For example, instead of setting the `AUDIENCE` constant on # your subclass, call the `audience=` method. # # ## Example # # class MyCredentials < Google::Auth::Credentials # # Set the default scope for these credentials # self.scope = "http://example.com/my_scope" # end # # # creds is a credentials object suitable for Google API clients # creds = MyCredentials.default # creds.scope # => ["http://example.com/my_scope"] # # class SubCredentials < MyCredentials # # Override the default scope for this subclass # self.scope = "http://example.com/sub_scope" # end # # creds2 = SubCredentials.default # creds2.scope # => ["http://example.com/sub_scope"] # class Credentials # rubocop:disable Metrics/ClassLength ## # The default token credential URI to be used when none is provided during initialization. TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze ## # The default target audience ID to be used when none is provided during initialization. AUDIENCE = "https://oauth2.googleapis.com/token".freeze @audience = @scope = @target_audience = @env_vars = @paths = @token_credential_uri = nil ## # The default token credential URI to be used when none is provided during initialization. # The URI is the authorization server's HTTP endpoint capable of issuing tokens and # refreshing expired tokens. # # @return [String] # def self.token_credential_uri lookup_auth_param :token_credential_uri do lookup_local_constant :TOKEN_CREDENTIAL_URI end end ## # Set the default token credential URI to be used when none is provided during initialization. # # @param [String] new_token_credential_uri # def self.token_credential_uri= new_token_credential_uri @token_credential_uri = new_token_credential_uri end ## # The default target audience ID to be used when none is provided during initialization. # Used only by the assertion grant type. # # @return [String] # def self.audience lookup_auth_param :audience do lookup_local_constant :AUDIENCE end end ## # Sets the default target audience ID to be used when none is provided during initialization. # # @param [String] new_audience # def self.audience= new_audience @audience = new_audience end ## # The default scope to be used when none is provided during initialization. # A scope is an access range defined by the authorization server. # The scope can be a single value or a list of values. # # Either {#scope} or {#target_audience}, but not both, should be non-nil. # If {#scope} is set, this credential will produce access tokens. # If {#target_audience} is set, this credential will produce ID tokens. # # @return [String, Array, nil] # def self.scope lookup_auth_param :scope do vals = lookup_local_constant :SCOPE vals ? Array(vals).flatten.uniq : nil end end ## # Sets the default scope to be used when none is provided during initialization. # # Either {#scope} or {#target_audience}, but not both, should be non-nil. # If {#scope} is set, this credential will produce access tokens. # If {#target_audience} is set, this credential will produce ID tokens. # # @param [String, Array, nil] new_scope # def self.scope= new_scope new_scope = Array new_scope unless new_scope.nil? @scope = new_scope end ## # The default final target audience for ID tokens, to be used when none # is provided during initialization. # # Either {#scope} or {#target_audience}, but not both, should be non-nil. # If {#scope} is set, this credential will produce access tokens. # If {#target_audience} is set, this credential will produce ID tokens. # # @return [String, nil] # def self.target_audience lookup_auth_param :target_audience end ## # Sets the default final target audience for ID tokens, to be used when none # is provided during initialization. # # Either {#scope} or {#target_audience}, but not both, should be non-nil. # If {#scope} is set, this credential will produce access tokens. # If {#target_audience} is set, this credential will produce ID tokens. # # @param [String, nil] new_target_audience # def self.target_audience= new_target_audience @target_audience = new_target_audience end ## # The environment variables to search for credentials. Values can either be a file path to the # credentials file, or the JSON contents of the credentials file. # The env_vars will never be nil. If there are no vars, the empty array is returned. # # @return [Array] # def self.env_vars env_vars_internal || [] end ## # @private # Internal recursive lookup for env_vars. # def self.env_vars_internal lookup_auth_param :env_vars, :env_vars_internal do # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists. path_env_vars = lookup_local_constant :PATH_ENV_VARS json_env_vars = lookup_local_constant :JSON_ENV_VARS (Array(path_env_vars) + Array(json_env_vars)).flatten.uniq if path_env_vars || json_env_vars end end ## # Sets the environment variables to search for credentials. # Setting to `nil` "unsets" the value, and defaults to the superclass # (or to the empty array if there is no superclass). # # @param [String, Array, nil] new_env_vars # def self.env_vars= new_env_vars new_env_vars = Array new_env_vars unless new_env_vars.nil? @env_vars = new_env_vars end ## # The file paths to search for credentials files. # The paths will never be nil. If there are no paths, the empty array is returned. # # @return [Array] # def self.paths paths_internal || [] end ## # @private # Internal recursive lookup for paths. # def self.paths_internal lookup_auth_param :paths, :paths_internal do # Pull in values if the DEFAULT_PATHS constant exists. vals = lookup_local_constant :DEFAULT_PATHS vals ? Array(vals).flatten.uniq : nil end end ## # Set the file paths to search for credentials files. # Setting to `nil` "unsets" the value, and defaults to the superclass # (or to the empty array if there is no superclass). # # @param [String, Array, nil] new_paths # def self.paths= new_paths new_paths = Array new_paths unless new_paths.nil? @paths = new_paths end ## # @private # Return the given parameter value, defaulting up the class hierarchy. # # First returns the value of the instance variable, if set. # Next, calls the given block if provided. (This is generally used to # look up legacy constant-based values.) # Otherwise, calls the superclass method if present. # Returns nil if all steps fail. # # @param name [Symbol] The parameter name # @param method_name [Symbol] The lookup method name, if different # @return [Object] The value # def self.lookup_auth_param name, method_name = name val = instance_variable_get :"@#{name}" val = yield if val.nil? && block_given? return val unless val.nil? return superclass.send method_name if superclass.respond_to? method_name nil end ## # @private # Return the value of the given constant if it is defined directly in # this class, or nil if not. # # @param [Symbol] Name of the constant # @return [Object] The value # def self.lookup_local_constant name const_defined?(name, false) ? const_get(name) : nil end ## # The Signet::OAuth2::Client object the Credentials instance is using. # # @return [Signet::OAuth2::Client] # attr_accessor :client ## # Identifier for the project the client is authenticating with. # # @return [String] # attr_reader :project_id ## # Identifier for a separate project used for billing/quota, if any. # # @return [String,nil] # attr_reader :quota_project_id # @private Temporary; remove when universe domain metadata endpoint is stable (see b/349488459). def disable_universe_domain_check return false unless @client.respond_to? :disable_universe_domain_check @client.disable_universe_domain_check end # @private Delegate client methods to the client object. extend Forwardable ## # @!attribute [r] token_credential_uri # @return [String] The token credential URI. The URI is the authorization server's HTTP # endpoint capable of issuing tokens and refreshing expired tokens. # # @!attribute [r] audience # @return [String] The target audience ID when issuing assertions. Used only by the # assertion grant type. # # @!attribute [r] scope # @return [String, Array] The scope for this client. A scope is an access range # defined by the authorization server. The scope can be a single value or a list of values. # # @!attribute [r] issuer # @return [String] The issuer ID associated with this client. # # @!attribute [r] signing_key # @return [String, OpenSSL::PKey] The signing key associated with this client. # # @!attribute [r] updater_proc # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method, # suitable for passing as a closure. # # @!attribute [r] target_audience # @return [String] The final target audience for ID tokens returned by this credential. # # @!attribute [rw] universe_domain # @return [String] The universe domain issuing these credentials. # # @!attribute [rw] logger # @return [Logger] The logger used to log credential operations such as token refresh. # def_delegators :@client, :token_credential_uri, :audience, :scope, :issuer, :signing_key, :updater_proc, :target_audience, :universe_domain, :universe_domain=, :logger, :logger= ## # Creates a new Credentials instance with the provided auth credentials, and with the default # values configured on the class. # # @param [String, Pathname, Hash, Google::Auth::BaseClient] source_creds # The source of credentials. It can be provided as one of the following: # # * The path to a JSON keyfile (as a `String` or a `Pathname`) # * The contents of a JSON keyfile (as a `Hash`) # * A `Google::Auth::BaseClient` credentials object, including but not limited to # a `Signet::OAuth2::Client` object. # * Any credentials object that supports the methods this wrapper delegates to an inner client. # # If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client. # Otherwise the inner client will be constructed from the JSON keyfile or the contens of the hash. # # @param [Hash] options # The options for configuring this wrapper credentials object and the inner client. # The options hash is used in two ways: # # 1. **Configuring the wrapper object:** Some options are used to directly # configure the wrapper `Credentials` instance. These include: # # * `:project_id` (and optionally `:project`) - the project identifier for the client # * `:quota_project_id` - the quota project identifier for the client # * `:logger` - the logger used to log credential operations such as token refresh. # # 2. **Configuring the inner client:** When the `source_creds` parameter # is a `String` or `Hash`, a new `Signet::OAuth2::Client` is created # internally. The following options are used to configure this inner client: # # * `:scope` - the scope for the client # * `:target_audience` - the target audience for the client # # Any other options in the `options` hash are passed directly to the # inner client constructor. This allows you to configure additional # parameters of the `Signet::OAuth2::Client`, such as connection parameters, # timeouts, etc. # # @raise [Google::Auth::InitializationError] If source_creds is nil # @raise [ArgumentError] If both scope and target_audience are specified # def initialize source_creds, options = {} if source_creds.nil? raise InitializationError, "The source credentials passed to Google::Auth::Credentials.new were nil." end options = symbolize_hash_keys options @project_id = options[:project_id] || options[:project] @quota_project_id = options[:quota_project_id] case source_creds when String, Pathname update_from_filepath source_creds, options when Hash update_from_hash source_creds, options else update_from_client source_creds end setup_logging logger: options.fetch(:logger, :default) @project_id ||= CredentialsLoader.load_gcloud_project_id @env_vars = nil @paths = nil @scope = nil end ## # Creates a new Credentials instance with auth credentials acquired by searching the # environment variables and paths configured on the class, and with the default values # configured on the class. # # The auth credentials are searched for in the following order: # # 1. configured environment variables (see {Credentials.env_vars}) # 2. configured default file paths (see {Credentials.paths}) # 3. application default (see {Google::Auth.get_application_default}) # # @param [Hash] options # The options for configuring the credentials instance. The following is supported: # # * +:scope+ - the scope for the client # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client # * +:connection_builder+ - the connection builder to use for the client # * +:default_connection+ - the default connection to use for the client # # @return [Credentials] # def self.default options = {} # First try to find keyfile file or json from environment variables. client = from_env_vars options # Second try to find keyfile file from known file paths. client ||= from_default_paths options # Finally get instantiated client from Google::Auth client ||= from_application_default options client end ## # @private Lookup Credentials from environment variables. def self.from_env_vars options env_vars.each do |env_var| str = ENV[env_var] next if str.nil? io = if ::File.file? str ::StringIO.new ::File.read str else json = ::JSON.parse str rescue nil json ? ::StringIO.new(str) : nil end next if io.nil? return from_io io, options end nil end ## # @private Lookup Credentials from default file paths. def self.from_default_paths options paths.each do |path| next unless path && ::File.file?(path) io = ::StringIO.new ::File.read path return from_io io, options end nil end ## # @private Lookup Credentials using Google::Auth.get_application_default. def self.from_application_default options scope = options[:scope] || self.scope auth_opts = { token_credential_uri: options[:token_credential_uri] || token_credential_uri, audience: options[:audience] || audience, target_audience: options[:target_audience] || target_audience, enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil? } client = Google::Auth.get_application_default scope, auth_opts new client, options end # @private Read credentials from a JSON stream. def self.from_io io, options creds_input = { json_key_io: io, scope: options[:scope] || scope, target_audience: options[:target_audience] || target_audience, enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?, token_credential_uri: options[:token_credential_uri] || token_credential_uri, audience: options[:audience] || audience } # Determine the class, which consumes the IO stream json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class creds_input[:json_key_io] # Re-serialize the parsed JSON and replace the IO stream in creds_input creds_input[:json_key_io] = StringIO.new MultiJson.dump(json_key) client = clz.make_creds creds_input options = options.select { |k, _v| k == :logger } new client, options end # @private # Initializes the Signet client. def self.init_client hash, options = {} options = update_client_options options io = StringIO.new JSON.generate hash # Determine the class, which consumes the IO stream json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class io # Re-serialize the parsed JSON and create a new IO stream. new_io = StringIO.new MultiJson.dump(json_key) clz.make_creds options.merge!(json_key_io: new_io) end # @private # Updates client options with defaults from the credential class # # @param [Hash] options Options to update # @return [Hash] Updated options hash # @raise [ArgumentError] If both scope and target_audience are specified def self.update_client_options options options = options.dup # options have higher priority over constructor defaults options[:token_credential_uri] ||= token_credential_uri options[:audience] ||= audience options[:scope] ||= scope options[:target_audience] ||= target_audience if !Array(options[:scope]).empty? && options[:target_audience] raise ArgumentError, "Cannot specify both scope and target_audience" end options.delete :scope unless options[:target_audience].nil? options end private_class_method :from_env_vars, :from_default_paths, :from_application_default, :from_io # Creates a duplicate of these credentials. This method tries to create the duplicate of the # wrapped credentials if they support duplication and use them as is if they don't. # # The wrapped credentials are typically `Signet::OAuth2::Client` objects and they keep # the transient state (token, refresh token, etc). The duplication discards that state, # allowing e.g. to get the token with a different scope. # # @param options [Hash] Overrides for the credentials parameters. # # The options hash is used in two ways: # # 1. **Configuring the duplicate of the wrapper object:** Some options are used to directly # configure the wrapper `Credentials` instance. These include: # # * `:project_id` (and optionally `:project`) - the project identifier for the credentials # * `:quota_project_id` - the quota project identifier for the credentials # # 2. **Configuring the duplicate of the inner client:** If the inner client supports duplication # the options hash is passed to it. This allows for configuration of additional parameters, # most importantly (but not limited to) the following: # # * `:scope` - the scope for the client # # @return [Credentials] def duplicate options = {} options = deep_hash_normalize options options = { project_id: @project_id, quota_project_id: @quota_project_id }.merge(options) new_client = if @client.respond_to? :duplicate @client.duplicate options else @client end self.class.new new_client, options end protected # Verify that the keyfile argument is a file. # # @param [String] keyfile Path to the keyfile # @raise [Google::Auth::InitializationError] If the keyfile does not exist def verify_keyfile_exists! keyfile exists = ::File.file? keyfile raise InitializationError, "The keyfile '#{keyfile}' is not a valid file." unless exists end # returns a new Hash with string keys instead of symbol keys. def stringify_hash_keys hash hash.to_h.transform_keys(&:to_s) end # returns a new Hash with symbol keys instead of string keys. def symbolize_hash_keys hash hash.to_h.transform_keys(&:to_sym) end def update_from_client client @project_id ||= client.project_id if client.respond_to? :project_id @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id @client = client end alias update_from_signet update_from_client def update_from_hash hash, options hash = stringify_hash_keys hash hash["scope"] ||= options[:scope] hash["target_audience"] ||= options[:target_audience] @project_id ||= hash["project_id"] || hash["project"] @quota_project_id ||= hash["quota_project_id"] @client = self.class.init_client hash, options end def update_from_filepath path, options verify_keyfile_exists! path json = JSON.parse ::File.read(path) json["scope"] ||= options[:scope] json["target_audience"] ||= options[:target_audience] @project_id ||= json["project_id"] || json["project"] @quota_project_id ||= json["quota_project_id"] @client = self.class.init_client json, options end def setup_logging logger: :default return unless @client.respond_to? :logger= logging_env = ENV["GOOGLE_SDK_RUBY_LOGGING_GEMS"].to_s.downcase if ["false", "none"].include? logging_env logger = nil elsif @client.logger logger = @client.logger elsif logger == :default logger = nil if ["true", "all"].include?(logging_env) || logging_env.split(",").include?("googleauth") formatter = Google::Logging::StructuredFormatter.new if Google::Cloud::Env.get.logging_agent_expected? logger = Logger.new $stderr, progname: "googleauth", formatter: formatter end end @client.logger = logger end private # 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 end end end googleauth-1.16.1/lib/googleauth/errors.rb0000644000004100000410000000762715135672542020561 0ustar www-datawww-data# frozen_string_literal: true require "signet/oauth_2/client" # Copyright 2025 Google LLC # # 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 Google module Auth ## # Error mixin module for Google Auth errors # All Google Auth errors should include this module # module Error; end ## # Mixin module that contains detailed error information # typically this is available if credentials initialization # succeeds and credentials object is valid # module DetailedError include Error # The type of the credentials that the error was originated from # @return [String, nil] The class name of the credential that raised the error attr_reader :credential_type_name # The principal for the authentication flow. Typically obtained from credentials # @return [String, Symbol, nil] The principal identifier associated with the credentials attr_reader :principal # All details passed in the options hash when creating the error # @return [Hash] Additional details about the error attr_reader :details # @private def self.included base base.extend ClassMethods end # Class methods to be added to including classes module ClassMethods # Creates a new error with detailed information # @param message [String] The error message # @param credential_type_name [String] The credential type that raised the error # @param principal [String, Symbol] The principal for the authentication flow # @return [Error] The new error with details def with_details message, credential_type_name:, principal: new(message).tap do |error| error.instance_variable_set :@credential_type_name, credential_type_name error.instance_variable_set :@principal, principal end end end end ## # Error raised during Credentials initialization. # All new code should use this instead of ArgumentError during initializtion. # class InitializationError < StandardError include Error end ## # Generic error raised during operation of Credentials # This should be used for all purposes not covered by other errors. # class CredentialsError < StandardError include DetailedError end ## # An error indicating the remote server refused to authorize the client. # Maintains backward compatibility with Signet. # # Should not be used in the new code, even when wrapping `Signet::AuthorizationError`. # New code should use CredentialsError instead. # class AuthorizationError < Signet::AuthorizationError include DetailedError end ## # An error indicating that the server sent an unexpected http status. # Maintains backward compatibility with Signet. # # Should not be used in the new code, even when wrapping `Signet::UnexpectedStatusError`. # New code should use CredentialsError instead. # class UnexpectedStatusError < Signet::UnexpectedStatusError include DetailedError end ## # An error indicating the client failed to parse a value. # Maintains backward compatibility with Signet. # # Should not be used in the new code, even when wrapping `Signet::ParseError`. # New code should use CredentialsError instead. # class ParseError < Signet::ParseError include DetailedError end end end googleauth-1.16.1/lib/googleauth/impersonated_service_account.rb0000644000004100000410000004103015135672542025155 0ustar www-datawww-data# Copyright 2024 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 "googleauth/base_client" require "googleauth/errors" require "googleauth/helpers/connection" module Google module Auth # Authenticates requests using impersonation from base credentials. # This is a two-step process: first authentication claim from the base credentials is created # and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint. # The short-lived token and its expiration time are cached. class ImpersonatedServiceAccountCredentials # @private CREDENTIAL_TYPE_NAME = "impersonated_service_account".freeze # @private ERROR_SUFFIX = <<~ERROR.freeze when trying to get security access token from IAM Credentials endpoint using the credentials provided. ERROR # @private IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze # BaseClient most importantly implements the `:updater_proc` getter, # that returns a reference to an `apply!` method that updates # a hash argument provided with the authorization header containing # the access token (impersonation token in this case). include Google::Auth::BaseClient include Helpers::Connection # @return [Object] The original authenticated credentials used to fetch short-lived impersonation access tokens attr_reader :base_credentials # @return [Object] The modified version of base credentials, tailored for impersonation purposes # with necessary scope adjustments attr_reader :source_credentials # @return [String] The URL endpoint used to generate an impersonation token. This URL should follow a specific # format to specify the impersonated service account. attr_reader :impersonation_url # @return [Array, String] The scope(s) required for the impersonated access token, # indicating the permissions needed for the short-lived token attr_reader :scope # @return [String, nil] The short-lived impersonation access token, retrieved and cached # after making the impersonation request attr_reader :access_token # @return [Time, nil] The expiration time of the current access token, used to determine # if the token is still valid attr_reader :expires_at # Create a ImpersonatedServiceAccountCredentials # When you use service account impersonation, you start with an authenticated principal # (e.g. your user account or a service account) # and request short-lived credentials for a service account # that has the authorization that your use case requires. # # @note Warning: # This method does not validate the credential configuration. A security # risk occurs when a credential configuration configured with malicious urls # is used. # When the credential configuration is accepted from an # untrusted source, you should validate it before using with this method. # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials # for more details. # # @param options [Hash] A hash of options to configure the credentials. # @option options [Object] :base_credentials (required) The authenticated principal. # It will be used as following: # * will be duplicated (with IAM scope) to create the source credentials if it supports duplication # * as source credentials otherwise. # @option options [String] :impersonation_url (required) The URL to impersonate the service account. # This URL should follow the format: # `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`, # where: # - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`). # - `{source_sa_email}` is the email address of the service account to impersonate. # @option options [Array, String] :scope (required) The scope(s) for the short-lived impersonation token, # defining the permissions required for the token. # @option options [Object] :source_credentials The authenticated principal that will be used # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials. # @option options [IO] :json_key_io The IO object that contains the credential configuration. # It is exclusive with `:base_credentials` and `:source_credentials` options. # # @return [Google::Auth::ImpersonatedServiceAccountCredentials] def self.make_creds options = {} if options[:json_key_io] make_creds_from_json options else new options end end # @private def self.make_creds_from_json options json_key_io = options[:json_key_io] if options[:base_credentials] || options[:source_credentials] raise Google::Auth::InitializationError, "json_key_io is not compatible with base_credentials or source_credentials" end require "googleauth/default_credentials" impersonated_json = MultiJson.load json_key_io.read source_credentials_info = impersonated_json["source_credentials"] if source_credentials_info["type"] == CREDENTIAL_TYPE_NAME raise Google::Auth::InitializationError, "Source credentials can't be of type impersonated_service_account, " \ "use delegates to chain impersonation." end source_credentials = DefaultCredentials.make_creds( json_key_io: StringIO.new(MultiJson.dump(source_credentials_info)) ) impersonation_url = impersonated_json["service_account_impersonation_url"] scope = options[:scope] || impersonated_json["scopes"] new( source_credentials: source_credentials, impersonation_url: impersonation_url, scope: scope ) end private_class_method :make_creds_from_json # Initializes a new instance of ImpersonatedServiceAccountCredentials. # # @param options [Hash] A hash of options to configure the credentials. # @option options [Object] :base_credentials (required) The authenticated principal. # It will be used as following: # * will be duplicated (with IAM scope) to create the source credentials if it supports duplication # * as source credentials otherwise. # @option options [String] :impersonation_url (required) The URL to impersonate the service account. # This URL should follow the format: # `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`, # where: # - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`). # - `{source_sa_email}` is the email address of the service account to impersonate. # @option options [Array, String] :scope (required) The scope(s) for the short-lived impersonation token, # defining the permissions required for the token. # It will override the scope from the `json_key_io` file if provided. # @option options [Object] :source_credentials The authenticated principal that will be used # to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials. # It is redundant to provide both source and base credentials as only source will be used, # but it can be done, e.g. when duplicating existing credentials. # # @raise [ArgumentError] If any of the required options are missing. # # @return [Google::Auth::ImpersonatedServiceAccountCredentials] def initialize options = {} @base_credentials, @impersonation_url, @scope = options.values_at :base_credentials, :impersonation_url, :scope # Fail-fast checks for required parameters if @base_credentials.nil? && !options.key?(:source_credentials) raise ArgumentError, "Missing required option: either :base_credentials or :source_credentials" end raise ArgumentError, "Missing required option: :impersonation_url" if @impersonation_url.nil? raise ArgumentError, "Missing required option: :scope" if @scope.nil? # Some credentials (all Signet-based ones and this one) include scope and a bunch of transient state # (e.g. refresh status) as part of themselves # so a copy needs to be created with the scope overriden and transient state dropped. # # If a credentials does not support `duplicate` we'll try to use it as is assuming it has a broad enough scope. # This might result in an "access denied" error downstream when the token from that credentials is being used # for the token exchange. @source_credentials = if options.key? :source_credentials options[:source_credentials] elsif @base_credentials.respond_to? :duplicate @base_credentials.duplicate({ scope: IAM_SCOPE }) else @base_credentials end end # Determines whether the current access token expires within the specified number of seconds. # # @param seconds [Integer] The number of seconds to check against the token's expiration time. # # @return [Boolean] Whether the access token expires within the given time frame def expires_within? seconds # This method is needed for BaseClient @expires_at && @expires_at - Time.now.utc < seconds end # The universe domain of the impersonated credentials. # Effectively this retrieves the universe domain of the source credentials. # # @return [String] The universe domain of the credentials. def universe_domain @source_credentials.universe_domain end # @return [Logger, nil] The logger of the credentials. def logger @source_credentials.logger if source_credentials.respond_to? :logger end # Creates a duplicate of these credentials without transient token state # # @param options [Hash] Overrides for the credentials parameters. # The following keys are recognized # * `base_credentials` the base credentials used to initialize the impersonation # * `source_credentials` the authenticated credentials which usually would be # base credentials with scope overridden to IAM_SCOPE # * `impersonation_url` the URL to use to make an impersonation token exchange # * `scope` the scope(s) to access # # @return [Google::Auth::ImpersonatedServiceAccountCredentials] def duplicate options = {} options = deep_hash_normalize options options = { base_credentials: @base_credentials, source_credentials: @source_credentials, impersonation_url: @impersonation_url, scope: @scope }.merge(options) self.class.new options end # The principal behind the credentials. This class allows custom source credentials type # that might not implement `principal`, in which case `:unknown` is returned. # # @private # @return [String, Symbol] The string representation of the principal, # the token type in lieu of the principal, or :unknown if source principal is unknown. def principal if @source_credentials.respond_to? :principal @source_credentials.principal else :unknown end end private # Generates a new impersonation access token by exchanging the source credentials' token # at the impersonation URL. # # This method first fetches an access token from the source credentials and then exchanges it # for an impersonation token using the specified impersonation URL. The generated token and # its expiration time are cached for subsequent use. # # @private # @param _options [Hash] (optional) Additional options for token retrieval (currently unused). # # @raise [Google::Auth::UnexpectedStatusError] If the response status is 403 or 500. # @raise [Google::Auth::AuthorizationError] For other unexpected response statuses. # # @return [String] The newly generated impersonation access token. def fetch_access_token! _options = {} auth_header = prepare_auth_header resp = make_impersonation_request auth_header case resp.status when 200 response = MultiJson.load resp.body self.expires_at = response["expireTime"] @access_token = response["accessToken"] access_token when 403, 500 handle_error_response resp, UnexpectedStatusError else handle_error_response resp, AuthorizationError end end # Prepares the authorization header for the impersonation request # by fetching a token from source credentials. # # @private # @return [Hash] The authorization header with the source credentials' token def prepare_auth_header auth_header = {} @source_credentials.updater_proc.call auth_header auth_header end # Makes the HTTP request to the impersonation endpoint. # # @private # @param [Hash] auth_header The authorization header containing the source token # @return [Faraday::Response] The HTTP response from the impersonation endpoint def make_impersonation_request auth_header connection.post @impersonation_url do |req| req.headers.merge! auth_header req.headers["Content-Type"] = "application/json" req.body = MultiJson.dump({ scope: @scope }) end end # Creates and raises an appropriate error based on the response. # # @private # @param [Faraday::Response] resp The HTTP response # @param [Class] error_class The error class to instantiate # @raise [StandardError] The appropriate error with details def handle_error_response resp, error_class msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}" raise error_class.with_details( msg, credential_type_name: self.class.name, principal: principal ) end # Setter for the expires_at value that makes sure it is converted # to Time object. def expires_at= new_expires_at @expires_at = normalize_timestamp new_expires_at end # Returns the type of token (access_token). # This method is needed for BaseClient. def token_type :access_token end # Normalizes a timestamp to a Time object. # # @param time [Time, String, nil] The timestamp to normalize. # # @return [Time, nil] The normalized Time object, or nil if the input is nil. # # @raise [Google::Auth::CredentialsError] If the input is not a Time, String, or nil. def normalize_timestamp time case time when NilClass nil when Time time when String Time.parse time else message = "Invalid time value #{time}" raise CredentialsError.with_details(message, credential_type_name: self.class.name, principal: principal) end 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 end end end googleauth-1.16.1/lib/googleauth/credentials_loader.rb0000644000004100000410000002137615135672542023065 0ustar www-datawww-data# Copyright 2015 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 "os" require "rbconfig" require "googleauth/errors" module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth # CredentialsLoader contains the behaviour used to locate and find default # credentials files on the file system. module CredentialsLoader ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS".freeze PRIVATE_KEY_VAR = "GOOGLE_PRIVATE_KEY".freeze CLIENT_EMAIL_VAR = "GOOGLE_CLIENT_EMAIL".freeze CLIENT_ID_VAR = "GOOGLE_CLIENT_ID".freeze CLIENT_SECRET_VAR = "GOOGLE_CLIENT_SECRET".freeze REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze AWS_REGION_VAR = "AWS_REGION".freeze AWS_DEFAULT_REGION_VAR = "AWS_DEFAULT_REGION".freeze AWS_ACCESS_KEY_ID_VAR = "AWS_ACCESS_KEY_ID".freeze AWS_SECRET_ACCESS_KEY_VAR = "AWS_SECRET_ACCESS_KEY".freeze AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze GCLOUD_POSIX_COMMAND = "gcloud".freeze GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none --quiet".freeze CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze WELL_KNOWN_PATH = "gcloud/#{CREDENTIALS_FILE_NAME}".freeze WELL_KNOWN_ERROR = "Unable to read the default credential file".freeze SYSTEM_DEFAULT_ERROR = "Unable to read the system default credential file".freeze CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \ "s.googleusercontent.com".freeze # make_creds proxies the construction of a credentials instance # # By default, it calls #new on the current class, but this behaviour can # be modified, allowing different instances to be created. def make_creds *args creds = new(*args) creds = creds.configure_connection args[0] if creds.respond_to?(:configure_connection) && args.size == 1 creds end # Creates an instance from the path specified in an environment # variable. # # @param scope [string|array|nil] the scope(s) to access # @param options [Hash] Connection options. These may be used to configure # how OAuth tokens are retrieved, by providing a suitable # `Faraday::Connection`. For example, if a connection proxy must be # used in the current network, you may provide a connection with # with the needed proxy options. # The following keys are recognized: # * `:default_connection` The connection object to use. # * `:connection_builder` A `Proc` that returns a connection. # @raise [Google::Auth::InitializationError] If the credentials file cannot be read def from_env scope = nil, options = {} options = interpret_options scope, options if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty? path = ENV[ENV_VAR] raise InitializationError, "file #{path} does not exist" unless File.exist? path File.open path do |f| return make_creds options.merge(json_key_io: f) end elsif service_account_env_vars? || authorized_user_env_vars? make_creds options end rescue StandardError => e raise InitializationError, "#{NOT_FOUND_ERROR}: #{e}" end # Creates an instance from a well known path. # # @param scope [string|array|nil] the scope(s) to access # @param options [Hash] Connection options. These may be used to configure # how OAuth tokens are retrieved, by providing a suitable # `Faraday::Connection`. For example, if a connection proxy must be # used in the current network, you may provide a connection with # with the needed proxy options. # The following keys are recognized: # * `:default_connection` The connection object to use. # * `:connection_builder` A `Proc` that returns a connection. # @raise [Google::Auth::InitializationError] If the credentials file cannot be read def from_well_known_path scope = nil, options = {} options = interpret_options scope, options home_var = OS.windows? ? "APPDATA" : "HOME" base = WELL_KNOWN_PATH root = ENV[home_var].nil? ? "" : ENV[home_var] base = File.join ".config", base unless OS.windows? path = File.join root, base return nil unless File.exist? path File.open path do |f| return make_creds options.merge(json_key_io: f) end rescue StandardError => e raise InitializationError, "#{WELL_KNOWN_ERROR}: #{e}" end # Creates an instance from the system default path # # @param scope [string|array|nil] the scope(s) to access # @param options [Hash] Connection options. These may be used to configure # how OAuth tokens are retrieved, by providing a suitable # `Faraday::Connection`. For example, if a connection proxy must be # used in the current network, you may provide a connection with # with the needed proxy options. # The following keys are recognized: # * `:default_connection` The connection object to use. # * `:connection_builder` A `Proc` that returns a connection. # @raise [Google::Auth::InitializationError] If the credentials file cannot be read or is invalid def from_system_default_path scope = nil, options = {} options = interpret_options scope, options if OS.windows? return nil unless ENV["ProgramData"] prefix = File.join ENV["ProgramData"], "Google/Auth" else prefix = "/etc/google/auth/" end path = File.join prefix, CREDENTIALS_FILE_NAME return nil unless File.exist? path File.open path do |f| return make_creds options.merge(json_key_io: f) end rescue StandardError => e raise InitializationError, "#{SYSTEM_DEFAULT_ERROR}: #{e}" end module_function # Finds project_id from gcloud CLI configuration def load_gcloud_project_id gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows? gcloud = GCLOUD_POSIX_COMMAND unless OS.windows? gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", err: :close, &:read) config = MultiJson.load gcloud_json config["configuration"]["properties"]["core"]["project"] rescue StandardError nil end # @private # Loads a JSON key from an IO object, verifies its type, and rewinds the IO. # # @param json_key_io [IO] An IO object containing the JSON key. # @param expected_type [String] The expected credential type name. # @raise [Google::Auth::InitializationError] If the JSON key type does not match the expected type. def load_and_verify_json_key_type json_key_io, expected_type json_key = MultiJson.load json_key_io.read json_key_io.rewind # Rewind the stream so it can be read again. return if json_key["type"] == expected_type raise Google::Auth::InitializationError, "The provided credentials were not of type '#{expected_type}'. " \ "Instead, the type was '#{json_key['type']}'." end private def interpret_options scope, options if scope.is_a? Hash options = scope scope = nil end return options.merge scope: scope if scope && !options[:scope] options end def service_account_env_vars? ([PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR] - ENV.keys).empty? && !ENV.to_h.fetch_values(PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR).join(" ").empty? end def authorized_user_env_vars? ([CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR] - ENV.keys).empty? && !ENV.to_h.fetch_values(CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR).join(" ").empty? end end end end googleauth-1.16.1/lib/googleauth/oauth2/0000755000004100000410000000000015135672542020106 5ustar www-datawww-datagoogleauth-1.16.1/lib/googleauth/oauth2/sts_client.rb0000644000004100000410000001167715135672542022616 0ustar www-datawww-data# Copyright 2023 Google LLC # # 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 "googleauth/errors" require "googleauth/helpers/connection" module Google module Auth module OAuth2 # OAuth 2.0 Token Exchange Spec. # This module defines a token exchange utility based on the # [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/rfc8693) spec. This will be mainly # used to exchange external credentials for GCP access tokens in workload identity pools to # access Google APIs. # The implementation will support various types of client authentication as allowed in the spec. # # A deviation on the spec will be for additional Google specific options that cannot be easily # mapped to parameters defined in the RFC. # The returned dictionary response will be based on the [rfc8693 section 2.2.1] # (https://tools.ietf.org/html/rfc8693#section-2.2.1) spec JSON response. # class STSClient include Helpers::Connection URLENCODED_HEADERS = { "Content-Type": "application/x-www-form-urlencoded" }.freeze # Create a new instance of the STSClient. # # @param [Hash] options Configuration options # @option options [String] :token_exchange_endpoint The token exchange endpoint # @option options [Faraday::Connection] :connection The Faraday connection to use # @raise [Google::Auth::InitializationError] If token_exchange_endpoint is nil def initialize options = {} raise InitializationError, "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil? self.default_connection = options[:connection] @token_exchange_endpoint = options[:token_exchange_endpoint] end # Exchanges the provided token for another type of token based on the # rfc8693 spec # # @param [Faraday instance] connection # A callable faraday instance used to make HTTP requests. # @param [String] grant_type # The OAuth 2.0 token exchange grant type. # @param [String] subject_token # The OAuth 2.0 token exchange subject token. # @param [String] subject_token_type # The OAuth 2.0 token exchange subject token type. # @param [String] resource # The optional OAuth 2.0 token exchange resource field. # @param [String] audience # The optional OAuth 2.0 token exchange audience field. # @param [Array] scopes # The optional list of scopes to use. # @param [String] requested_token_type # The optional OAuth 2.0 token exchange requested token type. # @param additional_headers (Hash): # The optional additional headers to pass to the token exchange endpoint. # # @return [Hash] A hash containing the token exchange response. # @raise [ArgumentError] If required options are missing # @raise [Google::Auth::AuthorizationError] If the token exchange request fails def exchange_token options = {} missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys unless missing_required_opts.empty? raise ArgumentError, "Missing required options: #{missing_required_opts.join ', '}" end # TODO: Add the ability to add authentication to the headers headers = URLENCODED_HEADERS.dup.merge(options[:additional_headers] || {}) request_body = make_request options response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers if response.status != 200 raise AuthorizationError, "Token exchange failed with status #{response.status}" end MultiJson.load response.body end private def make_request options = {} request_body = { grant_type: options[:grant_type], audience: options[:audience], scope: Array(options[:scopes])&.join(" ") || [], requested_token_type: options[:requested_token_type], subject_token: options[:subject_token], subject_token_type: options[:subject_token_type] } unless options[:additional_options].nil? request_body[:options] = CGI.escape MultiJson.dump(options[:additional_options], symbolize_name: true) end request_body end end end end end googleauth-1.16.1/lib/googleauth/bearer_token.rb0000644000004100000410000001460215135672542021674 0ustar www-datawww-data# Copyright 2025 Google LLC # # 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 "googleauth/base_client" require "googleauth/errors" module Google module Auth ## # Implementation of Bearer Token authentication scenario. # # Bearer tokens are strings representing an authorization grant. # They can be OAuth2 ("ya.29") tokens, JWTs, IDTokens -- anything # that is sent as a `Bearer` in an `Authorization` header. # # Not all 'authentication' strings can be used with this class, # e.g. an API key cannot since API keys are sent in a # `x-goog-api-key` header or as a query parameter. # # This class should be used when the end-user is managing the # authentication token separately, e.g. with a separate service. # This means that tasks like tracking the lifetime of and # refreshing the token are outside the scope of this class. # # There is no JSON representation for this type of credentials. # If the end-user has credentials in JSON format they should typically # use the corresponding credentials type, e.g. ServiceAccountCredentials # with the service account JSON. # class BearerTokenCredentials include Google::Auth::BaseClient # @private Authorization header name AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY # @return [String] The token to be sent as a part of Bearer claim attr_reader :token # The following aliasing is needed for BaseClient since it sends :token_type alias bearer_token token # @return [Time, nil] The token expiration time provided by the end-user. attr_reader :expires_at # @return [String] The universe domain of the universe # this token is for attr_accessor :universe_domain class << self # Create the BearerTokenCredentials. # # @param [Hash] options The credentials options # @option options [String] :token The bearer token to use. # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user. # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch. # If `expires_at` is `nil`, it is treated as "token never expires". # @option options [String] :universe_domain The universe domain of the universe # this token is for (defaults to googleapis.com) # @return [Google::Auth::BearerTokenCredentials] def make_creds options = {} new options end end # Initialize the BearerTokenCredentials. # # @param [Hash] options The credentials options # @option options [String] :token The bearer token to use. # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user. # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch. # If `expires_at` is `nil`, it is treated as "token never expires". # @option options [String] :universe_domain The universe domain of the universe # this token is for (defaults to googleapis.com) # @raise [ArgumentError] If the bearer token is nil or empty def initialize options = {} raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty? @token = options[:token] @expires_at = case options[:expires_at] when Time options[:expires_at] when Numeric Time.at options[:expires_at] end @universe_domain = options[:universe_domain] || "googleapis.com" end # Determines if the credentials object has expired. # # @param [Numeric] seconds The optional timeout in seconds. # @return [Boolean] True if the token has expired, false otherwise, or # if the expires_at was not provided. def expires_within? seconds return false if @expires_at.nil? # Treat nil expiration as "never expires" Time.now + seconds >= @expires_at end # Creates a duplicate of these credentials. # # @param [Hash] options Additional options for configuring the credentials # @option options [String] :token The bearer token to use. # @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time # object or a number of seconds since epoch. # @option options [String] :universe_domain The universe domain (defaults to googleapis.com) # @return [Google::Auth::BearerTokenCredentials] def duplicate options = {} self.class.new( token: options[:token] || @token, expires_at: options[:expires_at] || @expires_at, universe_domain: options[:universe_domain] || @universe_domain ) end # For credentials that are initialized with a token without a principal, # the type of that token should be returned as a principal instead # @private # @return [Symbol] the token type in lieu of the principal def principal token_type end protected ## # BearerTokenCredentials do not support fetching a new token. # # If the token has an expiration time and is expired, this method will # raise an error. # # @param [Hash] _options Options for fetching a new token (not used). # @return [nil] Always returns nil. # @raise [Google::Auth::CredentialsError] If the token is expired. def fetch_access_token! _options = {} if @expires_at && Time.now >= @expires_at raise CredentialsError.with_details( "Bearer token has expired.", credential_type_name: self.class.name, principal: principal ) end nil end private def token_type :bearer_token end end end end googleauth-1.16.1/lib/googleauth.rb0000644000004100000410000000215215135672542017231 0ustar www-datawww-data# Copyright 2015 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 "googleauth/application_default" require "googleauth/api_key" require "googleauth/bearer_token" require "googleauth/client_id" require "googleauth/credentials" require "googleauth/default_credentials" require "googleauth/errors" require "googleauth/external_account" require "googleauth/id_tokens" require "googleauth/impersonated_service_account" require "googleauth/service_account" require "googleauth/service_account_jwt_header" require "googleauth/user_authorizer" require "googleauth/user_refresh" require "googleauth/web_user_authorizer" googleauth-1.16.1/.yardopts0000644000004100000410000000022215135672542015642 0ustar www-datawww-data--no-private --title=Google Auth --markup markdown --markup-provider redcarpet ./lib/**/*.rb - README.md CHANGELOG.md CODE_OF_CONDUCT.md LICENSE googleauth-1.16.1/Credentials.md0000644000004100000410000001620615135672542016564 0ustar www-datawww-data# Introduction The closest thing to a base credentials class is the `BaseClient` module. It includes functionality common to most credentials, such as applying authentication tokens to request headers, managing token expiration and refresh, handling logging, and providing updater procs for API clients. Many credentials classes inherit from `Signet::OAuth2::Client` (`lib/googleauth/signet.rb`) class which provides OAuth-based authentication. The `Signet::OAuth2::Client` includes the `BaseClient` functionality. Most credential types either inherit from `Signet::OAuth2::Client` or include the `BaseClient` module directly. Notably, `Google::Auth::Credentials` (`lib/googleauth/credentials.rb`) is not a base type or a credentials type per se. It is a wrapper for other credential classes that exposes common initialization functionality, such as creating credentials from environment variables, default paths, or application defaults. It is used and subclassed by Google's API client libraries. # List of credentials types ## Simple Authentication (non-OAuth) **Google::Auth::APIKeyCredentials** - `lib/googleauth/api_key.rb` - Includes `Google::Auth::BaseClient` module - Implements Google API Key authentication - API Keys are text strings that don't have an associated JSON file - API Keys provide project information but don't reference an IAM principal - They do not expire and cannot be refreshed - Can be loaded from the `GOOGLE_API_KEY` environment variable 2. **Google::Auth::BearerTokenCredentials** - `lib/googleauth/bearer_token.rb` - Includes `Google::Auth::BaseClient` module - Implements Bearer Token authentication - Bearer tokens are strings representing an authorization grant - Can be OAuth2 tokens, JWTs, ID tokens, or any token sent as a `Bearer` in an `Authorization` header - Used when the end-user is managing the token separately (e.g., with another service) - Token lifetime tracking and refresh are outside this class's scope - No JSON representation for this type of credentials ## GCP-Specialized authentication 3. **Google::Auth::GCECredentials < Signet::OAuth2::Client** - `lib/googleauth/compute_engine.rb` - For obtaining authentication tokens from GCE metadata server - Used automatically when code is running on Google Compute Engine - Fetches tokens from the metadata server with no additional configuration needed - This credential type does not have a supported JSON form 4. **Google::Auth::IAMCredentials < Signet::OAuth2::Client** - `lib/googleauth/iam.rb` - For IAM-based authentication (e.g. service-to-service) - Implements authentication-as-a-service for systems already authenticated - Exchanges existing credentials for a short-lived access token - This credential type does not have a supported JSON form ## Service Account Authentication 5. **Google::Auth::ServiceAccountCredentials < Signet::OAuth2::Client** - `lib/googleauth/service_account.rb` - Authenticates requests using Service Account credentials via an OAuth access token - Created from JSON key file downloaded from Google Cloud Console. The JSON form of this credential type has a `"type"` field with the value `"service_account"`. - Supports both OAuth access tokens and self-signed JWT authentication - Can specify scopes for access token requests 6. **Google::Auth::ServiceAccountJwtHeaderCredentials** - `lib/googleauth/service_account_jwt_header.rb` - Authenticates using Service Account credentials with JWT headers - Typically used via `ServiceAccountCredentials` and not by itself - Creates JWT directly for making authenticated calls - Does not require a round trip to the authorization server - Doesn't support OAuth scopes - uses audience (target API) instead 7. **Google::Auth::ImpersonatedServiceAccountCredentials < Signet::OAuth2::Client** - `lib/googleauth/impersonated_service_account.rb` - For service account impersonation - Allows a GCP principal identified by a set of source credentials to impersonate a service account - Useful for delegation of authority and managing permissions across service accounts - Source credentials must have the Service Account Token Creator role on the target - This credential type supports JSON configuration. The JSON form of this credential type has a `"type"` field with the value `"impersonated_service_account"`. ## User Authentication 8. **Google::Auth::UserRefreshCredentials < Signet::OAuth2::Client** - `lib/googleauth/user_refresh.rb` - For user refresh token authentication (from 3-legged OAuth flow) - Authenticates on behalf of a user who has authorized the application - Handles token refresh when original access token expires - Typically obtained through web or installed application flow. The JSON form of this credential type has a `"type"` field with the value `"authorized_user"`. `Google::Auth::UserAuthorizer` (`lib/googleauth/user_authorizer.rb`) and `Google::Auth::WebUserAuthorizer` (`lib/googleauth/web_user_authorizer.rb`) are used to facilitate user authentication. The `UserAuthorizer` handles interactive 3-Legged-OAuth2 (3LO) user consent authorization for command-line applications. The `WebUserAuthorizer` is a variation of UserAuthorizer adapted for Rack-based web applications that manages OAuth state and provides callback handling. ## External Account Authentication `Google::Auth::ExternalAccount::Credentials` (`lib/googleauth/external_account.rb`) is not a credentials type, it is a module that procides an entry point for External Account credentials. It also serves as a factory that creates appropriate credential types based on credential source (similar to `Google::Auth::get_application_default`). It is included in all External Account credentials types, and it itself includes `Google::Auth::BaseClient` module so all External Account credentials types include `Google::Auth::BaseClient`. The JSON form of this credential type has a `"type"` field with the value `"external_account"`. 9. **Google::Auth::ExternalAccount::AwsCredentials** - `lib/googleauth/external_account/aws_credentials.rb` - Includes `Google::Auth::BaseClient` module - Includes `ExternalAccount::BaseCredentials` module - Uses AWS credentials to authenticate to Google Cloud - Exchanges temporary AWS credentials for Google access tokens - Used for workloads running on AWS that need to access Google Cloud 10. **Google::Auth::ExternalAccount::IdentityPoolCredentials** - `lib/googleauth/external_account/identity_pool_credentials.rb` - Includes `Google::Auth::BaseClient` module - Includes `ExternalAccount::BaseCredentials` module - Authenticates using external identity pool - Exchanges external identity tokens for Google access tokens - Supports file-based and URL-based credential sources 11. **Google::Auth::ExternalAccount::PluggableCredentials** - `lib/googleauth/external_account/pluggable_credentials.rb` - Includes `Google::Auth::BaseClient` module - Includes `ExternalAccount::BaseCredentials` module - Supports executable-based credential sources - Executes external programs to retrieve credentials - Allows for custom authentication mechanisms via external executables googleauth-1.16.1/CODE_OF_CONDUCT.md0000644000004100000410000000367515135672542016612 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/) googleauth-1.16.1/LICENSE0000644000004100000410000002611615135672542015013 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 2015 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. googleauth-1.16.1/Errors.md0000644000004100000410000001457615135672542015613 0ustar www-datawww-data# Error Handling in Google Auth Library for Ruby ## Overview The Google Auth Library for Ruby provides a structured approach to error handling. This document explains the error hierarchy, how to access detailed error information, and provides examples of handling errors effectively. ## Error Hierarchy The Google Auth Library has two main error hierarchies: the core authentication errors and the specialized ID token flow errors. ### Core Authentication Errors These errors are used throughout the main library for general authentication and credential operations: ``` Google::Auth::Error (module) ├── Google::Auth::InitializationError (class) └── Google::Auth::DetailedError (module) ├── Google::Auth::CredentialsError (class) ├── Google::Auth::AuthorizationError (class) ├── Google::Auth::UnexpectedStatusError (class) └── Google::Auth::ParseError (class) ``` ### ID Token Errors These specialized errors are used specifically for ID token flow. They also include the `Google::Auth::Error` module, allowing them to be caught with the same error handling as the core authentication errors: ``` Google::Auth::Error (module) ├── Google::Auth::IDTokens::KeySourceError (class) └── Google::Auth::IDTokens::VerificationError (class) ├── ExpiredTokenError (class) ├── SignatureError (class) ├── IssuerMismatchError (class) ├── AudienceMismatchError (class) └── AuthorizedPartyMismatchError (class) ``` ### Error Module Types - **`Google::Auth::Error`**: Base module that all Google Auth errors include. Use this to catch any error from the library. - **`Google::Auth::DetailedError`**: Extends `Error` to include detailed information about the credential that caused the error, including the credential type and principal. ## Core Authentication Error Classes - **`InitializationError`**: Raised during credential initialization when required parameters are missing or invalid. - **`CredentialsError`**: Generic error raised during authentication flows. - **`AuthorizationError`**: Raised when a remote server refuses to authorize the client. Inherits from `Signet::AuthorizationError`. Is being raised where `Signet::AuthorizationError` was raised previously. - **`UnexpectedStatusError`**: Raised when a server returns an unexpected HTTP status code. Inherits from `Signet::UnexpectedStatusError`. Is being raised where `Signet::UnexpectedStatusError` was raised previously. - **`ParseError`**: Raised when the client fails to parse a value from a response. Inherits from `Signet::ParseError`. Is being raised where `Signet::ParseError` was raised previously. ## Detailed Error Information Errors that include the `DetailedError` module provide additional context about what went wrong: - **`credential_type_name`**: The class name of the credential that raised the error (e.g., `"Google::Auth::ServiceAccountCredentials"`) - **`principal`**: The identity associated with the credentials (e.g., an email address for service accounts, `:api_key` for API key credentials) ### Example: Catching and Handling Core Errors ```ruby begin credentials = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open("your-key.json") ) # Use credentials... rescue Google::Auth::InitializationError => e puts "Failed to initialize credentials: #{e.message}" # e.g., Missing required fields in the service account key file rescue Google::Auth::DetailedError => e puts "Authorization failed: #{e.message}" puts "Credential type: #{e.credential_type_name}" puts "Principal: #{e.principal}" # e.g., Invalid or revoked service account rescue Google::Auth::Error => e puts "Unknown Google Auth error: #{e.message}" end ``` ## Backwards compatibility Some classes in the Google Auth Library raise standard Ruby `ArgumentError` and `TypeError`. These errors are preserved for backward compatibility, however the new code will raise `Google::Auth::InitializationError` instead. ## ID Token Verification The Google Auth Library includes functionality for verifying ID tokens through the `Google::Auth::IDTokens` namespace. These operations have their own specialized error classes that also include the `Google::Auth::Error` module, allowing them to be caught with the same error handling as other errors in the library. ### ID Token Error Classes - **`KeySourceError`**: Raised when the library fails to obtain the keys needed to verify a token, typically from a JWKS (JSON Web Key Set) endpoint. - **`VerificationError`**: Base class for all errors related to token verification failures. - **`ExpiredTokenError`**: Raised when a token has expired according to its expiration time claim (`exp`). - **`SignatureError`**: Raised when a token's signature cannot be verified, indicating it might be tampered with or corrupted. - **`IssuerMismatchError`**: Raised when a token's issuer (`iss` claim) doesn't match the expected issuer. - **`AudienceMismatchError`**: Raised when a token's audience (`aud` claim) doesn't match the expected audience. - **`AuthorizedPartyMismatchError`**: Raised when a token's authorized party (`azp` claim) doesn't match the expected client ID. ### Example: Handling ID Token Verification Errors ```ruby require "googleauth/id_tokens" begin # Verify the provided ID token payload = Google::Auth::IDTokens.verify_oidc( id_token, audience: "expected-audience-12345.apps.googleusercontent.com" ) # Use the verified token payload user_email = payload["email"] rescue Google::Auth::IDTokens::ExpiredTokenError => e puts "The token has expired. Please obtain a new one." rescue Google::Auth::IDTokens::SignatureError => e puts "Invalid token signature." rescue Google::Auth::IDTokens::IssuerMismatchError => e puts "Invalid token issuer." rescue Google::Auth::IDTokens::AudienceMismatchError => e puts "This token is not intended for this application (invalid audience)." rescue Google::Auth::IDTokens::AuthorizedPartyMismatchError => e puts "Invalid token authorized party." rescue Google::Auth::IDTokens::VerificationError => e puts "Token verification failed: #{e.message}" # Generic verification error handling rescue Google::Auth::IDTokens::KeySourceError => e puts "Unable to retrieve verification keys: #{e.message}" rescue Google::Auth::Error => e puts "Unknown Google Auth error: #{e.message}" # This will catch any Google Auth error end ``` googleauth-1.16.1/googleauth.gemspec0000644000004100000410000000732515135672542017512 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: googleauth 1.16.1 ruby lib Gem::Specification.new do |s| s.name = "googleauth".freeze s.version = "1.16.1".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/googleapis/google-auth-library-ruby/issues", "changelog_uri" => "https://github.com/googleapis/google-auth-library-ruby/blob/main/CHANGELOG.md", "source_code_uri" => "https://github.com/googleapis/google-auth-library-ruby" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Google LLC".freeze] s.date = "1980-01-02" s.description = "Implements simple authorization for accessing Google APIs, and provides support for Application Default Credentials.".freeze s.email = ["googleapis-packages@google.com".freeze] s.files = [".yardopts".freeze, "CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "Credentials.md".freeze, "Errors.md".freeze, "LICENSE".freeze, "README.md".freeze, "SECURITY.md".freeze, "lib/googleauth.rb".freeze, "lib/googleauth/api_key.rb".freeze, "lib/googleauth/application_default.rb".freeze, "lib/googleauth/base_client.rb".freeze, "lib/googleauth/bearer_token.rb".freeze, "lib/googleauth/client_id.rb".freeze, "lib/googleauth/compute_engine.rb".freeze, "lib/googleauth/credentials.rb".freeze, "lib/googleauth/credentials_loader.rb".freeze, "lib/googleauth/default_credentials.rb".freeze, "lib/googleauth/errors.rb".freeze, "lib/googleauth/external_account.rb".freeze, "lib/googleauth/external_account/aws_credentials.rb".freeze, "lib/googleauth/external_account/base_credentials.rb".freeze, "lib/googleauth/external_account/external_account_utils.rb".freeze, "lib/googleauth/external_account/identity_pool_credentials.rb".freeze, "lib/googleauth/external_account/pluggable_credentials.rb".freeze, "lib/googleauth/helpers/connection.rb".freeze, "lib/googleauth/iam.rb".freeze, "lib/googleauth/id_tokens.rb".freeze, "lib/googleauth/id_tokens/errors.rb".freeze, "lib/googleauth/id_tokens/key_sources.rb".freeze, "lib/googleauth/id_tokens/verifier.rb".freeze, "lib/googleauth/impersonated_service_account.rb".freeze, "lib/googleauth/json_key_reader.rb".freeze, "lib/googleauth/oauth2/sts_client.rb".freeze, "lib/googleauth/scope_util.rb".freeze, "lib/googleauth/service_account.rb".freeze, "lib/googleauth/service_account_jwt_header.rb".freeze, "lib/googleauth/signet.rb".freeze, "lib/googleauth/stores/file_token_store.rb".freeze, "lib/googleauth/stores/redis_token_store.rb".freeze, "lib/googleauth/token_store.rb".freeze, "lib/googleauth/user_authorizer.rb".freeze, "lib/googleauth/user_refresh.rb".freeze, "lib/googleauth/version.rb".freeze, "lib/googleauth/web_user_authorizer.rb".freeze] s.homepage = "https://github.com/googleapis/google-auth-library-ruby".freeze s.licenses = ["Apache-2.0".freeze] s.required_ruby_version = Gem::Requirement.new(">= 3.0".freeze) s.rubygems_version = "3.6.9".freeze s.summary = "Google Auth Library for Ruby".freeze s.specification_version = 4 s.add_runtime_dependency(%q.freeze, [">= 1.0".freeze, "< 3.a".freeze]) s.add_runtime_dependency(%q.freeze, ["~> 2.2".freeze]) s.add_runtime_dependency(%q.freeze, ["~> 0.1".freeze]) s.add_runtime_dependency(%q.freeze, [">= 1.4".freeze, "< 4.0".freeze]) s.add_runtime_dependency(%q.freeze, ["~> 1.11".freeze]) s.add_runtime_dependency(%q.freeze, [">= 0.9".freeze, "< 2.0".freeze]) s.add_runtime_dependency(%q.freeze, [">= 0.16".freeze, "< 2.a".freeze]) end googleauth-1.16.1/README.md0000644000004100000410000002367015135672542015267 0ustar www-datawww-data# Google Auth Library for Ruby
Homepage
http://www.github.com/googleapis/google-auth-library-ruby
Authors
Tim Emiola
Copyright
Copyright © 2015 Google, Inc.
License
Apache 2.0
[![Gem Version](https://badge.fury.io/rb/googleauth.svg)](http://badge.fury.io/rb/googleauth) ## Description This is Google's officially supported ruby client library for using OAuth 2.0 authorization and authentication with Google APIs. ## Install Be sure `https://rubygems.org/` is in your gem sources. For normal client usage, this is sufficient: ```bash $ gem install googleauth ``` ## Example Usage ```ruby require 'googleauth' # Get the environment configured authorization scopes = ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/compute'] authorization = Google::Auth.get_application_default(scopes) # Add the the access token obtained using the authorization to a hash, e.g # headers. some_headers = {} authorization.apply(some_headers) ``` ## Application Default Credentials This library provides an implementation of [application default credentials][application default credentials] for Ruby. The Application Default Credentials provide a simple way to get authorization credentials for use in calling Google APIs. They are best suited for cases when the call needs to have the same identity and authorization level for the application independent of the user. This is the recommended approach to authorize calls to Cloud APIs, particularly when you're building an application that uses Google Compute Engine. ## User Credentials The library also provides support for requesting and storing user credentials (3-Legged OAuth2.) Two implementations are currently available, a generic authorizer useful for command line apps or custom integrations as well as a web variant tailored toward Rack-based applications. The authorizers are intended for authorization use cases. For sign-on, see [Google Identity Platform](https://developers.google.com/identity/) ## Important notes If you accept a credential configuration (credential JSON/File/Stream) from an external source for authentication to Google Cloud, you must validate it before providing it to any Google API or library. Providing an unvalidated credential configuration to Google APIs can compromise the security of your systems and data. For more information, refer to [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). ### Example (Web) ```ruby require 'googleauth' require 'googleauth/web_user_authorizer' require 'googleauth/stores/redis_token_store' require 'redis' client_id = Google::Auth::ClientId.from_file('/path/to/client_secrets.json') scope = ['https://www.googleapis.com/auth/drive'] token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new) authorizer = Google::Auth::WebUserAuthorizer.new( client_id, scope, token_store, '/oauth2callback') get('/authorize') do # NOTE: Assumes the user is already authenticated to the app user_id = request.session['user_id'] credentials = authorizer.get_credentials(user_id, request) if credentials.nil? redirect authorizer.get_authorization_url(login_hint: user_id, request: request) end # Credentials are valid, can call APIs # ... end get('/oauth2callback') do target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred( request) redirect target_url end ``` ### Example (Web with PKCE) Proof Key for Code Exchange (PKCE) is an [RFC](https://www.rfc-editor.org/rfc/rfc7636) that aims to prevent malicious operating system processes from hijacking an OAUTH 2.0 exchange. PKCE mitigates the above vulnerability by including `code_challenge` and `code_challenge_method` parameters in the Authorization Request and a `code_verifier` parameter in the Access Token Request. ```ruby require 'googleauth' require 'googleauth/web_user_authorizer' require 'googleauth/stores/redis_token_store' require 'redis' client_id = Google::Auth::ClientId.from_file('/path/to/client_secrets.json') scope = ['https://www.googleapis.com/auth/drive'] token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new) authorizer = Google::Auth::WebUserAuthorizer.new( client_id, scope, token_store, '/oauth2callback') get('/authorize') do # NOTE: Assumes the user is already authenticated to the app user_id = request.session['user_id'] # User needs to take care of generating the code_verifier and storing it in # the session. request.session['code_verifier'] ||= Google::Auth::WebUserAuthorizer.generate_code_verifier authorizer.code_verifier = request.session['code_verifier'] credentials = authorizer.get_credentials(user_id, request) if credentials.nil? redirect authorizer.get_authorization_url(login_hint: user_id, request: request) end # Credentials are valid, can call APIs # ... end get('/oauth2callback') do target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred( request) redirect target_url end ``` ### Example (Command Line) [Deprecated] The Google Auth OOB flow has been discontiued on January 31, 2023. The OOB flow is a legacy flow that is no longer considered secure. To continue using Google Auth, please migrate your applications to a more secure flow. For more information on how to do this, please refer to this [OOB Migration](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration) guide. ```ruby require 'googleauth' require 'googleauth/stores/file_token_store' OOB_URI = 'urn:ietf:wg:oauth:2.0:oob' scope = 'https://www.googleapis.com/auth/drive' client_id = Google::Auth::ClientId.from_file('/path/to/client_secrets.json') token_store = Google::Auth::Stores::FileTokenStore.new( :file => '/path/to/tokens.yaml') authorizer = Google::Auth::UserAuthorizer.new(client_id, scope, token_store) user_id = ENV['USER'] credentials = authorizer.get_credentials(user_id) if credentials.nil? url = authorizer.get_authorization_url(base_url: OOB_URI ) puts "Open #{url} in your browser and enter the resulting code:" code = gets credentials = authorizer.get_and_store_credentials_from_code( user_id: user_id, code: code, base_url: OOB_URI) end # OK to use credentials ``` ### Example (Service Account) ```ruby scope = 'https://www.googleapis.com/auth/androidpublisher' authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open('/path/to/service_account_json_key.json'), scope: scope) authorizer.fetch_access_token! ``` You can also use a JSON keyfile by setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ```bash export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account_json_key.json ``` ```ruby require 'googleauth' require 'google/apis/drive_v3' Drive = ::Google::Apis::DriveV3 drive = Drive::DriveService.new scope = 'https://www.googleapis.com/auth/drive' authorizer = Google::Auth::ServiceAccountCredentials.from_env(scope: scope) drive.authorization = authorizer list_files = drive.list_files() ``` ### 3-Legged OAuth with a Service Account This is similar to regular service account authorization (see [this answer](https://support.google.com/a/answer/2538798?hl=en) for more details on the differences), but you'll need to indicate which user your service account is impersonating by manually updating the `sub` field. ```ruby scope = 'https://www.googleapis.com/auth/androidpublisher' authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open('/path/to/service_account_json_key.json'), scope: scope ) authorizer.update!(sub: "email-to-impersonate@your-domain.com") authorizer.fetch_access_token! ``` ### Example (Environment Variables) ```bash export GOOGLE_ACCOUNT_TYPE=service_account export GOOGLE_CLIENT_ID=000000000000000000000 export GOOGLE_CLIENT_EMAIL=xxxx@xxxx.iam.gserviceaccount.com export GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" ``` ```ruby require 'googleauth' require 'google/apis/drive_v3' Drive = ::Google::Apis::DriveV3 drive = Drive::DriveService.new # Auths with ENV vars: # "GOOGLE_CLIENT_ID", # "GOOGLE_CLIENT_EMAIL", # "GOOGLE_ACCOUNT_TYPE", # "GOOGLE_PRIVATE_KEY" auth = ::Google::Auth::ServiceAccountCredentials .make_creds(scope: 'https://www.googleapis.com/auth/drive') drive.authorization = auth list_files = drive.list_files() ``` ### Storage Authorizers require a storage instance to manage long term persistence of access and refresh tokens. Two storage implementations are included: * Google::Auth::Stores::FileTokenStore * Google::Auth::Stores::RedisTokenStore Custom storage implementations can also be used. See [token_store.rb](https://googleapis.dev/ruby/googleauth/latest/Google/Auth/TokenStore.html) for additional details. ## Supported Ruby Versions This library is supported on Ruby 3.0+. 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. 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. ## License This library is licensed under Apache 2.0. Full license text is available in [LICENSE][license]. ## Contributing See [CONTRIBUTING][contributing]. ## Support Please [report bugs at the project on Github](https://github.com/google/google-auth-library-ruby/issues). Don't hesitate to [ask questions](http://stackoverflow.com/questions/tagged/google-auth-library-ruby) about the client or APIs on [StackOverflow](http://stackoverflow.com). [application default credentials]: https://cloud.google.com/docs/authentication/provide-credentials-adc [license]: https://github.com/googleapis/google-auth-library-ruby/tree/main/LICENSE googleauth-1.16.1/CHANGELOG.md0000644000004100000410000003241015135672542015611 0ustar www-datawww-data# Release History ### 1.16.1 (2026-01-15) #### Bug Fixes * restore support for JSON keys missing 'type' field ([#558](https://github.com/googleapis/google-auth-library-ruby/issues/558)) ### 1.16.0 (2025-11-21) #### Features * Add ADC support for impersonated credentials ([#547](https://github.com/googleapis/google-auth-library-ruby/issues/547)) #### Bug Fixes * Include security warning in ExternalAccount and ImpersonatedServiceAccount credentials ([#551](https://github.com/googleapis/google-auth-library-ruby/issues/551)) ### 1.15.1 (2025-10-14) #### Bug Fixes * Deprecate method make_creds in DefaultCredentials ([#545](https://github.com/googleapis/google-auth-library-ruby/issues/545)) ### 1.15.0 (2025-08-25) #### Features * add typed errors to authentication library ([#533](https://github.com/googleapis/google-auth-library-ruby/issues/533)) * Support for JWT 3.x ([#542](https://github.com/googleapis/google-auth-library-ruby/issues/542)) #### Bug Fixes * fix incorrect error and apply some code complexity refactoring ([#529](https://github.com/googleapis/google-auth-library-ruby/issues/529)) * support Pathname for cred loading ([#537](https://github.com/googleapis/google-auth-library-ruby/issues/537)) #### Documentation * add summary documentation on credentials types and improve YARD comments * add summary documentation on credentials types and improve YARD comments ([#530](https://github.com/googleapis/google-auth-library-ruby/issues/530)) ### 1.14.0 (2025-03-14) #### Features * add API key credentials ([#520](https://github.com/googleapis/google-auth-library-ruby/issues/520)) * Add Bearer token credentials * add BearerToken credentials ([#522](https://github.com/googleapis/google-auth-library-ruby/issues/522)) * Update minimum Ruby version to 3.0 ([#527](https://github.com/googleapis/google-auth-library-ruby/issues/527)) #### Bug Fixes * Eliminated the "attribute accessor as module_function" warning ([#519](https://github.com/googleapis/google-auth-library-ruby/issues/519)) * Get the project_id from gcloud ([#479](https://github.com/googleapis/google-auth-library-ruby/issues/479)) * logger configuration in service account JWT header ([#525](https://github.com/googleapis/google-auth-library-ruby/issues/525)) ### 1.13.1 (2025-01-24) #### Bug Fixes * Signet client subclasses no longer make the update! method private ([#516](https://github.com/googleapis/google-auth-library-ruby/issues/516)) ### 1.13.0 (2025-01-22) #### Features * create impersonated service credentials ([#499](https://github.com/googleapis/google-auth-library-ruby/issues/499)) #### Documentation * Include note about validating externally-provided credentials ([#512](https://github.com/googleapis/google-auth-library-ruby/issues/512)) ### 1.12.2 (2024-12-19) #### Bug Fixes * GCECredentials lazily fetches from the metadata server to ensure a universe domain is known ([#509](https://github.com/googleapis/google-auth-library-ruby/issues/509)) ### 1.12.1 (2024-12-17) #### Bug Fixes * Restored previous behavior where the apply! method returns the auth header ([#506](https://github.com/googleapis/google-auth-library-ruby/issues/506)) ### 1.12.0 (2024-12-05) #### Features * provided opt-in debug logging ([#490](https://github.com/googleapis/google-auth-library-ruby/issues/490)) ### 1.11.2 (2024-10-23) #### Bug Fixes * Temporarily disable universe domain query from GCE metadata server ([#493](https://github.com/googleapis/google-auth-library-ruby/issues/493)) * Use updated metadata path for universe-domain ([#496](https://github.com/googleapis/google-auth-library-ruby/issues/496)) ### 1.11.1 (2024-10-04) #### Bug Fixes * Fixed parsing of expiration timestamp from ID tokens ([#492](https://github.com/googleapis/google-auth-library-ruby/issues/492)) * Use NoMethodError instead of NotImplementedError for unimplemented base class methods ([#487](https://github.com/googleapis/google-auth-library-ruby/issues/487)) ### 1.11.0 (2024-02-09) #### Features * Deprecate the positional argument for callback_uri, and introduce keyword argument instead ([#475](https://github.com/googleapis/google-auth-library-ruby/issues/475)) ### 1.10.0 (2024-02-08) #### Features * add PKCE to 3 Legged OAuth exchange ([#471](https://github.com/googleapis/google-auth-library-ruby/issues/471)) #### Bug Fixes * Client library credentials provide correct self-signed JWT and external account behavior when loading from a file path or JSON data ([#474](https://github.com/googleapis/google-auth-library-ruby/issues/474)) * Prioritize universe domain specified in GCECredentials arguments over metadata-fetched value ([#472](https://github.com/googleapis/google-auth-library-ruby/issues/472)) ### 1.9.2 (2024-01-25) #### Bug Fixes * Prevent access tokens from being fetched at service account construction in the self-signed-jwt case ([#467](https://github.com/googleapis/google-auth-library-ruby/issues/467)) ### 1.9.1 (2023-12-12) #### Bug Fixes * update expires_in for cached metadata-retrieved tokens ([#464](https://github.com/googleapis/google-auth-library-ruby/issues/464)) ### 1.9.0 (2023-12-07) #### Features * Include universe_domain in credentials ([#460](https://github.com/googleapis/google-auth-library-ruby/issues/460)) * Use google-cloud-env for more robust Metadata Service access ([#459](https://github.com/googleapis/google-auth-library-ruby/issues/459)) ### 1.8.1 (2023-09-19) #### Documentation * improve ADC related error and warning messages ([#452](https://github.com/googleapis/google-auth-library-ruby/issues/452)) ### 1.8.0 (2023-09-07) #### Features * Pass additional parameters to auhtorization url ([#447](https://github.com/googleapis/google-auth-library-ruby/issues/447)) #### Documentation * improve ADC related error and warning messages ([#449](https://github.com/googleapis/google-auth-library-ruby/issues/449)) ### 1.7.0 (2023-07-14) #### Features * Adding support for pluggable auth credentials ([#437](https://github.com/googleapis/google-auth-library-ruby/issues/437)) #### Documentation * fixed iss argument and description in comments of IDTokens ([#438](https://github.com/googleapis/google-auth-library-ruby/issues/438)) ### 1.6.0 (2023-06-20) #### Features * adding identity pool credentials ([#433](https://github.com/googleapis/google-auth-library-ruby/issues/433)) #### Documentation * deprecation message for discontinuing command line auth flow ([#435](https://github.com/googleapis/google-auth-library-ruby/issues/435)) ### 1.5.2 (2023-04-13) #### Bug Fixes * AWS IMDSV2 session token fetching shall call PUT method instead of GET ([#429](https://github.com/googleapis/google-auth-library-ruby/issues/429)) * GCECredentials - Allow retrieval of ID token ([#425](https://github.com/googleapis/google-auth-library-ruby/issues/425)) ### 1.5.1 (2023-04-10) #### Bug Fixes * Remove external account config validation ([#427](https://github.com/googleapis/google-auth-library-ruby/issues/427)) ### 1.5.0 (2023-03-21) #### Features * Add support for AWS Workload Identity Federation ([#418](https://github.com/googleapis/google-auth-library-ruby/issues/418)) ### 1.4.0 (2022-12-14) #### Features * make new_jwt_token public in order to fetch raw token directly ([#405](https://github.com/googleapis/google-auth-library-ruby/issues/405)) ### 1.3.0 (2022-10-18) #### Features * Use OpenSSL 3.0 compatible interfaces for IDTokens ([#397](https://github.com/googleapis/google-auth-library-ruby/issues/397)) ### 1.2.0 (2022-06-23) * Updated minimum Ruby version to 2.6 ### 1.1.3 (2022-04-20) #### Documentation * Add README instructions for 3-Legged OAuth with a service account ### 1.1.2 (2022-02-22) #### Bug Fixes * Support Faraday 2 ### 1.1.1 (2022-02-14) #### Bug Fixes * add quota_project to user refresh credentials ### 1.1.0 (2021-10-24) #### Features * Support short-lived tokens in Credentials ### 1.0.0 (2021-09-27) Bumped version to 1.0.0. Releases from this point will follow semver. * Allow dependency on future 1.x versions of signet * Prevented gcloud from authenticating on the console when getting the gcloud project ### 0.17.1 (2021-09-01) * Updates to gem metadata ### 0.17.0 (2021-07-30) * Allow scopes to be self-signed into jwts ### 0.16.2 (2021-04-28) * Stop attempting to get the project from gcloud when applying self-signed JWTs ### 0.16.1 (2021-04-01) * Accept application/text content-type for plain idtoken response ### 0.16.0 (2021-03-04) * Drop support for Ruby 2.4 and add support for Ruby 3.0 ### 0.15.1 (2021-02-08) * Fix crash when using a client credential without any paths or env_vars set ### 0.15.0 (2021-01-26) * Credential parameters inherit from superclasses * Service accounts apply a self-signed JWT if scopes are marked as default * Retry fetch_access_token when GCE metadata server returns unexpected errors * Support correct service account and user refresh behavior for custom credential env variables ### 0.14.0 / 2020-10-09 * Honor GCE_METADATA_HOST environment variable * Fix errors in some environments when requesting an access token for multiple scopes ### 0.13.1 / 2020-07-30 * Support scopes when using GCE Metadata Server authentication ([@ball-hayden][]) ### 0.13.0 / 2020-06-17 * Support for validating ID tokens. * Fixed header application of ID tokens from service accounts. ### 0.12.0 / 2020-04-08 * Support for ID token credentials. * Support reading quota_id_project from service account credentials. ### 0.11.0 / 2020-02-24 * Support Faraday 1.x. * Allow special "postmessage" value for redirect_uri. ### 0.10.0 / 2019-10-09 Note: This release now requires Ruby 2.4 or later * Increase metadata timeout to improve reliability in some hosting environments * Support an environment variable to suppress Cloud SDK credentials warnings * Make the header check case insensitive * Set instance variables at initialization to avoid spamming warnings * Pass "Metadata-Flavor" header to metadata server when checking for GCE ### 0.9.0 / 2019-08-05 * Restore compatibility with Ruby 2.0. This is the last release that will work on end-of-lifed versions of Ruby. The 0.10 release will require Ruby 2.4 or later. * Update Credentials to use methods for values that are intended to be changed by users, replacing constants. * Add retry on error for fetch_access_token * Allow specifying custom state key-values * Add verbosity none to gcloud command * Make arity of WebUserAuthorizer#get_credentials compatible with the base class ### 0.8.1 / 2019-03-27 * Silence unnecessary gcloud warning * Treat empty credentials environment variables as unset ### 0.8.0 / 2019-01-02 * Support connection options :default_connection and :connection_builder when creating credentials that need to refresh OAuth tokens. This lets clients provide connection objects with custom settings, such as proxies, needed for the client environment. * Removed an unnecessary warning about project IDs. ### 0.7.1 / 2018-10-25 * Make load_gcloud_project_id module function. ### 0.7.0 / 2018-10-24 * Add project_id instance variable to UserRefreshCredentials, ServiceAccountCredentials, and Credentials. ### 0.6.7 / 2018-10-16 * Update memoist dependency to ~> 0.16. ### 0.6.6 / 2018-08-22 * Remove ruby version warnings. ### 0.6.5 / 2018-08-16 * Fix incorrect http verb when revoking credentials. * Warn on EOL ruby versions. ### 0.6.4 / 2018-08-03 * Resolve issue where DefaultCredentials constant was undefined. ### 0.6.3 / 2018-08-02 * Resolve issue where token_store was being written to twice ### 0.6.2 / 2018-08-01 * Add warning when using cloud sdk credentials ### 0.6.1 / 2017-10-18 * Fix file permissions ### 0.6.0 / 2017-10-17 * Support ruby-jwt 2.0 * Add simple credentials class ### 0.5.3 / 2017-07-21 * Fix file permissions on the gem's `.rb` files. ### 0.5.2 / 2017-07-19 * Add retry mechanism when fetching access tokens in `GCECredentials` and `UserRefreshCredentials` classes. * Update Google API OAuth2 token credential URI to v4. ### 0.5.1 / 2016-01-06 * Change header name emitted by `Client#apply` from "Authorization" to "authorization" ([@murgatroid99][]) * Fix ADC not working on some windows machines ([@vsubramani][]) ### 0.5.0 / 2015-10-12 * Initial support for user credentials ([@sqrrrl][]) * Update Signet to 0.7 ### 0.4.2 / 2015-08-05 * Updated UserRefreshCredentials hash to use string keys ([@haabaato][]) * Add support for a system default credentials file. ([@mr-salty][]) * Fix bug when loading credentials from ENV ([@dwilkie][]) * Relax the constraint of dependent version of multi_json ([@igrep][]) * Enables passing credentials via environment variables. ([@haabaato][]) ### 0.4.1 / 2015-04-25 * Improves handling of --no-scopes GCE authorization ([@tbetbetbe][]) * Refactoring and cleanup ([@joneslee85][]) ### 0.4.0 / 2015-03-25 * Adds an implementation of JWT header auth ([@tbetbetbe][]) ### 0.3.0 / 2015-03-23 * makes the scope parameter's optional in all APIs. ([@tbetbetbe][]) * changes the scope parameter's position in various constructors. ([@tbetbetbe][]) [@dwilkie]: https://github.com/dwilkie [@haabaato]: https://github.com/haabaato [@igrep]: https://github.com/igrep [@joneslee85]: https://github.com/joneslee85 [@mr-salty]: https://github.com/mr-salty [@tbetbetbe]: https://github.com/tbetbetbe [@murgatroid99]: https://github.com/murgatroid99 [@vsubramani]: https://github.com/vsubramani [@ball-hayden]: https://github.com/ball-hayden