├── .github ├── release-trigger.yml ├── renovate.json ├── release-please.yml ├── blunderbuss.yml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── support_request.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── ci.yml └── CONTRIBUTING.md ├── .rspec ├── .release-please-manifest.json ├── .yardopts ├── .repo-metadata.json ├── .kokoro ├── samples.sh ├── release.sh ├── presubmit │ └── samples.cfg ├── release.cfg └── populate-secrets.sh ├── SECURITY.md ├── .rubocop.yml ├── Gemfile ├── release-please-config.json ├── integration ├── helper.rb └── id_tokens │ └── key_source_test.rb ├── .gitignore ├── lib ├── googleauth │ ├── version.rb │ ├── helpers │ │ └── connection.rb │ ├── stores │ │ ├── file_token_store.rb │ │ └── redis_token_store.rb │ ├── token_store.rb │ ├── id_tokens │ │ ├── errors.rb │ │ └── verifier.rb │ ├── json_key_reader.rb │ ├── iam.rb │ ├── scope_util.rb │ ├── application_default.rb │ ├── base_client.rb │ ├── client_id.rb │ ├── errors.rb │ ├── external_account │ │ ├── external_account_utils.rb │ │ └── identity_pool_credentials.rb │ ├── oauth2 │ │ └── sts_client.rb │ ├── default_credentials.rb │ ├── api_key.rb │ ├── external_account.rb │ ├── bearer_token.rb │ ├── service_account_jwt_header.rb │ └── user_refresh.rb └── googleauth.rb ├── samples ├── Gemfile ├── acceptance │ ├── helper.rb │ ├── auth_cloud_idtoken_metadata_server_test.rb │ ├── authenticate_implicit_with_adc_test.rb │ └── authenticate_with_api_key_test.rb ├── auth_cloud_idtoken_metadata_server.rb ├── authenticate_implicit_with_adc.rb └── authenticate_with_api_key.rb ├── .toys ├── release.rb ├── samples.rb ├── .lib │ └── repo_context.rb ├── .toys.rb ├── ci.rb └── linkinator.rb ├── test ├── helper.rb ├── scope_util_test.rb ├── json_key_reader_test.rb ├── errors_test.rb ├── client_id_test.rb ├── api_key_test.rb ├── bearer_token_test.rb └── principal_test.rb ├── spec ├── googleauth │ ├── stores │ │ ├── redis_token_store_spec.rb │ │ ├── file_token_store_spec.rb │ │ └── store_examples.rb │ ├── iam_spec.rb │ ├── service_account │ │ └── jwt_header_auth_examples.rb │ ├── oauth2 │ │ └── sts_client_spec.rb │ └── apply_auth_examples.rb └── spec_helper.rb ├── googleauth.gemspec ├── .trampolinerc ├── CODE_OF_CONDUCT.md ├── Errors.md └── Credentials.md /.github/release-trigger.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false 3 | } 4 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.16.0" 3 | } 4 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | bumpMinorPreMajor: true 2 | handleGHRelease: true 3 | manifest: true 4 | monorepoTags: true 5 | packageName: googleauth 6 | primaryBranch: main 7 | releaseType: ruby-yoshi 8 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --title=Google Auth 3 | --markup markdown 4 | --markup-provider redcarpet 5 | 6 | ./lib/**/*.rb 7 | - 8 | README.md 9 | CHANGELOG.md 10 | CODE_OF_CONDUCT.md 11 | LICENSE 12 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | # Configuration for the Blunderbuss GitHub app. For more info see 2 | # https://github.com/googleapis/repo-automation-bots/tree/main/packages/blunderbuss 3 | assign_issues: 4 | - 'viacheslav-rostovtsev' 5 | -------------------------------------------------------------------------------- /.repo-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_documentation": "https://googleapis.dev/ruby/googleauth/latest", 3 | "distribution-name": "googleauth", 4 | "language": "ruby", 5 | "library_type": "AUTH", 6 | "release_level": "stable", 7 | "repo": "googleapis/google-auth-library-ruby" 8 | } 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls who is tagged for review for any given pull request. 3 | # 4 | # For syntax help see: 5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 6 | 7 | * @googleapis/ruby-team 8 | -------------------------------------------------------------------------------- /.kokoro/samples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # Install gems in the user directory because the default install directory 6 | # is in a read-only location. 7 | export GEM_HOME=$HOME/.gem 8 | export PATH=$GEM_HOME/bin:$PATH 9 | 10 | gem install --no-document toys 11 | 12 | toys samples < /dev/null 13 | -------------------------------------------------------------------------------- /.kokoro/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # Install gems in the user directory because the default install directory 6 | # is in a read-only location. 7 | export GEM_HOME=$HOME/.gem 8 | export PATH=$GEM_HOME/bin:$PATH 9 | 10 | toys release perform -v --reporter-org=googleapis --force-republish --enable-rad < /dev/null 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support request 3 | about: If you have a support contract with Google, please create an issue in the Google Cloud Support console. 4 | 5 | --- 6 | 7 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | google-style: google-style.yml 3 | 4 | AllCops: 5 | Exclude: 6 | - "integration/**/*" 7 | - "spec/**/*" 8 | - "test/**/*" 9 | Metrics/BlockLength: 10 | Exclude: 11 | - "googleauth.gemspec" 12 | Metrics/ClassLength: 13 | Max: 200 14 | Metrics/CyclomaticComplexity: 15 | Max: 15 16 | Metrics/ModuleLength: 17 | Max: 200 18 | Metrics/PerceivedComplexity: 19 | Max: 15 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in googleauth.gemspec 4 | gemspec 5 | 6 | gem "fakefs", ">= 1.0", "< 4" 7 | gem "fakeredis", "~> 0.5" 8 | gem "gems", "~> 1.2" 9 | gem "google-style", "~> 1.30.1" 10 | gem "logging", "~> 2.0" 11 | gem "minitest", "~> 5.14" 12 | gem "minitest-focus", "~> 1.1" 13 | gem "rack-test", "~> 2.0" 14 | gem "redcarpet", "~> 3.0" 15 | gem "redis", ">= 4.0", "< 6" 16 | gem "rspec", "~> 3.0" 17 | gem "webmock", "~> 3.8" 18 | gem "yard", "~> 0.9" 19 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bump-minor-pre-major": true, 3 | "bump-patch-for-minor-pre-major": false, 4 | "draft": false, 5 | "include-component-in-tag": true, 6 | "include-v-in-tag": true, 7 | "prerelease": false, 8 | "release-type": "ruby-yoshi", 9 | "skip-github-release": false, 10 | "separate-pull-requests": true, 11 | "tag-separator": "/", 12 | "sequential-calls": true, 13 | "packages": { 14 | ".": { 15 | "component": "googleauth", 16 | "version-file": "lib/googleauth/version.rb" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integration/helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "minitest/autorun" 16 | require "minitest/focus" 17 | require "googleauth" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | Gemfile.lock 3 | *.gem 4 | *.rbc 5 | /.config 6 | /coverage/ 7 | /InstalledFiles 8 | /pkg/ 9 | /spec/reports/ 10 | /test/tmp/ 11 | /test/version_tmp/ 12 | /tmp/ 13 | 14 | ## Specific to RubyMotion: 15 | .dat* 16 | .repl_history 17 | build/ 18 | 19 | ## Documentation cache and generated files: 20 | /.yardoc/ 21 | /_yardoc/ 22 | /doc/ 23 | /rdoc/ 24 | 25 | ## Environment normalisation: 26 | /.bundle/ 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | # Gemfile.lock 32 | # .ruby-version 33 | # .ruby-gemset 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | 38 | /node_modules 39 | /package-lock.json 40 | -------------------------------------------------------------------------------- /lib/googleauth/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Google 16 | # Module Auth provides classes that provide Google-specific authorization 17 | # used to access Google APIs. 18 | module Auth 19 | VERSION = "1.16.0".freeze 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /samples/Gemfile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | source "https://rubygems.org" 16 | 17 | gem "google-cloud-language-v1" 18 | gem "google-cloud-storage" 19 | 20 | group :test do 21 | gem "minitest", "~> 5.16" 22 | gem "minitest-focus", "~> 1.1" 23 | gem "toys-core", "~> 0.15" 24 | end 25 | -------------------------------------------------------------------------------- /samples/acceptance/helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "minitest/autorun" 16 | require "minitest/focus" 17 | require "toys/utils/git_cache" 18 | require Toys::Utils::GitCache.new.get "https://github.com/googleapis/ruby-common-tools.git", 19 | path: "lib/sample_loader.rb", update: 300 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this library 4 | 5 | --- 6 | 7 | Thanks for stopping by to let us know something could be better! 8 | 9 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.kokoro/presubmit/samples.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Download resources for system tests (service account key, etc.) 11 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-ruby" 12 | 13 | # Download trampoline resources. 14 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 15 | 16 | # Use the trampoline script to run in docker. 17 | build_file: "google-auth-library-ruby/.kokoro/trampoline_v2.sh" 18 | 19 | # Configure the docker image for kokoro-trampoline. 20 | env_vars: { 21 | key: "TRAMPOLINE_IMAGE" 22 | value: "gcr.io/cloud-devrel-kokoro-resources/yoshi-ruby/release" 23 | } 24 | 25 | env_vars: { 26 | key: "TRAMPOLINE_BUILD_FILE" 27 | value: ".kokoro/samples.sh" 28 | } 29 | 30 | env_vars: { 31 | key: "SECRET_MANAGER_KEYS" 32 | value: "ruby-main-ci-service-account" 33 | } 34 | -------------------------------------------------------------------------------- /.toys/release.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if ENV["RUBY_COMMON_TOOLS"] 18 | common_tools_dir = File.expand_path ENV["RUBY_COMMON_TOOLS"] 19 | load File.join(common_tools_dir, "toys", "release") 20 | else 21 | load_git remote: "https://github.com/googleapis/ruby-common-tools.git", 22 | path: "toys/release", 23 | update: true 24 | end 25 | -------------------------------------------------------------------------------- /samples/acceptance/auth_cloud_idtoken_metadata_server_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth" 16 | require_relative "helper" 17 | 18 | describe "Get an ID token from the metadata server" do 19 | let(:url) { "https://pubsub.googleapis.com/" } 20 | 21 | it "get_an_id_token" do 22 | sample = SampleLoader.load "auth_cloud_idtoken_metadata_server.rb" 23 | 24 | assert_output(/Generated ID token./) do 25 | sample.run url: url 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | Thanks for stopping by to let us know something could be better! 8 | 9 | **PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. 10 | 11 | Please run down the following list and make sure you've tried the usual "quick fixes": 12 | 13 | - Search the issues already opened: https://github.com/googleapis/google-auth-library-ruby/issues 14 | - Search Stack Overflow: https://stackoverflow.com/questions/tagged/google-auth-library-ruby 15 | 16 | If you are still having issues, please be sure to include as much information as possible: 17 | 18 | #### Environment details 19 | 20 | - OS: 21 | - Ruby version: 22 | - Gem name and version: 23 | 24 | #### Steps to reproduce 25 | 26 | 1. ... 27 | 28 | #### Code example 29 | 30 | ```ruby 31 | # example 32 | ``` 33 | 34 | Making sure to follow these steps will guarantee the quickest resolution possible. 35 | 36 | Thanks! 37 | -------------------------------------------------------------------------------- /samples/acceptance/authenticate_implicit_with_adc_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "google/cloud/storage" 16 | 17 | require_relative "helper" 18 | 19 | describe "Authenticate Implicit with ADC Samples" do 20 | let(:storage_client) { Google::Cloud::Storage.new } 21 | 22 | it "list_buckets" do 23 | # list_buckets 24 | sample = SampleLoader.load "authenticate_implicit_with_adc.rb" 25 | 26 | assert_output(/Plaintext: Listed all storage buckets./) do 27 | sample.run project_id: storage_client.project 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "minitest/autorun" 16 | require "minitest/focus" 17 | require "webmock/minitest" 18 | 19 | require "googleauth" 20 | 21 | ## 22 | # A simple in-memory implementation of TokenStore 23 | # for UserAuthorizer initialization when testing 24 | class TestTokenStore 25 | def initialize 26 | @tokens = {} 27 | end 28 | 29 | def load id 30 | @tokens[id] 31 | end 32 | 33 | def store id, token 34 | @tokens[id] = token 35 | end 36 | 37 | def delete id 38 | @tokens.delete id 39 | end 40 | end -------------------------------------------------------------------------------- /.toys/samples.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | expand :minitest do |t| 16 | t.name = "test" 17 | t.libs = ["lib", "samples"] 18 | t.use_bundler on_missing: :install, gemfile_path: "samples/Gemfile" 19 | t.files = "samples/acceptance/*_test.rb" 20 | end 21 | 22 | desc "Run samples tests" 23 | 24 | include :exec 25 | include :terminal, styled: true 26 | 27 | def run 28 | require "json" 29 | require "repo_context" 30 | RepoContext.load_kokoro_env 31 | 32 | Dir.chdir context_directory 33 | 34 | puts "Samples tests ...", :bold, :cyan 35 | exec_tool ["samples", "test"], name: "Samples tests" 36 | end 37 | -------------------------------------------------------------------------------- /spec/googleauth/stores/redis_token_store_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift spec_dir 17 | $LOAD_PATH.uniq! 18 | 19 | require "googleauth" 20 | require "googleauth/stores/redis_token_store" 21 | require "spec_helper" 22 | require "fakeredis/rspec" 23 | require "googleauth/stores/store_examples" 24 | 25 | describe Google::Auth::Stores::RedisTokenStore do 26 | let :redis do 27 | Redis.new 28 | end 29 | 30 | let :store do 31 | Google::Auth::Stores::RedisTokenStore.new redis: redis 32 | end 33 | 34 | it_behaves_like "token store" 35 | end 36 | -------------------------------------------------------------------------------- /lib/googleauth.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/application_default" 16 | require "googleauth/api_key" 17 | require "googleauth/bearer_token" 18 | require "googleauth/client_id" 19 | require "googleauth/credentials" 20 | require "googleauth/default_credentials" 21 | require "googleauth/errors" 22 | require "googleauth/external_account" 23 | require "googleauth/id_tokens" 24 | require "googleauth/impersonated_service_account" 25 | require "googleauth/service_account" 26 | require "googleauth/service_account_jwt_header" 27 | require "googleauth/user_authorizer" 28 | require "googleauth/user_refresh" 29 | require "googleauth/web_user_authorizer" 30 | -------------------------------------------------------------------------------- /samples/acceptance/authenticate_with_api_key_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative "../authenticate_with_api_key" 16 | require_relative "helper" 17 | require "minitest/autorun" 18 | 19 | describe "authenticate_with_api_key" do 20 | let(:api_key) { ENV["GOOGLE_API_KEY"] } 21 | 22 | it "authenticates with API key" do 23 | skip "No API key available" if api_key.nil? || api_key.empty? 24 | 25 | stdout_output = capture_io { authenticate_with_api_key api_key } 26 | 27 | output = stdout_output[0] 28 | assert_includes output, "Text: Hello, world!" 29 | assert_includes output, "Sentiment:" 30 | assert_includes output, "Successfully authenticated using the API key" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /.toys/.lib/repo_context.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | class RepoContext 16 | @loaded_env = false 17 | 18 | def self.load_kokoro_env 19 | return if @loaded_env 20 | @loaded_env = true 21 | 22 | gfile_dir = ::ENV["KOKORO_GFILE_DIR"] 23 | return unless gfile_dir 24 | 25 | filename = "#{gfile_dir}/ruby_env_vars.json" 26 | raise "#{filename} is not a file" unless ::File.file? filename 27 | env_vars = ::JSON.parse ::File.read filename 28 | env_vars.each { |k, v| ::ENV[k] ||= v } 29 | 30 | filename = "#{gfile_dir}/secret_manager/ruby-main-ci-service-account" 31 | raise "#{filename} is not a file" unless ::File.file? filename 32 | ::ENV["GOOGLE_APPLICATION_CREDENTIALS"] = filename 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /.toys/.toys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | expand :clean, paths: :gitignore 18 | 19 | expand :rspec do |t| 20 | t.libs = ["lib", "spec"] 21 | t.use_bundler 22 | end 23 | 24 | expand :minitest do |t| 25 | t.libs = ["lib", "test"] 26 | t.use_bundler 27 | t.files = "test/**/*_test.rb" 28 | end 29 | 30 | expand :minitest do |t| 31 | t.name = "integration" 32 | t.libs = ["lib", "integration"] 33 | t.use_bundler 34 | t.files = "integration/**/*_test.rb" 35 | end 36 | 37 | expand :rubocop, bundler: true 38 | 39 | expand :yardoc do |t| 40 | t.generate_output_flag = true 41 | # t.fail_on_warning = true 42 | t.use_bundler 43 | end 44 | alias_tool :yard, :yardoc 45 | 46 | expand :gem_build 47 | 48 | expand :gem_build, name: "install", install_gem: true 49 | -------------------------------------------------------------------------------- /lib/googleauth/helpers/connection.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "faraday" 16 | 17 | module Google 18 | # Module Auth provides classes that provide Google-specific authorization 19 | # used to access Google APIs. 20 | module Auth 21 | # Helpers provides utility methods for Google::Auth. 22 | module Helpers 23 | # Connection provides a Faraday connection for use with Google::Auth. 24 | module Connection 25 | module_function 26 | 27 | def default_connection 28 | @default_connection 29 | end 30 | 31 | def default_connection= conn 32 | @default_connection = conn 33 | end 34 | 35 | def connection 36 | @default_connection || Faraday.default_connection 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/googleauth/stores/file_token_store_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift spec_dir 17 | $LOAD_PATH.uniq! 18 | 19 | require "googleauth" 20 | require "googleauth/stores/file_token_store" 21 | require "spec_helper" 22 | require "fakefs/safe" 23 | require "fakefs/spec_helpers" 24 | require "googleauth/stores/store_examples" 25 | 26 | module FakeFS 27 | class File 28 | # FakeFS doesn't implement. And since we don't need to actually lock, 29 | # just stub out... 30 | def flock *; end 31 | end 32 | end 33 | 34 | describe Google::Auth::Stores::FileTokenStore do 35 | include FakeFS::SpecHelpers 36 | 37 | let :store do 38 | Google::Auth::Stores::FileTokenStore.new file: "/tokens.yaml" 39 | end 40 | 41 | it_behaves_like "token store" 42 | end 43 | -------------------------------------------------------------------------------- /.toys/ci.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | desc "Run CI checks" 18 | 19 | TESTS = ["test", "integration", "spec", "rubocop", "yardoc", "build", "linkinator"] 20 | 21 | flag :only 22 | TESTS.each do |name| 23 | flag "include_#{name}".to_sym, "--[no-]include-#{name}" 24 | end 25 | 26 | include :exec, result_callback: :handle_result 27 | include :terminal 28 | 29 | def handle_result result 30 | if result.success? 31 | puts "** #{result.name} passed\n\n", :green, :bold 32 | else 33 | puts "** CI terminated: #{result.name} failed!", :red, :bold 34 | exit 1 35 | end 36 | end 37 | 38 | def run 39 | ::Dir.chdir context_directory 40 | TESTS.each do |name| 41 | setting = get "include_#{name}".to_sym 42 | setting = !only if setting.nil? 43 | exec ["toys", name], name: name.capitalize if setting 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/googleauth/stores/store_examples.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift spec_dir 17 | $LOAD_PATH.uniq! 18 | 19 | require "spec_helper" 20 | 21 | shared_examples "token store" do 22 | before :each do 23 | store.store "default", "test" 24 | end 25 | 26 | it "should return a stored value" do 27 | expect(store.load("default")).to eq "test" 28 | end 29 | 30 | it "should return nil for missing tokens" do 31 | expect(store.load("notavalidkey")).to be_nil 32 | end 33 | 34 | it "should return nil for deleted tokens" do 35 | store.delete "default" 36 | expect(store.load("default")).to be_nil 37 | end 38 | 39 | it "should save overwrite values on store" do 40 | store.store "default", "test2" 41 | expect(store.load("default")).to eq "test2" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /.kokoro/release.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Use the trampoline script to run in docker. 11 | build_file: "google-auth-library-ruby/.kokoro/trampoline_v2.sh" 12 | 13 | # Configure the docker image for kokoro-trampoline. 14 | env_vars: { 15 | key: "TRAMPOLINE_IMAGE" 16 | value: "us-central1-docker.pkg.dev/cloud-sdk-release-custom-pool/release-images/ruby-release" 17 | } 18 | 19 | env_vars: { 20 | key: "TRAMPOLINE_BUILD_FILE" 21 | value: ".kokoro/release.sh" 22 | } 23 | 24 | env_vars: { 25 | key: "SECRET_MANAGER_PROJECT_ID" 26 | value: "cloud-sdk-release-custom-pool" 27 | } 28 | 29 | env_vars: { 30 | key: "SECRET_MANAGER_KEYS" 31 | value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" 32 | } 33 | 34 | # Pick up Rubygems key from internal keystore 35 | before_action { 36 | fetch_keystore { 37 | keystore_resource { 38 | keystore_config_id: 73713 39 | keyname: "rubygems-publish-key" 40 | backend: "blade:keystore-fastconfigpush" 41 | } 42 | } 43 | } 44 | 45 | # Store the packages uploaded to rubygems.org, which 46 | # we can later use to generate SBOMs and attestations. 47 | action { 48 | define_artifacts { 49 | regex: "github/google-auth-library-ruby/pkg/*.gem" 50 | strip_prefix: "github" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.toys/linkinator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | desc "Run Link checks" 18 | 19 | flag :install, desc: "Install linkinator instead of running checks" 20 | 21 | include :exec, e: true 22 | include :terminal 23 | 24 | def run 25 | ::Dir.chdir context_directory 26 | if install 27 | Kernel.exec "npm install linkinator" 28 | else 29 | exec_tool ["yardoc"] 30 | check_links 31 | end 32 | end 33 | 34 | def check_links 35 | result = exec ["npx", "linkinator", "./doc", "--skip", "stackoverflow.com"], out: :capture 36 | puts result.captured_out 37 | checked_links = result.captured_out.split "\n" 38 | checked_links.select! { |link| link =~ /^\[(\d+)\]/ && ::Regexp.last_match[1] != "200" } 39 | unless checked_links.empty? 40 | checked_links.each do |link| 41 | puts link, :yellow 42 | end 43 | exit 1 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /samples/auth_cloud_idtoken_metadata_server.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # [START auth_cloud_idtoken_metadata_server] 16 | require "googleauth" 17 | 18 | ## 19 | # Uses the Google Cloud metadata server environment to create an identity token 20 | # and add it to the HTTP request as part of an Authorization header. 21 | # 22 | # @param url [String] The url or target audience to obtain the ID token for 23 | # (e.g. "http://www.example.com") 24 | # 25 | def auth_cloud_idtoken_metadata_server url: 26 | # Create the GCECredentials client. 27 | id_client = Google::Auth::GCECredentials.new target_audience: url 28 | 29 | # Get the ID token. 30 | # Once you've obtained the ID token, you can use it to make an authenticated call 31 | # to the target audience. 32 | id_client.fetch_access_token 33 | puts "Generated ID token." 34 | 35 | id_client.refresh! 36 | end 37 | # [END auth_cloud_idtoken_metadata_server] 38 | -------------------------------------------------------------------------------- /googleauth.gemspec: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | # encoding: utf-8 3 | 4 | $LOAD_PATH.push File.expand_path("lib", __dir__) 5 | require "googleauth/version" 6 | 7 | Gem::Specification.new do |gem| 8 | gem.name = "googleauth" 9 | gem.version = Google::Auth::VERSION 10 | 11 | gem.authors = ["Google LLC"] 12 | gem.email = ["googleapis-packages@google.com"] 13 | gem.summary = "Google Auth Library for Ruby" 14 | gem.description = "Implements simple authorization for accessing Google APIs, and provides support for " \ 15 | "Application Default Credentials." 16 | gem.homepage = "https://github.com/googleapis/google-auth-library-ruby" 17 | gem.license = "Apache-2.0" 18 | 19 | gem.files = Dir.glob("lib/**/*.rb") + Dir.glob("*.md") + ["LICENSE", ".yardopts"] 20 | gem.require_paths = ["lib"] 21 | 22 | gem.platform = Gem::Platform::RUBY 23 | gem.required_ruby_version = ">= 3.0" 24 | 25 | gem.add_dependency "faraday", ">= 1.0", "< 3.a" 26 | gem.add_dependency "google-cloud-env", "~> 2.2" 27 | gem.add_dependency "google-logging-utils", "~> 0.1" 28 | gem.add_dependency "jwt", ">= 1.4", "< 4.0" 29 | gem.add_dependency "multi_json", "~> 1.11" 30 | gem.add_dependency "os", ">= 0.9", "< 2.0" 31 | gem.add_dependency "signet", ">= 0.16", "< 2.a" 32 | 33 | if gem.respond_to? :metadata 34 | gem.metadata["changelog_uri"] = "https://github.com/googleapis/google-auth-library-ruby/blob/main/CHANGELOG.md" 35 | gem.metadata["source_code_uri"] = "https://github.com/googleapis/google-auth-library-ruby" 36 | gem.metadata["bug_tracker_uri"] = "https://github.com/googleapis/google-auth-library-ruby/issues" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | jobs: 11 | CI: 12 | if: ${{ github.repository == 'googleapis/google-auth-library-ruby' }} 13 | strategy: 14 | matrix: 15 | include: 16 | - os: ubuntu-22.04 17 | ruby: "3.1" 18 | task: test , spec 19 | - os: ubuntu-22.04 20 | ruby: "3.2" 21 | task: test , spec 22 | - os: ubuntu-latest 23 | ruby: "3.3" 24 | task: test , spec 25 | - os: ubuntu-latest 26 | ruby: "3.4" 27 | task: test , spec 28 | - os: macos-latest 29 | ruby: "3.4" 30 | task: test , spec 31 | - os: windows-latest 32 | ruby: "3.4" 33 | task: test , spec 34 | - os: ubuntu-latest 35 | ruby: "3.4" 36 | task: rubocop , integration , build , yardoc , linkinator 37 | fail-fast: false 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - name: Checkout repo 41 | uses: actions/checkout@v4 42 | - name: Install Ruby ${{ matrix.ruby }} 43 | uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: "${{ matrix.ruby }}" 46 | - name: Install NodeJS 18.x 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: "18.x" 50 | - name: Install dependencies 51 | shell: bash 52 | run: | 53 | gem install --no-document toys 54 | bundle install 55 | - name: Test ${{ matrix.task }} 56 | shell: bash 57 | run: toys do ${{ matrix.task }} < /dev/null 58 | -------------------------------------------------------------------------------- /.trampolinerc: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Add required env vars here. 16 | required_envvars+=( 17 | ) 18 | 19 | # Add env vars which are passed down into the container here. 20 | pass_down_envvars+=( 21 | "AUTORELEASE_PR" 22 | "EXTRA_CI_ARGS" 23 | "KOKORO_GIT_COMMIT" 24 | "RELEASE_DRY_RUN" 25 | "RELEASE_PACKAGE" 26 | "RUBY_VERSIONS" 27 | ) 28 | 29 | # Prevent unintentional override on the default image. 30 | if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then 31 | echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image." 32 | exit 1 33 | fi 34 | 35 | # Define the default value if it makes sense. 36 | if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then 37 | TRAMPOLINE_IMAGE_UPLOAD="" 38 | fi 39 | 40 | if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then 41 | TRAMPOLINE_IMAGE="" 42 | fi 43 | 44 | if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then 45 | TRAMPOLINE_DOCKERFILE="" 46 | fi 47 | 48 | if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then 49 | TRAMPOLINE_BUILD_FILE="" 50 | fi 51 | 52 | # Secret Manager secrets. 53 | source ${PROJECT_ROOT}/.kokoro/populate-secrets.sh 54 | -------------------------------------------------------------------------------- /lib/googleauth/stores/file_token_store.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "yaml/store" 16 | require "googleauth/token_store" 17 | 18 | module Google 19 | module Auth 20 | module Stores 21 | # Implementation of user token storage backed by a local YAML file 22 | class FileTokenStore < Google::Auth::TokenStore 23 | # Create a new store with the supplied file. 24 | # 25 | # @param [String, File] file 26 | # Path to storage file 27 | def initialize options = {} 28 | super() 29 | path = options[:file] 30 | @store = YAML::Store.new path 31 | end 32 | 33 | # (see Google::Auth::Stores::TokenStore#load) 34 | def load id 35 | @store.transaction { @store[id] } 36 | end 37 | 38 | # (see Google::Auth::Stores::TokenStore#store) 39 | def store id, token 40 | @store.transaction { @store[id] = token } 41 | end 42 | 43 | # (see Google::Auth::Stores::TokenStore#delete) 44 | def delete id 45 | @store.transaction { @store.delete id } 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/googleauth/token_store.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Google 16 | module Auth 17 | # Interface definition for token stores. It is not required that 18 | # implementations inherit from this class. It is provided for documentation 19 | # purposes to illustrate the API contract. 20 | class TokenStore 21 | class << self 22 | attr_accessor :default 23 | end 24 | 25 | # Load the token data from storage for the given ID. 26 | # 27 | # @param [String] id 28 | # ID of token data to load. 29 | # @return [String] 30 | # The loaded token data. 31 | def load _id 32 | raise NoMethodError, "load not implemented" 33 | end 34 | 35 | # Put the token data into storage for the given ID. 36 | # 37 | # @param [String] id 38 | # ID of token data to store. 39 | # @param [String] token 40 | # The token data to store. 41 | def store _id, _token 42 | raise NoMethodError, "store not implemented" 43 | end 44 | 45 | # Remove the token data from storage for the given ID. 46 | # 47 | # @param [String] id 48 | # ID of the token data to delete 49 | def delete _id 50 | raise NoMethodError, "delete not implemented" 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/googleauth/id_tokens/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require "googleauth/errors" 18 | 19 | 20 | module Google 21 | module Auth 22 | module IDTokens 23 | ## 24 | # Failed to obtain keys from the key source. 25 | # 26 | class KeySourceError < StandardError 27 | include Google::Auth::Error 28 | end 29 | 30 | ## 31 | # Failed to verify a token. 32 | # 33 | class VerificationError < StandardError 34 | include Google::Auth::Error 35 | end 36 | 37 | ## 38 | # Failed to verify token because it is expired. 39 | # 40 | class ExpiredTokenError < VerificationError; end 41 | 42 | ## 43 | # Failed to verify token because its signature did not match. 44 | # 45 | class SignatureError < VerificationError; end 46 | 47 | ## 48 | # Failed to verify token because its issuer did not match. 49 | # 50 | class IssuerMismatchError < VerificationError; end 51 | 52 | ## 53 | # Failed to verify token because its audience did not match. 54 | # 55 | class AudienceMismatchError < VerificationError; end 56 | 57 | ## 58 | # Failed to verify token because its authorized party did not match. 59 | # 60 | class AuthorizedPartyMismatchError < VerificationError; end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/googleauth/json_key_reader.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/errors" 16 | 17 | module Google 18 | # Module Auth provides classes that provide Google-specific authorization 19 | # used to access Google APIs. 20 | module Auth 21 | # JsonKeyReader contains the behaviour used to read private key and 22 | # client email fields from the service account 23 | module JsonKeyReader 24 | # Reads a JSON key from an IO object and extracts common fields. 25 | # 26 | # @param json_key_io [IO] An IO object containing the JSON key 27 | # @return [Array(String, String, String, String, String)] An array containing: 28 | # private_key, client_email, project_id, quota_project_id, and universe_domain 29 | # @raise [Google::Auth::InitializationError] If client_email or private_key 30 | # fields are missing from the JSON 31 | def read_json_key json_key_io 32 | json_key = MultiJson.load json_key_io.read 33 | raise InitializationError, "missing client_email" unless json_key.key? "client_email" 34 | raise InitializationError, "missing private_key" unless json_key.key? "private_key" 35 | [ 36 | json_key["private_key"], 37 | json_key["client_email"], 38 | json_key["project_id"], 39 | json_key["quota_project_id"], 40 | json_key["universe_domain"] 41 | ] 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = __dir__ 16 | root_dir = File.expand_path File.join(spec_dir, "..") 17 | lib_dir = File.expand_path File.join(root_dir, "lib") 18 | 19 | $LOAD_PATH.unshift spec_dir 20 | $LOAD_PATH.unshift lib_dir 21 | $LOAD_PATH.uniq! 22 | 23 | require "faraday" 24 | require "rspec" 25 | require "logging" 26 | require "rspec/logging_helper" 27 | require "webmock/rspec" 28 | require "multi_json" 29 | require "google/cloud/env" 30 | 31 | # Preload adapter to work around Rubinius error with FakeFS 32 | MultiJson.use :json_gem 33 | 34 | # Allow Faraday to support test stubs 35 | Faraday::Adapter.lookup_middleware :test 36 | 37 | # Configure RSpec to capture log messages for each test. The output from the 38 | # logs will be stored in the @log_output variable. It is a StringIO instance. 39 | RSpec.configure do |config| 40 | include RSpec::LoggingHelper 41 | config.capture_log_messages 42 | config.include WebMock::API 43 | config.filter_run focus: true 44 | config.run_all_when_everything_filtered = true 45 | end 46 | 47 | module TestHelpers 48 | include WebMock::API 49 | include WebMock::Matchers 50 | end 51 | 52 | class DummyTokenStore 53 | def initialize 54 | @tokens = {} 55 | end 56 | 57 | def load id 58 | @tokens[id] 59 | end 60 | 61 | def store id, token 62 | @tokens[id] = token 63 | end 64 | 65 | def delete id 66 | @tokens.delete id 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /samples/authenticate_implicit_with_adc.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # [START auth_cloud_implicit_adc] 16 | def authenticate_implicit_with_adc project_id: 17 | # The ID of your Google Cloud project 18 | # project_id = "your-google-cloud-project-id" 19 | 20 | ### 21 | # When interacting with Google Cloud Client libraries, the library can auto-detect the 22 | # credentials to use. 23 | # TODO(Developer): 24 | # 1. Before running this sample, 25 | # set up ADC as described in https://cloud.google.com/docs/authentication/external/set-up-adc 26 | # 2. Replace the project variable. 27 | # 3. Make sure that the user account or service account that you are using 28 | # has the required permissions. For this sample, you must have "storage.buckets.list". 29 | ### 30 | 31 | require "google/cloud/storage" 32 | 33 | # This sample demonstrates how to list buckets. 34 | # *NOTE*: Replace the client created below with the client required for your application. 35 | # Note that the credentials are not specified when constructing the client. 36 | # Hence, the client library will look for credentials using ADC. 37 | storage = Google::Cloud::Storage.new project_id: project_id 38 | buckets = storage.buckets 39 | puts "Buckets: " 40 | buckets.each do |bucket| 41 | puts bucket.name 42 | end 43 | puts "Plaintext: Listed all storage buckets." 44 | end 45 | # [END auth_cloud_implicit_adc] 46 | 47 | authenticate_implicit_with_adc project_id: ARGV.shift if $PROGRAM_NAME == __FILE__ 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, 4 | and in the interest of fostering an open and welcoming community, 5 | we pledge to respect all people who contribute through reporting issues, 6 | posting feature requests, updating documentation, 7 | submitting pull requests or patches, and other activities. 8 | 9 | We are committed to making participation in this project 10 | a harassment-free experience for everyone, 11 | regardless of level of experience, gender, gender identity and expression, 12 | sexual orientation, disability, personal appearance, 13 | body size, race, ethnicity, age, religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing other's private information, 22 | such as physical or electronic 23 | addresses, without explicit permission 24 | * Other unethical or unprofessional conduct. 25 | 26 | Project maintainers have the right and responsibility to remove, edit, or reject 27 | comments, commits, code, wiki edits, issues, and other contributions 28 | that are not aligned to this Code of Conduct. 29 | By adopting this Code of Conduct, 30 | project maintainers commit themselves to fairly and consistently 31 | applying these principles to every aspect of managing this project. 32 | Project maintainers who do not follow or enforce the Code of Conduct 33 | may be permanently removed from the project team. 34 | 35 | This code of conduct applies both within project spaces and in public spaces 36 | when an individual is representing the project or its community. 37 | 38 | Instances of abusive, harassing, or otherwise unacceptable behavior 39 | may be reported by opening an issue 40 | or contacting one or more of the project maintainers. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, 43 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 44 | -------------------------------------------------------------------------------- /integration/id_tokens/key_source_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "helper" 16 | 17 | describe Google::Auth::IDTokens do 18 | describe "key source" do 19 | let(:legacy_oidc_key_source) { 20 | Google::Auth::IDTokens::X509CertHttpKeySource.new "https://www.googleapis.com/oauth2/v1/certs" 21 | } 22 | let(:oidc_key_source) { Google::Auth::IDTokens.oidc_key_source } 23 | let(:iap_key_source) { Google::Auth::IDTokens.iap_key_source } 24 | 25 | it "Gets real keys from the OAuth2 V1 cert URL" do 26 | keys = legacy_oidc_key_source.refresh_keys 27 | refute_empty keys 28 | keys.each do |key| 29 | assert_kind_of OpenSSL::PKey::RSA, key.key 30 | refute key.key.private? 31 | assert_equal "RS256", key.algorithm 32 | end 33 | end 34 | 35 | it "Gets real keys from the OAuth2 V3 cert URL" do 36 | keys = oidc_key_source.refresh_keys 37 | refute_empty keys 38 | keys.each do |key| 39 | assert_kind_of OpenSSL::PKey::RSA, key.key 40 | refute key.key.private? 41 | assert_equal "RS256", key.algorithm 42 | end 43 | end 44 | 45 | it "Gets the same keys from the OAuth2 V1 and V3 cert URLs" do 46 | keys_v1 = legacy_oidc_key_source.refresh_keys.map(&:key).map(&:export).sort 47 | keys_v3 = oidc_key_source.refresh_keys.map(&:key).map(&:export).sort 48 | assert_equal keys_v1, keys_v3 49 | end 50 | 51 | it "Gets real keys from the IAP public key URL" do 52 | keys = iap_key_source.refresh_keys 53 | refute_empty keys 54 | keys.each do |key| 55 | assert_kind_of OpenSSL::PKey::EC, key.key 56 | assert_equal "ES256", key.algorithm 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/scope_util_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "helper" 16 | 17 | describe Google::Auth::ScopeUtil do 18 | scope_util_normalization_specs = Module.new do 19 | extend Minitest::Spec::DSL 20 | 21 | let(:normalized) { Google::Auth::ScopeUtil.normalize source } 22 | 23 | it "normalizes the email scope" do 24 | _(normalized).must_include( 25 | "https://www.googleapis.com/auth/userinfo.email" 26 | ) 27 | _(normalized).wont_include "email" 28 | end 29 | 30 | it "normalizes the profile scope" do 31 | _(normalized).must_include( 32 | "https://www.googleapis.com/auth/userinfo.profile" 33 | ) 34 | _(normalized).wont_include "profile" 35 | end 36 | 37 | it "normalizes the openid scope" do 38 | _(normalized).must_include "https://www.googleapis.com/auth/plus.me" 39 | _(normalized).wont_include "openid" 40 | end 41 | 42 | it "leaves other other scopes as-is" do 43 | _(normalized).must_include "https://www.googleapis.com/auth/drive" 44 | end 45 | end 46 | 47 | describe "with scope as string" do 48 | let :source do 49 | "email profile openid https://www.googleapis.com/auth/drive" 50 | end 51 | include scope_util_normalization_specs 52 | end 53 | 54 | describe "with scope as Array" do 55 | let :source do 56 | ["email", "profile", "openid", "https://www.googleapis.com/auth/drive"] 57 | end 58 | include scope_util_normalization_specs 59 | end 60 | 61 | it "detects incorrect type" do 62 | assert_raises ArgumentError do 63 | Google::Auth::ScopeUtil.normalize :"https://www.googleapis.com/auth/userinfo.email" 64 | end 65 | end 66 | 67 | it "detects incorrect array element type" do 68 | assert_raises ArgumentError do 69 | Google::Auth::ScopeUtil.normalize [:"https://www.googleapis.com/auth/userinfo.email"] 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/googleauth/iam_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift spec_dir 17 | $LOAD_PATH.uniq! 18 | 19 | require "googleauth/iam" 20 | 21 | describe Google::Auth::IAMCredentials do 22 | IAMCredentials = Google::Auth::IAMCredentials 23 | let(:test_selector) { "the-test-selector" } 24 | let(:test_token) { "the-test-token" } 25 | let(:test_creds) { IAMCredentials.new test_selector, test_token } 26 | 27 | describe "#apply!" do 28 | it "should update the target hash with the iam values" do 29 | md = { foo: "bar" } 30 | test_creds.apply! md 31 | expect(md[IAMCredentials::SELECTOR_KEY]).to eq test_selector 32 | expect(md[IAMCredentials::TOKEN_KEY]).to eq test_token 33 | expect(md[:foo]).to eq "bar" 34 | end 35 | end 36 | 37 | describe "updater_proc" do 38 | it "should provide a proc that updates a hash with the iam values" do 39 | md = { foo: "bar" } 40 | the_proc = test_creds.updater_proc 41 | got = the_proc.call md 42 | expect(got[IAMCredentials::SELECTOR_KEY]).to eq test_selector 43 | expect(got[IAMCredentials::TOKEN_KEY]).to eq test_token 44 | expect(got[:foo]).to eq "bar" 45 | end 46 | end 47 | 48 | describe "#apply" do 49 | it "should not update the original hash with the iam values" do 50 | md = { foo: "bar" } 51 | test_creds.apply md 52 | expect(md[IAMCredentials::SELECTOR_KEY]).to be_nil 53 | expect(md[IAMCredentials::TOKEN_KEY]).to be_nil 54 | expect(md[:foo]).to eq "bar" 55 | end 56 | 57 | it "should return a with the iam values" do 58 | md = { foo: "bar" } 59 | got = test_creds.apply md 60 | expect(got[IAMCredentials::SELECTOR_KEY]).to eq test_selector 61 | expect(got[IAMCredentials::TOKEN_KEY]).to eq test_token 62 | expect(got[:foo]).to eq "bar" 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /samples/authenticate_with_api_key.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # [START apikeys_authenticate_api_key] 16 | require "googleauth" 17 | require "google/cloud/language/v1" 18 | 19 | def authenticate_with_api_key api_key_string 20 | # Authenticates with an API key for Google Language service. 21 | # 22 | # TODO(Developer): Uncomment the following line and set the value before running this sample. 23 | # 24 | # api_key_string = "mykey12345" 25 | # 26 | # Note: You can also set the API key via environment variable: 27 | # export GOOGLE_API_KEY=your-api-key 28 | # and use Google::Auth::APIKeyCredentials.from_env method to load it. 29 | # Example: 30 | # credentials = Google::Auth::APIKeyCredentials.from_env 31 | # if credentials.nil? 32 | # puts "No API key found in environment" 33 | # exit 34 | # end 35 | 36 | # Initialize API key credentials using the class factory method 37 | credentials = Google::Auth::APIKeyCredentials.make_creds api_key: api_key_string 38 | 39 | # Initialize the Language Service client with the API key credentials 40 | client = Google::Cloud::Language::V1::LanguageService::Client.new do |config| 41 | config.credentials = credentials 42 | end 43 | 44 | # Create a document to analyze 45 | text = "Hello, world!" 46 | document = { 47 | content: text, 48 | type: :PLAIN_TEXT 49 | } 50 | 51 | # Make a request to analyze the sentiment of the text 52 | sentiment = client.analyze_sentiment(document: document).document_sentiment 53 | 54 | puts "Text: #{text}" 55 | puts "Sentiment: #{sentiment.score}, #{sentiment.magnitude}" 56 | puts "Successfully authenticated using the API key" 57 | end 58 | # [END apikeys_authenticate_api_key] 59 | 60 | if $PROGRAM_NAME == __FILE__ 61 | api_key = ARGV[0] 62 | if api_key.nil? || api_key.empty? 63 | puts "Usage: ruby #{__FILE__} api-key" 64 | exit 65 | end 66 | authenticate_with_api_key api_key 67 | end 68 | -------------------------------------------------------------------------------- /lib/googleauth/iam.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/signet" 16 | require "googleauth/credentials_loader" 17 | require "multi_json" 18 | 19 | module Google 20 | # Module Auth provides classes that provide Google-specific authorization 21 | # used to access Google APIs. 22 | module Auth 23 | # Authenticates requests using IAM credentials. 24 | class IAMCredentials 25 | SELECTOR_KEY = "x-goog-iam-authority-selector".freeze 26 | TOKEN_KEY = "x-goog-iam-authorization-token".freeze 27 | 28 | # Initializes an IAMCredentials. 29 | # 30 | # @param selector [String] The IAM selector. 31 | # @param token [String] The IAM token. 32 | # @raise [TypeError] If selector or token is not a String 33 | def initialize selector, token 34 | raise TypeError unless selector.is_a? String 35 | raise TypeError unless token.is_a? String 36 | @selector = selector 37 | @token = token 38 | end 39 | 40 | # Adds the credential fields to the hash. 41 | # 42 | # @param a_hash [Hash] The hash to update with credentials 43 | # @return [Hash] The updated hash with credentials 44 | def apply! a_hash 45 | a_hash[SELECTOR_KEY] = @selector 46 | a_hash[TOKEN_KEY] = @token 47 | a_hash 48 | end 49 | 50 | # Returns a clone of a_hash updated with the authorization header 51 | # 52 | # @param a_hash [Hash] The hash to clone and update with credentials 53 | # @return [Hash] A new hash with credentials 54 | def apply a_hash 55 | a_copy = a_hash.clone 56 | apply! a_copy 57 | a_copy 58 | end 59 | 60 | # Returns a reference to the #apply method, suitable for passing as 61 | # a closure 62 | # 63 | # @return [Proc] A procedure that updates a hash with credentials 64 | def updater_proc 65 | proc { |a_hash, _opts = {}| apply a_hash } 66 | end 67 | 68 | # Returns the IAM authority selector as the principal 69 | # @private 70 | # @return [String] the IAM authoirty selector 71 | def principal 72 | @selector 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/json_key_reader_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative "helper" 16 | require "googleauth/json_key_reader" 17 | require "stringio" 18 | require "multi_json" 19 | 20 | class DummyKeyReader 21 | include Google::Auth::JsonKeyReader 22 | end 23 | 24 | describe Google::Auth::JsonKeyReader do 25 | let(:dummy_reader) { DummyKeyReader.new } 26 | 27 | describe "#read_json_key" do 28 | it "reads all fields from a valid JSON key" do 29 | json_key_hash = { 30 | "private_key" => "dummy-key", 31 | "client_email" => "dummy@example.com", 32 | "project_id" => "dummy-project", 33 | "quota_project_id" => "quota-project", 34 | "universe_domain" => "googleapis.com" 35 | } 36 | 37 | json_key_io = StringIO.new(MultiJson.dump(json_key_hash)) 38 | 39 | private_key, client_email, project_id, quota_project_id, universe_domain = 40 | dummy_reader.read_json_key(json_key_io) 41 | 42 | _(private_key).must_equal "dummy-key" 43 | _(client_email).must_equal "dummy@example.com" 44 | _(project_id).must_equal "dummy-project" 45 | _(quota_project_id).must_equal "quota-project" 46 | _(universe_domain).must_equal "googleapis.com" 47 | end 48 | 49 | it "raises InitializationError when client_email is missing" do 50 | json_key_hash = { 51 | "private_key" => "dummy-key" 52 | } 53 | 54 | json_key_io = StringIO.new(MultiJson.dump(json_key_hash)) 55 | 56 | error = assert_raises Google::Auth::InitializationError do 57 | dummy_reader.read_json_key(json_key_io) 58 | end 59 | 60 | _(error.message).must_equal "missing client_email" 61 | end 62 | 63 | it "raises InitializationError when private_key is missing" do 64 | json_key_hash = { 65 | "client_email" => "dummy@example.com" 66 | } 67 | 68 | json_key_io = StringIO.new(MultiJson.dump(json_key_hash)) 69 | 70 | error = assert_raises Google::Auth::InitializationError do 71 | dummy_reader.read_json_key(json_key_io) 72 | end 73 | 74 | _(error.message).must_equal "missing private_key" 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /lib/googleauth/stores/redis_token_store.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "redis" 16 | require "googleauth/token_store" 17 | 18 | module Google 19 | module Auth 20 | module Stores 21 | # Implementation of user token storage backed by Redis. Tokens 22 | # are stored as JSON using the supplied key, prefixed with 23 | # `g-user-token:` 24 | class RedisTokenStore < Google::Auth::TokenStore 25 | DEFAULT_KEY_PREFIX = "g-user-token:".freeze 26 | 27 | # Create a new store with the supplied redis client. 28 | # 29 | # @param [::Redis, String] redis 30 | # Initialized redis client to connect to. 31 | # @param [String] prefix 32 | # Prefix for keys in redis. Defaults to 'g-user-token:' 33 | # @note If no redis instance is provided, a new one is created and 34 | # the options passed through. You may include any other keys accepted 35 | # by `Redis.new` 36 | def initialize options = {} 37 | super() 38 | redis = options.delete :redis 39 | prefix = options.delete :prefix 40 | @redis = case redis 41 | when Redis 42 | redis 43 | else 44 | Redis.new options 45 | end 46 | @prefix = prefix || DEFAULT_KEY_PREFIX 47 | end 48 | 49 | # (see Google::Auth::Stores::TokenStore#load) 50 | def load id 51 | key = key_for id 52 | @redis.get key 53 | end 54 | 55 | # (see Google::Auth::Stores::TokenStore#store) 56 | def store id, token 57 | key = key_for id 58 | @redis.set key, token 59 | end 60 | 61 | # (see Google::Auth::Stores::TokenStore#delete) 62 | def delete id 63 | key = key_for id 64 | @redis.del key 65 | end 66 | 67 | private 68 | 69 | # Generate a redis key from a token ID 70 | # 71 | # @param [String] id 72 | # ID of the token 73 | # @return [String] 74 | # Redis key 75 | def key_for id 76 | @prefix + id 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/googleauth/scope_util.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/signet" 16 | require "googleauth/credentials_loader" 17 | require "multi_json" 18 | 19 | module Google 20 | module Auth 21 | ## 22 | # Small utility for normalizing scopes into canonical form. 23 | # 24 | # The canonical form of scopes is as an array of strings, each in the form 25 | # of a full URL. This utility converts space-delimited scope strings into 26 | # this form, and handles a small number of common aliases. 27 | # 28 | # This is used by UserRefreshCredentials to verify that a credential grants 29 | # a requested scope. 30 | # 31 | module ScopeUtil 32 | ## 33 | # Aliases understood by this utility 34 | # 35 | ALIASES = { 36 | "email" => "https://www.googleapis.com/auth/userinfo.email", 37 | "profile" => "https://www.googleapis.com/auth/userinfo.profile", 38 | "openid" => "https://www.googleapis.com/auth/plus.me" 39 | }.freeze 40 | 41 | ## 42 | # Normalize the input, which may be an array of scopes or a whitespace- 43 | # delimited scope string. The output is always an array, even if a single 44 | # scope is input. 45 | # 46 | # @param scope [String,Array] Input scope(s) 47 | # @return [Array] An array of scopes in canonical form. 48 | # 49 | def self.normalize scope 50 | list = as_array scope 51 | list.map { |item| ALIASES[item] || item } 52 | end 53 | 54 | ## 55 | # Ensure the input is an array. If a single string is passed in, splits 56 | # it via whitespace. Does not interpret aliases. 57 | # 58 | # @param scope [String,Array] Input scope(s) 59 | # @return [Array] Always an array of strings 60 | # @raise [ArgumentError] If the input is not a string or array of strings 61 | # 62 | def self.as_array scope 63 | case scope 64 | when Array 65 | scope.each do |item| 66 | unless item.is_a? String 67 | raise ArgumentError, "Invalid scope value: #{item.inspect}. Must be string or array" 68 | end 69 | end 70 | scope 71 | when String 72 | scope.split 73 | else 74 | raise ArgumentError, "Invalid scope value: #{scope.inspect}. Must be string or array" 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /.kokoro/populate-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2020 Google LLC. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This file is called in the early stage of `trampoline_v2.sh` to 17 | # populate secrets needed for the CI builds. 18 | 19 | set -eo pipefail 20 | 21 | function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} 22 | function msg { println "$*" >&2 ;} 23 | function println { printf '%s\n' "$(now) $*" ;} 24 | 25 | # Populates requested secrets set in SECRET_MANAGER_KEYS 26 | if [[ -z "${SECRET_MANAGER_PROJECT_ID-}" ]]; then 27 | msg "SECRET_MANAGER_PROJECT_ID is not set in environment variables, using default" 28 | SECRET_MANAGER_PROJECT_ID="cloud-devrel-kokoro-resources" 29 | fi 30 | 31 | # In Kokoro CI builds, we use the service account attached to the 32 | # Kokoro VM. This means we need to setup auth on other CI systems. 33 | # For local run, we just use the gcloud command for retrieving the 34 | # secrets. 35 | 36 | if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then 37 | GCLOUD_COMMANDS=( 38 | "docker" 39 | "run" 40 | "--entrypoint=gcloud" 41 | "--volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR}" 42 | "gcr.io/google.com/cloudsdktool/cloud-sdk" 43 | ) 44 | if [[ "${TRAMPOLINE_CI:-}" == "kokoro" ]]; then 45 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" 46 | else 47 | echo "Authentication for this CI system is not implemented yet." 48 | exit 2 49 | # TODO: Determine appropriate SECRET_LOCATION and the GCLOUD_COMMANDS. 50 | fi 51 | else 52 | # For local run, use /dev/shm or temporary directory for 53 | # KOKORO_GFILE_DIR. 54 | if [[ -d "/dev/shm" ]]; then 55 | export KOKORO_GFILE_DIR=/dev/shm 56 | else 57 | export KOKORO_GFILE_DIR=$(mktemp -d -t ci-XXXXXXXX) 58 | fi 59 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" 60 | GCLOUD_COMMANDS=("gcloud") 61 | fi 62 | 63 | msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" 64 | mkdir -p ${SECRET_LOCATION} 65 | 66 | for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") 67 | do 68 | msg "Retrieving secret ${key}" 69 | "${GCLOUD_COMMANDS[@]}" \ 70 | secrets versions access latest \ 71 | --project "${SECRET_MANAGER_PROJECT_ID}" \ 72 | --secret $key > \ 73 | "$SECRET_LOCATION/$key" 74 | if [[ $? == 0 ]]; then 75 | msg "Secret written to ${SECRET_LOCATION}/${key}" 76 | else 77 | msg "Error retrieving secret ${key}" 78 | exit 2 79 | fi 80 | done 81 | -------------------------------------------------------------------------------- /lib/googleauth/application_default.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/compute_engine" 16 | require "googleauth/default_credentials" 17 | require "googleauth/errors" 18 | 19 | module Google 20 | # Module Auth provides classes that provide Google-specific authorization 21 | # used to access Google APIs. 22 | module Auth 23 | NOT_FOUND_ERROR = <<~ERROR_MESSAGE.freeze 24 | Your credentials were not found. To set up Application Default 25 | Credentials for your environment, see 26 | https://cloud.google.com/docs/authentication/external/set-up-adc 27 | ERROR_MESSAGE 28 | 29 | module_function 30 | 31 | # Obtains the default credentials implementation to use in this 32 | # environment. 33 | # 34 | # Use this to obtain the Application Default Credentials for accessing 35 | # Google APIs. Application Default Credentials are described in detail 36 | # at https://cloud.google.com/docs/authentication/production. 37 | # 38 | # If supplied, scope is used to create the credentials instance, when it can 39 | # be applied. E.g, on google compute engine and for user credentials the 40 | # scope is ignored. 41 | # 42 | # @param scope [string|array|nil] the scope(s) to access 43 | # @param options [Hash] Connection options. These may be used to configure 44 | # the `Faraday::Connection` used for outgoing HTTP requests. For 45 | # example, if a connection proxy must be used in the current network, 46 | # you may provide a connection with with the needed proxy options. 47 | # The following keys are recognized: 48 | # * `:default_connection` The connection object to use for token 49 | # refresh requests. 50 | # * `:connection_builder` A `Proc` that creates and returns a 51 | # connection to use for token refresh requests. 52 | # * `:connection` The connection to use to determine whether GCE 53 | # metadata credentials are available. 54 | # @raise [Google::Auth::InitializationError] If the credentials cannot be found 55 | def get_application_default scope = nil, options = {} 56 | creds = DefaultCredentials.from_env(scope, options) || 57 | DefaultCredentials.from_well_known_path(scope, options) || 58 | DefaultCredentials.from_system_default_path(scope, options) 59 | return creds unless creds.nil? 60 | raise InitializationError, NOT_FOUND_ERROR unless GCECredentials.on_gce? options 61 | GCECredentials.new options.merge(scope: scope) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/googleauth/service_account/jwt_header_auth_examples.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift spec_dir 17 | $LOAD_PATH.uniq! 18 | 19 | require "spec_helper" 20 | 21 | shared_examples "jwt header auth" do |aud="https://www.googleapis.com/myservice"| 22 | context "when jwt_aud_uri is present" do 23 | let(:test_uri) { aud } 24 | let(:test_scope) { "scope/1 scope/2" } 25 | let(:auth_prefix) { "Bearer " } 26 | let(:auth_key) { ServiceAccountJwtHeaderCredentials::AUTH_METADATA_KEY } 27 | let(:jwt_uri_key) { ServiceAccountJwtHeaderCredentials::JWT_AUD_URI_KEY } 28 | 29 | def expect_is_encoded_jwt hdr 30 | expect(hdr).to_not be_nil 31 | expect(hdr.start_with?(auth_prefix)).to be true 32 | authorization = hdr[auth_prefix.length..-1] 33 | payload, = JWT.decode authorization, @key.public_key, true, algorithm: "RS256" 34 | 35 | expect(payload["aud"]).to eq(test_uri) if not test_uri.nil? 36 | expect(payload["scope"]).to eq(test_scope) if test_uri.nil? 37 | expect(payload["iss"]).to eq(client_email) 38 | end 39 | 40 | describe "#apply!" do 41 | it "should update the target hash with a jwt token" do 42 | md = { foo: "bar" } 43 | md[jwt_uri_key] = test_uri if test_uri 44 | @client.apply! md 45 | auth_header = md[auth_key] 46 | expect_is_encoded_jwt auth_header 47 | expect(md[jwt_uri_key]).to be_nil 48 | end 49 | end 50 | 51 | describe "updater_proc" do 52 | it "should provide a proc that updates a hash with a jwt token" do 53 | md = { foo: "bar" } 54 | md[jwt_uri_key] = test_uri if test_uri 55 | the_proc = @client.updater_proc 56 | got = the_proc.call md 57 | auth_header = got[auth_key] 58 | expect_is_encoded_jwt auth_header 59 | expect(got[jwt_uri_key]).to be_nil 60 | expect(md[jwt_uri_key]).to_not be_nil if test_uri 61 | end 62 | end 63 | 64 | describe "#apply" do 65 | it "should not update the original hash with a jwt token" do 66 | md = { foo: "bar" } 67 | md[jwt_uri_key] = test_uri if test_uri 68 | the_proc = @client.updater_proc 69 | got = the_proc.call md 70 | auth_header = md[auth_key] 71 | expect(auth_header).to be_nil 72 | expect(got[jwt_uri_key]).to be_nil 73 | expect(md[jwt_uri_key]).to_not be_nil if test_uri 74 | end 75 | 76 | it "should add a jwt token to the returned hash" do 77 | md = { foo: "bar" } 78 | md[jwt_uri_key] = test_uri if test_uri 79 | got = @client.apply md 80 | auth_header = got[auth_key] 81 | expect_is_encoded_jwt auth_header 82 | end 83 | end 84 | 85 | describe "#needs_access_token?" do 86 | it "should always return false" do 87 | expect(@client.needs_access_token?).to eq(false) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/googleauth/base_client.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "google/logging/message" 16 | 17 | module Google 18 | # Module Auth provides classes that provide Google-specific authorization 19 | # used to access Google APIs. 20 | module Auth 21 | # BaseClient is a class used to contain common methods that are required by any 22 | # Credentials Client, including AwsCredentials, ServiceAccountCredentials, 23 | # and UserRefreshCredentials. This is a superclass of Signet::OAuth2::Client 24 | # and has been created to create a generic interface for all credentials clients 25 | # to use, including ones which do not inherit from Signet::OAuth2::Client. 26 | module BaseClient 27 | AUTH_METADATA_KEY = :authorization 28 | 29 | # Updates a_hash updated with the authentication token 30 | def apply! a_hash, opts = {} 31 | # fetch the access token there is currently not one, or if the client 32 | # has expired 33 | fetch_access_token! opts if needs_access_token? 34 | token = send token_type 35 | a_hash[AUTH_METADATA_KEY] = "Bearer #{token}" 36 | logger&.debug do 37 | hash = Digest::SHA256.hexdigest token 38 | Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})" 39 | end 40 | 41 | a_hash[AUTH_METADATA_KEY] 42 | end 43 | 44 | # Returns a clone of a_hash updated with the authentication token 45 | def apply a_hash, opts = {} 46 | a_copy = a_hash.clone 47 | apply! a_copy, opts 48 | a_copy 49 | end 50 | 51 | # Whether the id_token or access_token is missing or about to expire. 52 | def needs_access_token? 53 | send(token_type).nil? || expires_within?(60) 54 | end 55 | 56 | # Returns a reference to the #apply method, suitable for passing as 57 | # a closure 58 | def updater_proc 59 | proc { |a_hash, opts = {}| apply a_hash, opts } 60 | end 61 | 62 | def on_refresh &block 63 | @refresh_listeners = [] unless defined? @refresh_listeners 64 | @refresh_listeners << block 65 | end 66 | 67 | def notify_refresh_listeners 68 | listeners = defined?(@refresh_listeners) ? @refresh_listeners : [] 69 | listeners.each do |block| 70 | block.call self 71 | end 72 | end 73 | 74 | def expires_within? 75 | raise NoMethodError, "expires_within? not implemented" 76 | end 77 | 78 | # The logger used to log operations on this client, such as token refresh. 79 | attr_accessor :logger 80 | 81 | # @private 82 | def principal 83 | raise NoMethodError, "principal not implemented" 84 | end 85 | 86 | private 87 | 88 | def token_type 89 | raise NoMethodError, "token_type not implemented" 90 | end 91 | 92 | def fetch_access_token! 93 | raise NoMethodError, "fetch_access_token! not implemented" 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/googleauth/oauth2/sts_client_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth" 16 | require "googleauth/oauth2/sts_client" 17 | require "spec_helper" 18 | 19 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 20 | $LOAD_PATH.unshift spec_dir 21 | $LOAD_PATH.uniq! 22 | 23 | describe Google::Auth::OAuth2::STSClient do 24 | GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze 25 | RESOURCE = "https://api.example.com/".freeze 26 | AUDIENCE = "urn:example:cooperation-context".freeze 27 | REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze 28 | SUBJECT_TOKEN = "HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE".freeze 29 | SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt".freeze 30 | TOKEN_EXCHANGE_ENDPOINT = "https://example.com/token.oauth2".freeze 31 | SUCCESS_RESPONSE = { 32 | "access_token": "ACCESS_TOKEN", 33 | "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", 34 | "token_type": "Bearer", 35 | "expires_in": 3600, 36 | "scope": "scope1 scope2", 37 | }.freeze 38 | ERROR_RESPONSE = { 39 | "error": "invalid_request", 40 | "error_description": "Invalid subject token", 41 | "error_uri": "https://tools.ietf.org/html/rfc6749", 42 | }.freeze 43 | 44 | context "with valid parameters" do 45 | let :sts_client do 46 | Google::Auth::OAuth2::STSClient.new({token_exchange_endpoint: TOKEN_EXCHANGE_ENDPOINT}) 47 | end 48 | 49 | it 'should successfully exchange a token with only required parameters' do 50 | stub_request(:post, TOKEN_EXCHANGE_ENDPOINT).to_return(status: 200, body: SUCCESS_RESPONSE.to_json) 51 | 52 | res = sts_client.exchange_token({ 53 | grant_type: GRANT_TYPE, 54 | subject_token: SUBJECT_TOKEN, 55 | subject_token_type: SUBJECT_TOKEN_TYPE, 56 | audience: AUDIENCE, 57 | requested_token_type: REQUESTED_TOKEN_TYPE 58 | }) 59 | 60 | expect(res["access_token"]).to eq(SUCCESS_RESPONSE[:access_token]) 61 | end 62 | 63 | it 'should appropriately handle an error response' do 64 | stub_request(:post, TOKEN_EXCHANGE_ENDPOINT).to_return(status: 400, body: ERROR_RESPONSE.to_json) 65 | 66 | # Expect an AuthorizationError to be raised 67 | expect { 68 | sts_client.exchange_token({ 69 | grant_type: GRANT_TYPE, 70 | subject_token: SUBJECT_TOKEN, 71 | subject_token_type: SUBJECT_TOKEN_TYPE, 72 | audience: AUDIENCE, 73 | requested_token_type: REQUESTED_TOKEN_TYPE 74 | }) 75 | }.to raise_error(Google::Auth::AuthorizationError, /Token exchange failed with status 400/) 76 | end 77 | end 78 | 79 | context "with invalid parameters" do 80 | it 'should raise an InitializationError if the token exchange endpoint is not provided' do 81 | expect { 82 | Google::Auth::OAuth2::STSClient.new 83 | }.to raise_error(Google::Auth::InitializationError, /Token exchange endpoint can not be nil/) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement 9 | (CLA). 10 | 11 | * If you are an individual writing original source code and you're sure you 12 | own the intellectual property, then you'll need to sign an [individual CLA]. 13 | * If you work for a company that wants to allow you to contribute your work, 14 | then you'll need to sign a [corporate CLA]. 15 | 16 | [individual CLA]: http://code.google.com/legal/individual-cla-v1.0.html 17 | [corporate CLA]: http://code.google.com/legal/corporate-cla-v1.0.html 18 | 19 | Follow either of the two links above to access the appropriate CLA and 20 | instructions for how to sign and return it. Once we receive it, we'll be able to 21 | accept your pull requests. 22 | 23 | ## Issue reporting 24 | 25 | * Check that the issue has not already been reported. 26 | * Check that the issue has not already been fixed in the latest code 27 | (a.k.a. the `main` branch). 28 | * Be clear, concise and precise in your description of the problem. 29 | * Open an issue with a descriptive title and a summary in grammatically correct, 30 | complete sentences. 31 | * Include any relevant code to the issue summary. 32 | 33 | ## Pull requests 34 | 35 | * Read [how to properly contribute to open source projects on Github][2]. 36 | * Fork the project. 37 | * Use a topic/feature branch to easily amend a pull request later, if necessary. 38 | * Write [good commit messages][3]. 39 | * Use the same coding conventions as the rest of the project. 40 | * Commit and push until you are happy with your contribution. 41 | * Make sure to add tests for it. This is important so I don't break it 42 | in a future version unintentionally. 43 | * Add an entry to the [Changelog](CHANGELOG.md) accordingly. See [changelog entry format](#changelog-entry-format). 44 | * Please try not to mess with the Rakefile, version, or history. If you want to 45 | have your own version, or is otherwise necessary, that is fine, but please 46 | isolate to its own commit so I can cherry-pick around it. 47 | * Make sure the test suite is passing and the code you wrote doesn't produce 48 | RuboCop offenses. 49 | * [Squash related commits together][5]. 50 | * Open a [pull request][4] that relates to *only* one subject with a clear title 51 | and description in grammatically correct, complete sentences. 52 | 53 | ### Changelog entry format 54 | 55 | Here are a few examples: 56 | 57 | ``` 58 | * makes the scope parameter's optional in all APIs. (@tbetbetbe[]) 59 | * [#14](https://github.com/google/google-auth-library-ruby/issues/14): ADC Support for JWT Service Tokens. ([@tbetbetbe][]) 60 | ``` 61 | 62 | * Mark it up in [Markdown syntax][6]. 63 | * The entry line should start with `* ` (an asterisk and a space). 64 | * If the change has a related GitHub issue (e.g. a bug fix for a reported issue), put a link to the issue as `[#123](https://github.com/google/google-auth-library-ruby/issues/11): `. 65 | * Describe the brief of the change. The sentence should end with a punctuation. 66 | * At the end of the entry, add an implicit link to your GitHub user page as `([@username][])`. 67 | * If this is your first contribution to google-auth-library-ruby project, add a link definition for the implicit link to the bottom of the changelog as `[@username]: https://github.com/username`. 68 | 69 | [1]: https://github.com/google/google-auth-ruby-library/issues 70 | [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request 71 | [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 72 | [4]: https://help.github.com/articles/using-pull-requests 73 | [5]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 74 | [6]: http://daringfireball.net/projects/markdown/syntax 75 | -------------------------------------------------------------------------------- /test/errors_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative "helper" 16 | require_relative "../lib/googleauth/errors" 17 | 18 | describe Google::Auth::Error do 19 | it "can be included in custom errors" do 20 | custom_error = Class.new(StandardError) do 21 | include Google::Auth::Error 22 | end 23 | 24 | error = custom_error.new("Custom error message") 25 | _(error).must_be_kind_of Google::Auth::Error 26 | _(error.message).must_equal "Custom error message" 27 | end 28 | end 29 | 30 | describe Google::Auth::DetailedError do 31 | it "can be included in custom errors" do 32 | custom_error = Class.new(StandardError) do 33 | include Google::Auth::DetailedError 34 | end 35 | 36 | error = custom_error.new("Custom error message") 37 | _(error).must_be_kind_of Google::Auth::DetailedError 38 | _(error).must_be_kind_of Google::Auth::Error 39 | _(error.message).must_equal "Custom error message" 40 | end 41 | 42 | it "provides a with_details factory method on including classes" do 43 | custom_error = Class.new(StandardError) do 44 | include Google::Auth::DetailedError 45 | end 46 | 47 | error = custom_error.with_details("Custom error message", 48 | credential_type_name: "TestCredential", 49 | principal: "test-principal@example.com") 50 | 51 | _(error).must_be_kind_of Google::Auth::DetailedError 52 | _(error.message).must_equal "Custom error message" 53 | _(error.credential_type_name).must_equal "TestCredential" 54 | _(error.principal).must_equal "test-principal@example.com" 55 | end 56 | end 57 | 58 | describe Google::Auth::InitializationError do 59 | it "is a StandardError" do 60 | error = Google::Auth::InitializationError.new("Init error") 61 | _(error).must_be_kind_of StandardError 62 | end 63 | 64 | it "includes the Error module" do 65 | error = Google::Auth::InitializationError.new("Init error") 66 | _(error).must_be_kind_of Google::Auth::Error 67 | end 68 | end 69 | 70 | describe Google::Auth::CredentialsError do 71 | it "is a StandardError" do 72 | error = Google::Auth::CredentialsError.new("Credential error") 73 | _(error).must_be_kind_of StandardError 74 | end 75 | 76 | it "includes the DetailedError module" do 77 | error = Google::Auth::CredentialsError.new("Credential error") 78 | _(error).must_be_kind_of Google::Auth::DetailedError 79 | end 80 | end 81 | 82 | describe Google::Auth::AuthorizationError do 83 | it "is a Signet::AuthorizationError" do 84 | error = Google::Auth::AuthorizationError.new("Auth error") 85 | _(error).must_be_kind_of Signet::AuthorizationError 86 | end 87 | 88 | it "includes the DetailedError module" do 89 | error = Google::Auth::AuthorizationError.new("Auth error") 90 | _(error).must_be_kind_of Google::Auth::DetailedError 91 | end 92 | 93 | it "can be created with detailed information" do 94 | error = Google::Auth::AuthorizationError.with_details( 95 | "Failed to authorize request", 96 | credential_type_name: "Google::Auth::ServiceAccountCredentials", 97 | principal: "service-account@example.com" 98 | ) 99 | 100 | _(error.message).must_equal "Failed to authorize request" 101 | _(error.credential_type_name).must_equal "Google::Auth::ServiceAccountCredentials" 102 | _(error.principal).must_equal "service-account@example.com" 103 | end 104 | end -------------------------------------------------------------------------------- /test/client_id_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "helper" 16 | require "fakefs/safe" 17 | 18 | describe Google::Auth::ClientId do 19 | # A set of validity checks for a loaded ClientId. 20 | # This module can be included in any spec that defines `config`. 21 | client_id_load_checks = Module.new do 22 | def self.included spec 23 | # Define a module with some checks that a client ID is valid. 24 | # Memoize because this included hook gets called multiple times. 25 | valid_config_checks = @valid_config_checks ||= Module.new do 26 | extend Minitest::Spec::DSL 27 | 28 | it "should include a valid id" do 29 | _(client_id.id).must_equal "abc@example.com" 30 | end 31 | 32 | it "should include a valid secret" do 33 | _(client_id.secret).must_equal "notasecret" 34 | end 35 | end 36 | 37 | # Add these describe blocks to any spec class that includes the 38 | # client_id_load_checks module. Each describe block, in turn, includes 39 | # the valid_config_checks module defined above. 40 | spec.instance_eval do 41 | describe "loaded from hash" do 42 | let(:client_id) { Google::Auth::ClientId.from_hash config } 43 | include valid_config_checks 44 | end 45 | 46 | describe "loaded from file" do 47 | file_path = "/client_secrets.json" 48 | let :client_id do 49 | FakeFS do 50 | content = MultiJson.dump config 51 | File.write file_path, content 52 | Google::Auth::ClientId.from_file file_path 53 | end 54 | end 55 | include valid_config_checks 56 | end 57 | end 58 | end 59 | end 60 | 61 | describe "with web config" do 62 | let :config do 63 | { 64 | "web" => { 65 | "client_id" => "abc@example.com", 66 | "client_secret" => "notasecret" 67 | } 68 | } 69 | end 70 | include client_id_load_checks 71 | end 72 | 73 | describe "with installed app config" do 74 | let :config do 75 | { 76 | "installed" => { 77 | "client_id" => "abc@example.com", 78 | "client_secret" => "notasecret" 79 | } 80 | } 81 | end 82 | include client_id_load_checks 83 | end 84 | 85 | describe "with missing top level property" do 86 | let :config do 87 | { 88 | "notvalid" => { 89 | "client_id" => "abc@example.com", 90 | "client_secret" => "notasecret" 91 | } 92 | } 93 | end 94 | 95 | it "should raise error" do 96 | error = assert_raises do 97 | Google::Auth::ClientId.from_hash config 98 | end 99 | assert_match(/Expected top level property/, error.message) 100 | end 101 | end 102 | 103 | describe "with missing client id" do 104 | let :config do 105 | { 106 | "web" => { 107 | "client_secret" => "notasecret" 108 | } 109 | } 110 | end 111 | 112 | it "should raise error" do 113 | error = assert_raises do 114 | Google::Auth::ClientId.from_hash config 115 | end 116 | assert_match(/Client id can not be nil/, error.message) 117 | end 118 | end 119 | 120 | describe "with missing client secret" do 121 | let :config do 122 | { 123 | "web" => { 124 | "client_id" => "abc@example.com" 125 | } 126 | } 127 | end 128 | 129 | it "should raise error" do 130 | error = assert_raises do 131 | Google::Auth::ClientId.from_hash config 132 | end 133 | assert_match(/Client secret can not be nil/, error.message) 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/googleauth/client_id.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "multi_json" 16 | require "googleauth/credentials_loader" 17 | require "googleauth/errors" 18 | 19 | module Google 20 | module Auth 21 | ## 22 | # Representation of an application's identity for user authorization flows. 23 | # 24 | class ClientId 25 | # Toplevel JSON key for the an installed app configuration. 26 | # Must include client_id and client_secret subkeys if present. 27 | INSTALLED_APP = "installed".freeze 28 | # Toplevel JSON key for the a webapp configuration. 29 | # Must include client_id and client_secret subkeys if present. 30 | WEB_APP = "web".freeze 31 | # JSON key for the client ID within an app configuration. 32 | CLIENT_ID = "client_id".freeze 33 | # JSON key for the client secret within an app configuration. 34 | CLIENT_SECRET = "client_secret".freeze 35 | # An error message raised when none of the expected toplevel properties 36 | # can be found. 37 | MISSING_TOP_LEVEL_ELEMENT_ERROR = 38 | "Expected top level property 'installed' or 'web' to be present.".freeze 39 | 40 | ## 41 | # Text identifier of the client ID 42 | # @return [String] 43 | # 44 | attr_reader :id 45 | 46 | ## 47 | # Secret associated with the client ID 48 | # @return [String] 49 | # 50 | attr_reader :secret 51 | 52 | class << self 53 | attr_accessor :default 54 | end 55 | 56 | ## 57 | # Initialize the Client ID. Both id and secret must be non-nil. 58 | # 59 | # @param [String] id 60 | # Text identifier of the client ID 61 | # @param [String] secret 62 | # Secret associated with the client ID 63 | # @note Direct instantiation is discouraged to avoid embedding IDs 64 | # and secrets in source. See {#from_file} to load from 65 | # `client_secrets.json` files. 66 | # @raise [Google::Auth::InitializationError] If id or secret is nil 67 | # 68 | def initialize id, secret 69 | raise InitializationError, "Client id can not be nil" if id.nil? 70 | raise InitializationError, "Client secret can not be nil" if secret.nil? 71 | @id = id 72 | @secret = secret 73 | end 74 | 75 | ## 76 | # Constructs a Client ID from a JSON file downloaded from the 77 | # Google Developers Console. 78 | # 79 | # @param [String, File] file 80 | # Path of file to read from 81 | # @return [Google::Auth::ClientID] 82 | # @raise [Google::Auth::InitializationError] If file is nil 83 | # 84 | def self.from_file file 85 | raise InitializationError, "File can not be nil." if file.nil? 86 | File.open file.to_s do |f| 87 | json = f.read 88 | config = MultiJson.load json 89 | from_hash config 90 | end 91 | end 92 | 93 | ## 94 | # Constructs a Client ID from a previously loaded JSON file. The hash 95 | # structure should match the expected JSON format. 96 | # 97 | # @param [hash] config 98 | # Parsed contents of the JSON file 99 | # @return [Google::Auth::ClientID] 100 | # @raise [Google::Auth::InitializationError] If config is nil or missing required elements 101 | # 102 | def self.from_hash config 103 | raise InitializationError, "Hash can not be nil." if config.nil? 104 | raw_detail = config[INSTALLED_APP] || config[WEB_APP] 105 | raise InitializationError, MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil? 106 | ClientId.new raw_detail[CLIENT_ID], raw_detail[CLIENT_SECRET] 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/googleauth/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "signet/oauth_2/client" 4 | 5 | # Copyright 2025 Google LLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | module Google 20 | module Auth 21 | ## 22 | # Error mixin module for Google Auth errors 23 | # All Google Auth errors should include this module 24 | # 25 | module Error; end 26 | 27 | ## 28 | # Mixin module that contains detailed error information 29 | # typically this is available if credentials initialization 30 | # succeeds and credentials object is valid 31 | # 32 | module DetailedError 33 | include Error 34 | 35 | # The type of the credentials that the error was originated from 36 | # @return [String, nil] The class name of the credential that raised the error 37 | attr_reader :credential_type_name 38 | 39 | # The principal for the authentication flow. Typically obtained from credentials 40 | # @return [String, Symbol, nil] The principal identifier associated with the credentials 41 | attr_reader :principal 42 | 43 | # All details passed in the options hash when creating the error 44 | # @return [Hash] Additional details about the error 45 | attr_reader :details 46 | 47 | # @private 48 | def self.included base 49 | base.extend ClassMethods 50 | end 51 | 52 | # Class methods to be added to including classes 53 | module ClassMethods 54 | # Creates a new error with detailed information 55 | # @param message [String] The error message 56 | # @param credential_type_name [String] The credential type that raised the error 57 | # @param principal [String, Symbol] The principal for the authentication flow 58 | # @return [Error] The new error with details 59 | def with_details message, credential_type_name:, principal: 60 | new(message).tap do |error| 61 | error.instance_variable_set :@credential_type_name, credential_type_name 62 | error.instance_variable_set :@principal, principal 63 | end 64 | end 65 | end 66 | end 67 | 68 | ## 69 | # Error raised during Credentials initialization. 70 | # All new code should use this instead of ArgumentError during initializtion. 71 | # 72 | class InitializationError < StandardError 73 | include Error 74 | end 75 | 76 | ## 77 | # Generic error raised during operation of Credentials 78 | # This should be used for all purposes not covered by other errors. 79 | # 80 | class CredentialsError < StandardError 81 | include DetailedError 82 | end 83 | 84 | ## 85 | # An error indicating the remote server refused to authorize the client. 86 | # Maintains backward compatibility with Signet. 87 | # 88 | # Should not be used in the new code, even when wrapping `Signet::AuthorizationError`. 89 | # New code should use CredentialsError instead. 90 | # 91 | class AuthorizationError < Signet::AuthorizationError 92 | include DetailedError 93 | end 94 | 95 | ## 96 | # An error indicating that the server sent an unexpected http status. 97 | # Maintains backward compatibility with Signet. 98 | # 99 | # Should not be used in the new code, even when wrapping `Signet::UnexpectedStatusError`. 100 | # New code should use CredentialsError instead. 101 | # 102 | class UnexpectedStatusError < Signet::UnexpectedStatusError 103 | include DetailedError 104 | end 105 | 106 | ## 107 | # An error indicating the client failed to parse a value. 108 | # Maintains backward compatibility with Signet. 109 | # 110 | # Should not be used in the new code, even when wrapping `Signet::ParseError`. 111 | # New code should use CredentialsError instead. 112 | # 113 | class ParseError < Signet::ParseError 114 | include DetailedError 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/api_key_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "helper" 16 | require "googleauth/api_key" 17 | require "logger" 18 | 19 | describe Google::Auth::APIKeyCredentials do 20 | let(:api_key) { "test-api-key-12345" } 21 | let(:example_universe_domain) { "example.com" } 22 | 23 | describe "#initialize" do 24 | it "creates with an API key" do 25 | creds = Google::Auth::APIKeyCredentials.new api_key: api_key 26 | _(creds.api_key).must_equal api_key 27 | _(creds.universe_domain).must_equal "googleapis.com" 28 | end 29 | 30 | it "creates with custom universe domain" do 31 | creds = Google::Auth::APIKeyCredentials.new( 32 | api_key: api_key, 33 | universe_domain: example_universe_domain 34 | ) 35 | _(creds.universe_domain).must_equal example_universe_domain 36 | end 37 | 38 | it "raises if API key is missing" do 39 | expect do 40 | Google::Auth::APIKeyCredentials.new 41 | end.must_raise ArgumentError 42 | end 43 | 44 | it "raises if API key is empty" do 45 | expect do 46 | Google::Auth::APIKeyCredentials.new(api_key: "") 47 | end.must_raise ArgumentError 48 | end 49 | end 50 | 51 | describe "#from_env" do 52 | after do 53 | ENV.delete Google::Auth::APIKeyCredentials::API_KEY_VAR 54 | end 55 | 56 | it "returns nil if environment variable not set" do 57 | ENV.delete Google::Auth::APIKeyCredentials::API_KEY_VAR 58 | creds = Google::Auth::APIKeyCredentials.from_env 59 | _(creds).must_be_nil 60 | end 61 | 62 | it "returns nil if environment variable empty" do 63 | ENV[Google::Auth::APIKeyCredentials::API_KEY_VAR] = "" 64 | creds = Google::Auth::APIKeyCredentials.from_env 65 | _(creds).must_be_nil 66 | end 67 | 68 | it "creates credentials from environment variable" do 69 | ENV[Google::Auth::APIKeyCredentials::API_KEY_VAR] = api_key 70 | creds = Google::Auth::APIKeyCredentials.from_env 71 | _(creds).must_be_instance_of Google::Auth::APIKeyCredentials 72 | _(creds.api_key).must_equal api_key 73 | end 74 | end 75 | 76 | describe "#apply!" do 77 | let(:creds) { Google::Auth::APIKeyCredentials.new api_key: api_key } 78 | 79 | it "adds API key header to hash" do 80 | md = { foo: "bar" } 81 | want = { :foo => "bar", Google::Auth::APIKeyCredentials::API_KEY_HEADER => api_key } 82 | md = creds.apply md 83 | _(md).must_equal want 84 | end 85 | 86 | it "logs when a logger is set" do 87 | strio = StringIO.new 88 | logger = Logger.new strio 89 | creds.logger = logger 90 | md = {} 91 | md = creds.apply md 92 | _(strio.string).wont_be :empty? 93 | 94 | hashed_apikey = Digest::SHA256.hexdigest(api_key) 95 | _(strio.string).must_include hashed_apikey # Check if the hash is logged. 96 | 97 | _(strio.string).wont_include api_key # Explicitly check that the raw api key is NOT logged. 98 | end 99 | end 100 | 101 | describe "#token_type" do 102 | let(:creds) { Google::Auth::APIKeyCredentials.new api_key: api_key } 103 | 104 | it "returns :api_key" do 105 | _(creds.send(:token_type)).must_equal :api_key 106 | end 107 | end 108 | 109 | describe "#duplicate" do 110 | let(:creds) { Google::Auth::APIKeyCredentials.new api_key: api_key } 111 | 112 | it "creates a duplicate with same values" do 113 | dup = creds.duplicate 114 | _(dup.api_key).must_equal api_key 115 | _(dup.universe_domain).must_equal "googleapis.com" 116 | end 117 | 118 | it "allows overriding values" do 119 | dup = creds.duplicate api_key: "new-key", universe_domain: example_universe_domain 120 | _(dup.api_key).must_equal "new-key" 121 | _(dup.universe_domain).must_equal example_universe_domain 122 | end 123 | end 124 | 125 | describe "#expires_within?" do 126 | let(:creds) { Google::Auth::APIKeyCredentials.new api_key: api_key } 127 | 128 | it "always returns false" do 129 | _(creds.expires_within?(60)).must_equal false 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/googleauth/external_account/external_account_utils.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License.require "time" 14 | 15 | require "googleauth/base_client" 16 | require "googleauth/errors" 17 | require "googleauth/helpers/connection" 18 | require "googleauth/oauth2/sts_client" 19 | 20 | module Google 21 | # Module Auth provides classes that provide Google-specific authorization 22 | # used to access Google APIs. 23 | module Auth 24 | module ExternalAccount 25 | # Authenticates requests using External Account credentials, such 26 | # as those provided by the AWS provider or OIDC provider like Azure, etc. 27 | module ExternalAccountUtils 28 | # Cloud resource manager URL used to retrieve project information. 29 | CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/".freeze 30 | 31 | ## 32 | # Retrieves the project ID corresponding to the workload identity or workforce pool. 33 | # For workforce pool credentials, it returns the project ID corresponding to the workforce_pool_user_project. 34 | # When not determinable, None is returned. 35 | # 36 | # The resource may not have permission (resourcemanager.projects.get) to 37 | # call this API or the required scopes may not be selected: 38 | # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes 39 | # 40 | # @return [String, nil] The project ID corresponding to the workload identity 41 | # pool or workforce pool if determinable 42 | # 43 | def project_id 44 | return @project_id unless @project_id.nil? 45 | project_number = self.project_number || @workforce_pool_user_project 46 | 47 | # if we missing either project number or scope, we won't retrieve project_id 48 | return nil if project_number.nil? || @scope.nil? 49 | 50 | url = "#{CLOUD_RESOURCE_MANAGER}#{project_number}" 51 | response = connection.get url do |req| 52 | req.headers["Authorization"] = "Bearer #{@access_token}" 53 | req.headers["Content-Type"] = "application/json" 54 | end 55 | 56 | if response.status == 200 57 | response_data = MultiJson.load response.body, symbolize_names: true 58 | @project_id = response_data[:projectId] 59 | end 60 | 61 | @project_id 62 | end 63 | 64 | ## 65 | # Retrieve the project number corresponding to workload identity pool 66 | # STS audience pattern: 67 | # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...` 68 | # 69 | # @return [String, nil] The project number extracted from the audience string, 70 | # or nil if it cannot be determined 71 | # 72 | def project_number 73 | segments = @audience.split "/" 74 | idx = segments.index "projects" 75 | return nil if idx.nil? || idx + 1 == segments.size 76 | segments[idx + 1] 77 | end 78 | 79 | # Normalizes a timestamp value to a Time object 80 | # 81 | # @param time [Time, String, nil] The timestamp to normalize 82 | # @return [Time, nil] The normalized timestamp or nil if input is nil 83 | # @raise [Google::Auth::CredentialsError] If the time value is not nil, Time, or String 84 | def normalize_timestamp time 85 | case time 86 | when NilClass 87 | nil 88 | when Time 89 | time 90 | when String 91 | Time.parse time 92 | else 93 | raise CredentialsError, "Invalid time value #{time}" 94 | end 95 | end 96 | 97 | # Extracts the service account email from the impersonation URL 98 | # 99 | # @return [String, nil] The service account email extracted from the 100 | # service_account_impersonation_url, or nil if it cannot be determined 101 | def service_account_email 102 | return nil if @service_account_impersonation_url.nil? 103 | start_idx = @service_account_impersonation_url.rindex "/" 104 | end_idx = @service_account_impersonation_url.index ":generateAccessToken" 105 | if start_idx != -1 && end_idx != -1 && start_idx < end_idx 106 | start_idx += 1 107 | return @service_account_impersonation_url[start_idx..end_idx] 108 | end 109 | nil 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/googleauth/id_tokens/verifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | require "jwt" 18 | 19 | module Google 20 | module Auth 21 | module IDTokens 22 | ## 23 | # An object that can verify ID tokens. 24 | # 25 | # A verifier maintains a set of default settings, including the key 26 | # source and fields to verify. However, individual verification calls can 27 | # override any of these settings. 28 | # 29 | class Verifier 30 | ## 31 | # Create a verifier. 32 | # 33 | # @param key_source [key source] The default key source to use. All 34 | # verification calls must have a key source, so if no default key 35 | # source is provided here, then calls to {#verify} _must_ provide 36 | # a key source. 37 | # @param aud [String,nil] The default audience (`aud`) check, or `nil` 38 | # for no check. 39 | # @param azp [String,nil] The default authorized party (`azp`) check, 40 | # or `nil` for no check. 41 | # @param iss [String,nil] The default issuer (`iss`) check, or `nil` 42 | # for no check. 43 | # 44 | def initialize key_source: nil, 45 | aud: nil, 46 | azp: nil, 47 | iss: nil 48 | @key_source = key_source 49 | @aud = aud 50 | @azp = azp 51 | @iss = iss 52 | end 53 | 54 | ## 55 | # Verify the given token. 56 | # 57 | # @param token [String] the ID token to verify. 58 | # @param key_source [key source] If given, override the key source. 59 | # @param aud [String,nil] If given, override the `aud` check. 60 | # @param azp [String,nil] If given, override the `azp` check. 61 | # @param iss [String,nil] If given, override the `iss` check. 62 | # 63 | # @return [Hash] the decoded payload, if verification succeeded. 64 | # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys 65 | # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed. 66 | # Additional data may be available in the error subclass and message. 67 | def verify token, 68 | key_source: :default, 69 | aud: :default, 70 | azp: :default, 71 | iss: :default 72 | key_source = @key_source if key_source == :default 73 | aud = @aud if aud == :default 74 | azp = @azp if azp == :default 75 | iss = @iss if iss == :default 76 | 77 | raise KeySourceError, "No key sources" unless key_source 78 | keys = key_source.current_keys 79 | payload = decode_token token, keys, aud, azp, iss 80 | unless payload 81 | keys = key_source.refresh_keys 82 | payload = decode_token token, keys, aud, azp, iss 83 | end 84 | raise SignatureError, "Token not verified as issued by Google" unless payload 85 | payload 86 | end 87 | 88 | private 89 | 90 | def decode_token token, keys, aud, azp, iss 91 | payload = nil 92 | keys.find do |key| 93 | options = { algorithms: key.algorithm } 94 | decoded_token = JWT.decode token, key.key, true, options 95 | payload = decoded_token.first 96 | rescue JWT::ExpiredSignature 97 | raise ExpiredTokenError, "Token signature is expired" 98 | rescue JWT::DecodeError 99 | nil # Try the next key 100 | end 101 | 102 | normalize_and_verify_payload payload, aud, azp, iss 103 | end 104 | 105 | def normalize_and_verify_payload payload, aud, azp, iss 106 | return nil unless payload 107 | 108 | # Map the legacy "cid" claim to the canonical "azp" 109 | payload["azp"] ||= payload["cid"] if payload.key? "cid" 110 | 111 | # Payload content validation 112 | if aud && (Array(aud) & Array(payload["aud"])).empty? 113 | raise AudienceMismatchError, "Token aud mismatch: #{payload['aud']}" 114 | end 115 | if azp && (Array(azp) & Array(payload["azp"])).empty? 116 | raise AuthorizedPartyMismatchError, "Token azp mismatch: #{payload['azp']}" 117 | end 118 | if iss && (Array(iss) & Array(payload["iss"])).empty? 119 | raise IssuerMismatchError, "Token iss mismatch: #{payload['iss']}" 120 | end 121 | 122 | payload 123 | end 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/googleauth/oauth2/sts_client.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/errors" 16 | require "googleauth/helpers/connection" 17 | 18 | module Google 19 | module Auth 20 | module OAuth2 21 | # OAuth 2.0 Token Exchange Spec. 22 | # This module defines a token exchange utility based on the 23 | # [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/rfc8693) spec. This will be mainly 24 | # used to exchange external credentials for GCP access tokens in workload identity pools to 25 | # access Google APIs. 26 | # The implementation will support various types of client authentication as allowed in the spec. 27 | # 28 | # A deviation on the spec will be for additional Google specific options that cannot be easily 29 | # mapped to parameters defined in the RFC. 30 | # The returned dictionary response will be based on the [rfc8693 section 2.2.1] 31 | # (https://tools.ietf.org/html/rfc8693#section-2.2.1) spec JSON response. 32 | # 33 | class STSClient 34 | include Helpers::Connection 35 | 36 | URLENCODED_HEADERS = { "Content-Type": "application/x-www-form-urlencoded" }.freeze 37 | 38 | # Create a new instance of the STSClient. 39 | # 40 | # @param [Hash] options Configuration options 41 | # @option options [String] :token_exchange_endpoint The token exchange endpoint 42 | # @option options [Faraday::Connection] :connection The Faraday connection to use 43 | # @raise [Google::Auth::InitializationError] If token_exchange_endpoint is nil 44 | def initialize options = {} 45 | raise InitializationError, "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil? 46 | self.default_connection = options[:connection] 47 | @token_exchange_endpoint = options[:token_exchange_endpoint] 48 | end 49 | 50 | # Exchanges the provided token for another type of token based on the 51 | # rfc8693 spec 52 | # 53 | # @param [Faraday instance] connection 54 | # A callable faraday instance used to make HTTP requests. 55 | # @param [String] grant_type 56 | # The OAuth 2.0 token exchange grant type. 57 | # @param [String] subject_token 58 | # The OAuth 2.0 token exchange subject token. 59 | # @param [String] subject_token_type 60 | # The OAuth 2.0 token exchange subject token type. 61 | # @param [String] resource 62 | # The optional OAuth 2.0 token exchange resource field. 63 | # @param [String] audience 64 | # The optional OAuth 2.0 token exchange audience field. 65 | # @param [Array] scopes 66 | # The optional list of scopes to use. 67 | # @param [String] requested_token_type 68 | # The optional OAuth 2.0 token exchange requested token type. 69 | # @param additional_headers (Hash): 70 | # The optional additional headers to pass to the token exchange endpoint. 71 | # 72 | # @return [Hash] A hash containing the token exchange response. 73 | # @raise [ArgumentError] If required options are missing 74 | # @raise [Google::Auth::AuthorizationError] If the token exchange request fails 75 | def exchange_token options = {} 76 | missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys 77 | unless missing_required_opts.empty? 78 | raise ArgumentError, "Missing required options: #{missing_required_opts.join ', '}" 79 | end 80 | 81 | # TODO: Add the ability to add authentication to the headers 82 | headers = URLENCODED_HEADERS.dup.merge(options[:additional_headers] || {}) 83 | 84 | request_body = make_request options 85 | 86 | response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers 87 | 88 | if response.status != 200 89 | raise AuthorizationError, "Token exchange failed with status #{response.status}" 90 | end 91 | 92 | MultiJson.load response.body 93 | end 94 | 95 | private 96 | 97 | def make_request options = {} 98 | request_body = { 99 | grant_type: options[:grant_type], 100 | audience: options[:audience], 101 | scope: Array(options[:scopes])&.join(" ") || [], 102 | requested_token_type: options[:requested_token_type], 103 | subject_token: options[:subject_token], 104 | subject_token_type: options[:subject_token_type] 105 | } 106 | unless options[:additional_options].nil? 107 | request_body[:options] = CGI.escape MultiJson.dump(options[:additional_options], symbolize_name: true) 108 | end 109 | request_body 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/bearer_token_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "helper" 16 | require "googleauth/bearer_token" 17 | require "logger" 18 | require "digest" 19 | 20 | describe Google::Auth::BearerTokenCredentials do 21 | let(:token) { "test-bearer-token-12345" } 22 | let(:example_universe_domain) { "example.com" } 23 | let(:expires_at) { Time.now + 3600 } # 1 hour from now 24 | 25 | describe "#initialize" do 26 | it "creates with token and proper defaults" do 27 | creds = Google::Auth::BearerTokenCredentials.new token: token 28 | _(creds.token).must_equal token 29 | _(creds.universe_domain).must_equal "googleapis.com" 30 | end 31 | 32 | it "creates with custom universe domain" do 33 | creds = Google::Auth::BearerTokenCredentials.new( 34 | token: token, 35 | universe_domain: example_universe_domain 36 | ) 37 | _(creds.universe_domain).must_equal example_universe_domain 38 | end 39 | 40 | it "creates with expires_at as Time object" do 41 | creds = Google::Auth::BearerTokenCredentials.new(token: token, expires_at: expires_at) 42 | _(creds.expires_at).must_equal expires_at 43 | end 44 | 45 | it "creates with expires_at as Numeric timestamp" do 46 | expires_at_seconds = expires_at.to_i 47 | creds = Google::Auth::BearerTokenCredentials.new(token: token, expires_at: expires_at_seconds) 48 | _(creds.expires_at).must_equal Time.at(expires_at_seconds) 49 | end 50 | 51 | it "raises if bearer token is missing" do 52 | expect do 53 | Google::Auth::BearerTokenCredentials.new 54 | end.must_raise ArgumentError 55 | end 56 | 57 | it "raises if bearer token is empty" do 58 | expect do 59 | Google::Auth::BearerTokenCredentials.new(token: "") 60 | end.must_raise ArgumentError 61 | end 62 | end 63 | 64 | describe "#apply!" do 65 | let(:creds) { Google::Auth::BearerTokenCredentials.new token: token } 66 | 67 | it "adds Authorization token header to hash" do 68 | md = { foo: "bar" } 69 | want = { foo: "bar", Google::Auth::BearerTokenCredentials::AUTH_METADATA_KEY => "Bearer #{token}" } 70 | md = creds.apply md 71 | _(md).must_equal want 72 | end 73 | 74 | it "logs (hashed token) when a logger is set, but not the raw token" do 75 | strio = StringIO.new 76 | logger = Logger.new strio 77 | creds.logger = logger 78 | creds.apply({}) 79 | _(strio.string).wont_be:empty? 80 | 81 | hashed_token = Digest::SHA256.hexdigest(token) 82 | _(strio.string).must_include hashed_token 83 | _(strio.string).wont_include token 84 | end 85 | end 86 | 87 | describe "#duplicate" do 88 | let(:creds) { Google::Auth::BearerTokenCredentials.new token: token, expires_at: expires_at } 89 | 90 | it "creates a duplicate with same values" do 91 | dup = creds.duplicate 92 | _(dup.token).must_equal token 93 | _(dup.expires_at).must_equal expires_at 94 | _(dup.universe_domain).must_equal "googleapis.com" 95 | end 96 | 97 | it "allows overriding values" do 98 | new_expires_at = Time.now + 7200 99 | dup = creds.duplicate token: "new-token", expires_at: new_expires_at, universe_domain: example_universe_domain 100 | _(dup.token).must_equal "new-token" 101 | _(dup.expires_at).must_equal new_expires_at 102 | _(dup.universe_domain).must_equal example_universe_domain 103 | end 104 | end 105 | 106 | describe "#expires_within?" do 107 | let(:creds) { Google::Auth::BearerTokenCredentials.new token: token, expires_at: expires_at } 108 | 109 | it "returns true if after expiration" do 110 | _(creds.expires_within?(4000)).must_equal true # Check after expiration 111 | end 112 | 113 | it "returns false if before expiration" do 114 | _(creds.expires_within?(3000)).must_equal false # Check before expiration 115 | end 116 | 117 | it "returns false if no expiration is set" do 118 | creds_no_expires_at = Google::Auth::BearerTokenCredentials.new token: token 119 | _(creds_no_expires_at.expires_within?(3600)).must_equal false 120 | end 121 | end 122 | 123 | describe "#fetch_access_token!" do 124 | it "returns nil if not expired" do 125 | creds = Google::Auth::BearerTokenCredentials.new token: token, expires_at: expires_at 126 | _(creds.send(:fetch_access_token!)).must_be_nil 127 | end 128 | 129 | it "raises CredentialsError with details if token is expired" do 130 | expired_time = Time.now - 3600 131 | creds = Google::Auth::BearerTokenCredentials.new token: token, expires_at: expired_time 132 | error = expect do 133 | creds.send(:fetch_access_token!) 134 | end.must_raise Google::Auth::CredentialsError 135 | 136 | _(error.credential_type_name).must_equal "Google::Auth::BearerTokenCredentials" 137 | _(error.principal).must_equal :bearer_token 138 | _(error.message).must_equal "Bearer token has expired." 139 | _(error).must_be_kind_of Google::Auth::DetailedError 140 | _(error).must_be_kind_of Google::Auth::Error 141 | end 142 | 143 | it "returns nil if no expiry is set" do 144 | creds = Google::Auth::BearerTokenCredentials.new token: token 145 | _(creds.send(:fetch_access_token!)).must_be_nil 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/googleauth/apply_auth_examples.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | spec_dir = File.expand_path File.join(File.dirname(__FILE__)) 16 | $LOAD_PATH.unshift spec_dir 17 | $LOAD_PATH.uniq! 18 | 19 | require "faraday" 20 | require "google/cloud/env" 21 | require "spec_helper" 22 | 23 | shared_examples "apply/apply! are OK" do 24 | let(:auth_key) { :authorization } 25 | 26 | # tests that use these examples need to define 27 | # 28 | # @client which should be an auth client 29 | # 30 | # @make_auth_stubs, which should stub out the expected http behaviour of the 31 | # auth client 32 | describe "#fetch_access_token" do 33 | let(:token) { "1/abcdef1234567890" } 34 | let :access_stub do 35 | make_auth_stubs access_token: token 36 | end 37 | let :id_stub do 38 | make_auth_stubs id_token: token 39 | end 40 | 41 | it "should set access_token to the fetched value" do 42 | access_stub 43 | @client.fetch_access_token! 44 | expect(@client.access_token).to eq(token) 45 | expect(access_stub).to have_been_requested 46 | end 47 | 48 | it "should set id_token to the fetched value" do 49 | skip unless @id_client 50 | id_stub 51 | @id_client.fetch_access_token! 52 | expect(@id_client.id_token).to eq(token) 53 | expect(id_stub).to have_been_requested 54 | end 55 | 56 | it "should notify refresh listeners after updating" do 57 | access_stub 58 | expect do |b| 59 | @client.on_refresh(&b) 60 | @client.fetch_access_token! 61 | end.to yield_with_args(have_attributes( 62 | access_token: "1/abcdef1234567890" 63 | )) 64 | expect(access_stub).to have_been_requested 65 | end 66 | 67 | it "should log when a logger is set" do 68 | access_stub 69 | io = StringIO.new 70 | @client.logger = Logger.new io 71 | @client.fetch_access_token! 72 | expect(io.string).to include "INFO -- : Requesting access token from" 73 | end 74 | 75 | it "should not log to stdout when a logger is not set" do 76 | access_stub 77 | @client.logger = nil 78 | expect { @client.fetch_access_token! }.to_not output.to_stdout 79 | end 80 | 81 | it "should not log to stderr when a logger is not set" do 82 | access_stub 83 | @client.logger = nil 84 | expect { @client.fetch_access_token! }.to_not output.to_stderr 85 | end 86 | end 87 | 88 | describe "#apply!" do 89 | it "should update the target hash with fetched access token" do 90 | token = "1/abcdef1234567890" 91 | stub = make_auth_stubs access_token: token 92 | 93 | md = { foo: "bar" } 94 | @client.apply! md 95 | want = { :foo => "bar", auth_key => "Bearer #{token}" } 96 | expect(md).to eq(want) 97 | expect(stub).to have_been_requested 98 | end 99 | 100 | it "should update the target hash with fetched ID token" do 101 | skip unless @id_client 102 | token = "1/abcdef1234567890" 103 | stub = make_auth_stubs id_token: token 104 | 105 | md = { foo: "bar" } 106 | @id_client.apply! md 107 | want = { :foo => "bar", auth_key => "Bearer #{token}" } 108 | expect(md).to eq(want) 109 | expect(stub).to have_been_requested 110 | end 111 | end 112 | 113 | describe "updater_proc" do 114 | it "should provide a proc that updates a hash with the access token" do 115 | token = "1/abcdef1234567890" 116 | stub = make_auth_stubs access_token: token 117 | md = { foo: "bar" } 118 | the_proc = @client.updater_proc 119 | got = the_proc.call md 120 | want = { :foo => "bar", auth_key => "Bearer #{token}" } 121 | expect(got).to eq(want) 122 | expect(stub).to have_been_requested 123 | end 124 | end 125 | 126 | describe "#apply" do 127 | it "should not update the original hash with the access token" do 128 | token = "1/abcdef1234567890" 129 | stub = make_auth_stubs access_token: token 130 | 131 | md = { foo: "bar" } 132 | @client.apply md 133 | want = { foo: "bar" } 134 | expect(md).to eq(want) 135 | expect(stub).to have_been_requested 136 | end 137 | 138 | it "should add the token to the returned hash" do 139 | token = "1/abcdef1234567890" 140 | stub = make_auth_stubs access_token: token 141 | 142 | md = { foo: "bar" } 143 | got = @client.apply md 144 | want = { :foo => "bar", auth_key => "Bearer #{token}" } 145 | expect(got).to eq(want) 146 | expect(stub).to have_been_requested 147 | end 148 | 149 | it "should not fetch a new token if the current is not expired" do 150 | token = "1/abcdef1234567890" 151 | stub = make_auth_stubs access_token: token 152 | 153 | n = 5 # arbitrary 154 | n.times do |_t| 155 | md = { foo: "bar" } 156 | got = @client.apply md 157 | want = { :foo => "bar", auth_key => "Bearer #{token}" } 158 | expect(got).to eq(want) 159 | end 160 | expect(stub).to have_been_requested 161 | end 162 | 163 | it "should fetch a new token if the current one is expired" do 164 | token1 = "1/abcdef1234567890" 165 | token2 = "2/abcdef1234567891" 166 | 167 | [token1, token2].each do |t| 168 | make_auth_stubs access_token: t 169 | md = { foo: "bar" } 170 | got = @client.apply md 171 | want = { :foo => "bar", auth_key => "Bearer #{t}" } 172 | expect(got).to eq(want) 173 | @client.expires_at -= 3601 # default is to expire in 1hr 174 | ::Google::Cloud.env.compute_metadata.cache.expire_all! 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/googleauth/default_credentials.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "multi_json" 16 | require "stringio" 17 | 18 | require "googleauth/credentials_loader" 19 | require "googleauth/errors" 20 | require "googleauth/external_account" 21 | require "googleauth/service_account" 22 | require "googleauth/service_account_jwt_header" 23 | require "googleauth/user_refresh" 24 | require "googleauth/impersonated_service_account" 25 | 26 | module Google 27 | # Module Auth provides classes that provide Google-specific authorization 28 | # used to access Google APIs. 29 | module Auth 30 | # DefaultCredentials is used to preload the credentials file, to determine 31 | # which type of credentials should be loaded. 32 | class DefaultCredentials 33 | extend CredentialsLoader 34 | 35 | ## 36 | # Override CredentialsLoader#make_creds to use the class determined by 37 | # loading the json. 38 | # 39 | # **Important:** If you accept a credential configuration (credential 40 | # JSON/File/Stream) from an external source for authentication to Google 41 | # Cloud, you must validate it before providing it to any Google API or 42 | # library. Providing an unvalidated credential configuration to Google 43 | # APIs can compromise the security of your systems and data. For more 44 | # information, refer to [Validate credential configurations from external 45 | # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). 46 | # 47 | # @deprecated This method is deprecated and will be removed in a future version. 48 | # Please use the `make_creds` method on the specific credential class you intend to load, 49 | # e.g., `Google::Auth::ServiceAccountCredentials.make_creds`. 50 | # 51 | # This method does not validate the credential configuration. The security 52 | # risk occurs when a credential configuration is accepted from a source that 53 | # is not under your control and used without validation on your side. 54 | # 55 | # If you know that you will be loading credential configurations of a 56 | # specific type, it is recommended to use a credential-type-specific 57 | # `make_creds` method. 58 | # This will ensure that an unexpected credential type with potential for 59 | # malicious intent is not loaded unintentionally. You might still have to do 60 | # validation for certain credential types. Please follow the recommendation 61 | # for that method. For example, if you want to load only service accounts, 62 | # you can use: 63 | # ``` 64 | # creds = Google::Auth::ServiceAccountCredentials.make_creds 65 | # ``` 66 | # @see Google::Auth::ServiceAccountCredentials.make_creds 67 | # 68 | # If you are loading your credential configuration from an untrusted source and have 69 | # not mitigated the risks (e.g. by validating the configuration yourself), make 70 | # these changes as soon as possible to prevent security risks to your environment. 71 | # 72 | # Regardless of the method used, it is always your responsibility to validate 73 | # configurations received from external sources. 74 | # 75 | # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials for more details. 76 | # 77 | # @param options [Hash] Options for creating the credentials 78 | # @return [Google::Auth::Credentials] The credentials instance 79 | # @raise [Google::Auth::InitializationError] If the credentials cannot be determined 80 | def self.make_creds options = {} 81 | json_key_io = options[:json_key_io] 82 | json_key, clz = determine_creds_class json_key_io 83 | if json_key 84 | io = StringIO.new MultiJson.dump(json_key) 85 | clz.make_creds options.merge(json_key_io: io) 86 | else 87 | clz.make_creds options 88 | end 89 | end 90 | 91 | # Reads the input json and determines which creds class to use. 92 | # 93 | # @param json_key_io [IO, nil] An optional IO object containing the JSON key. 94 | # If nil, the credential type is determined from environment variables. 95 | # @return [Array(Hash, Class)] The JSON key (or nil if from environment) and the credential class to use 96 | # @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type, 97 | # or if the environment variable is undefined or unsupported. 98 | def self.determine_creds_class json_key_io = nil 99 | if json_key_io 100 | json_key = MultiJson.load json_key_io.read 101 | key = "type" 102 | raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key 103 | type = json_key[key] 104 | else 105 | env_var = CredentialsLoader::ACCOUNT_TYPE_VAR 106 | type = ENV[env_var] 107 | raise InitializationError, "#{env_var} is undefined in env" unless type 108 | json_key = nil 109 | end 110 | 111 | clz = case type 112 | when ServiceAccountCredentials::CREDENTIAL_TYPE_NAME 113 | ServiceAccountCredentials 114 | when UserRefreshCredentials::CREDENTIAL_TYPE_NAME 115 | UserRefreshCredentials 116 | when ExternalAccount::Credentials::CREDENTIAL_TYPE_NAME 117 | ExternalAccount::Credentials 118 | when ImpersonatedServiceAccountCredentials::CREDENTIAL_TYPE_NAME 119 | ImpersonatedServiceAccountCredentials 120 | else 121 | raise InitializationError, "credentials type '#{type}' is not supported" 122 | end 123 | [json_key, clz] 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/googleauth/api_key.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/base_client" 16 | require "googleauth/credentials_loader" 17 | 18 | module Google 19 | module Auth 20 | ## 21 | # Implementation of Google API Key authentication. 22 | # 23 | # API Keys are text strings. They don't have an associated JSON file. 24 | # 25 | # The end-user is managing their API Keys directly, not via 26 | # an authentication library. 27 | # 28 | # API Keys provide project information for an API request. 29 | # API Keys don't reference an IAM principal, they do not expire, 30 | # and cannot be refreshed. 31 | # 32 | class APIKeyCredentials 33 | include Google::Auth::BaseClient 34 | 35 | # @private Authorization header key 36 | API_KEY_HEADER = "x-goog-api-key".freeze 37 | 38 | # @private Environment variable containing API key 39 | API_KEY_VAR = "GOOGLE_API_KEY".freeze 40 | 41 | # @return [String] The API key 42 | attr_reader :api_key 43 | 44 | # @return [String] The universe domain of the universe 45 | # this API key is for 46 | attr_accessor :universe_domain 47 | 48 | class << self 49 | # Creates an APIKeyCredentials from the environment. 50 | # Checks the ENV['GOOGLE_API_KEY'] variable. 51 | # 52 | # @param [String] _scope 53 | # The scope to use for OAuth. Not used by API key auth. 54 | # @param [Hash] options 55 | # The options to pass to the credentials instance 56 | # 57 | # @return [Google::Auth::APIKeyCredentials, nil] 58 | # Credentials if the API key environment variable is present, 59 | # nil otherwise 60 | def from_env _scope = nil, options = {} 61 | api_key = ENV[API_KEY_VAR] 62 | return nil if api_key.nil? || api_key.empty? 63 | new options.merge(api_key: api_key) 64 | end 65 | 66 | # Create the APIKeyCredentials. 67 | # 68 | # @param [Hash] options The credentials options 69 | # @option options [String] :api_key 70 | # The API key to use for authentication 71 | # @option options [String] :universe_domain 72 | # The universe domain of the universe this API key 73 | # belongs to (defaults to googleapis.com) 74 | # @return [Google::Auth::APIKeyCredentials] 75 | def make_creds options = {} 76 | new options 77 | end 78 | end 79 | 80 | # Initialize the APIKeyCredentials. 81 | # 82 | # @param [Hash] options The credentials options 83 | # @option options [String] :api_key 84 | # The API key to use for authentication 85 | # @option options [String] :universe_domain 86 | # The universe domain of the universe this API key 87 | # belongs to (defaults to googleapis.com) 88 | # @raise [ArgumentError] If the API key is nil or empty 89 | def initialize options = {} 90 | raise ArgumentError, "API key must be provided" if options[:api_key].nil? || options[:api_key].empty? 91 | @api_key = options[:api_key] 92 | @universe_domain = options[:universe_domain] || "googleapis.com" 93 | end 94 | 95 | # Determines if the credentials object has expired. 96 | # Since API keys don't expire, this always returns false. 97 | # 98 | # @param [Fixnum] _seconds 99 | # The optional timeout in seconds since the last refresh 100 | # @return [Boolean] 101 | # True if the token has expired, false otherwise. 102 | def expires_within? _seconds 103 | false 104 | end 105 | 106 | # Creates a duplicate of these credentials. 107 | # 108 | # @param [Hash] options Additional options for configuring the credentials 109 | # @return [Google::Auth::APIKeyCredentials] 110 | def duplicate options = {} 111 | self.class.new( 112 | api_key: options[:api_key] || @api_key, 113 | universe_domain: options[:universe_domain] || @universe_domain 114 | ) 115 | end 116 | 117 | # Updates the provided hash with the API Key header. 118 | # 119 | # The `apply!` method modifies the provided hash in place, adding the 120 | # `x-goog-api-key` header with the API Key value. 121 | # 122 | # The API Key is hashed before being logged for security purposes. 123 | # 124 | # NB: this method typically would be called through `updater_proc`. 125 | # Some older clients call it directly though, so it has to be public. 126 | # 127 | # @param [Hash] a_hash The hash to which the API Key header should be added. 128 | # This is typically a hash representing the request headers. This hash 129 | # will be modified in place. 130 | # @param [Hash] _opts Additional options (currently not used). Included 131 | # for consistency with the `BaseClient` interface. 132 | # @return [Hash] The modified hash (the same hash passed as the `a_hash` 133 | # argument). 134 | def apply! a_hash, _opts = {} 135 | a_hash[API_KEY_HEADER] = @api_key 136 | logger&.debug do 137 | hash = Digest::SHA256.hexdigest @api_key 138 | Google::Logging::Message.from message: "Sending API key auth token. (sha256:#{hash})" 139 | end 140 | a_hash 141 | end 142 | 143 | # For credentials that are initialized with a token without a principal, 144 | # the type of that token should be returned as a principal instead 145 | # @private 146 | # @return [Symbol] the token type in lieu of the principal 147 | def principal 148 | token_type 149 | end 150 | 151 | protected 152 | 153 | # The token type should be :api_key 154 | def token_type 155 | :api_key 156 | end 157 | 158 | # We don't need to fetch access tokens for API key auth 159 | def fetch_access_token! _options = {} 160 | nil 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/googleauth/external_account.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "time" 16 | require "uri" 17 | require "googleauth/credentials_loader" 18 | require "googleauth/errors" 19 | require "googleauth/external_account/aws_credentials" 20 | require "googleauth/external_account/identity_pool_credentials" 21 | require "googleauth/external_account/pluggable_credentials" 22 | 23 | module Google 24 | # Module Auth provides classes that provide Google-specific authorization 25 | # used to access Google APIs. 26 | module Auth 27 | # Authenticates requests using External Account credentials, such 28 | # as those provided by the AWS provider. 29 | module ExternalAccount 30 | # Provides an entrypoint for all Exernal Account credential classes. 31 | class Credentials 32 | # The subject token type used for AWS external_account credentials. 33 | AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".freeze 34 | MISSING_CREDENTIAL_SOURCE = "missing credential source for external account".freeze 35 | INVALID_EXTERNAL_ACCOUNT_TYPE = "credential source is not supported external account type".freeze 36 | 37 | # @private 38 | # @type [::String] The type name for this credential. 39 | CREDENTIAL_TYPE_NAME = "external_account".freeze 40 | 41 | # Create a ExternalAccount::Credentials 42 | # 43 | # @note Warning: 44 | # This method does not validate the credential configuration. A security 45 | # risk occurs when a credential configuration configured with malicious urls 46 | # is used. 47 | # When the credential configuration is accepted from an 48 | # untrusted source, you should validate it before using with this method. 49 | # See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials 50 | # for more details. 51 | # 52 | # @param options [Hash] Options for creating credentials 53 | # @option options [IO] :json_key_io (required) An IO object containing the JSON key 54 | # @option options [String,Array,nil] :scope The scope(s) to access 55 | # @return [Google::Auth::ExternalAccount::AwsCredentials, 56 | # Google::Auth::ExternalAccount::IdentityPoolCredentials, 57 | # Google::Auth::ExternalAccount::PluggableAuthCredentials] 58 | # The appropriate external account credentials based on the credential source 59 | # @raise [Google::Auth::InitializationError] If the json file is missing, lacks required fields, 60 | # or does not contain a supported credential source 61 | def self.make_creds options = {} 62 | json_key_io, scope = options.values_at :json_key_io, :scope 63 | 64 | raise InitializationError, "A json file is required for external account credentials." unless json_key_io 65 | CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME 66 | user_creds = read_json_key json_key_io 67 | 68 | # AWS credentials is determined by aws subject token type 69 | return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE 70 | 71 | raise InitializationError, MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil? 72 | user_creds[:scope] = scope 73 | make_external_account_credentials user_creds 74 | end 75 | 76 | # Reads the required fields from the JSON. 77 | # 78 | # @param json_key_io [IO] An IO object containing the JSON key 79 | # @return [Hash] The parsed JSON key 80 | # @raise [Google::Auth::InitializationError] If the JSON is missing required fields 81 | def self.read_json_key json_key_io 82 | json_key = MultiJson.load json_key_io.read, symbolize_keys: true 83 | wanted = [ 84 | :audience, :subject_token_type, :token_url, :credential_source 85 | ] 86 | wanted.each do |key| 87 | raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key 88 | end 89 | json_key 90 | end 91 | 92 | class << self 93 | private 94 | 95 | # Creates AWS credentials from the provided user credentials 96 | # 97 | # @param user_creds [Hash] The user credentials containing AWS credential source information 98 | # @param scope [String,Array,nil] The scope(s) to access 99 | # @return [Google::Auth::ExternalAccount::AwsCredentials] The AWS credentials 100 | def make_aws_credentials user_creds, scope 101 | Google::Auth::ExternalAccount::AwsCredentials.new( 102 | audience: user_creds[:audience], 103 | scope: scope, 104 | subject_token_type: user_creds[:subject_token_type], 105 | token_url: user_creds[:token_url], 106 | credential_source: user_creds[:credential_source], 107 | service_account_impersonation_url: user_creds[:service_account_impersonation_url], 108 | universe_domain: user_creds[:universe_domain] 109 | ) 110 | end 111 | 112 | # Creates the appropriate external account credentials based on the credential source type 113 | # 114 | # @param user_creds [Hash] The user credentials containing credential source information 115 | # @return [Google::Auth::ExternalAccount::IdentityPoolCredentials, 116 | # Google::Auth::ExternalAccount::PluggableAuthCredentials] 117 | # The appropriate external account credentials 118 | # @raise [Google::Auth::InitializationError] If the credential source is not a supported type 119 | def make_external_account_credentials user_creds 120 | unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil? 121 | return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds 122 | end 123 | unless user_creds[:credential_source][:executable].nil? 124 | return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds 125 | end 126 | raise InitializationError, INVALID_EXTERNAL_ACCOUNT_TYPE 127 | end 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /Errors.md: -------------------------------------------------------------------------------- 1 | # Error Handling in Google Auth Library for Ruby 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ## Error Hierarchy 8 | 9 | The Google Auth Library has two main error hierarchies: the core authentication errors and the specialized ID token flow errors. 10 | 11 | ### Core Authentication Errors 12 | 13 | These errors are used throughout the main library for general authentication and credential operations: 14 | 15 | ``` 16 | Google::Auth::Error (module) 17 | ├── Google::Auth::InitializationError (class) 18 | └── Google::Auth::DetailedError (module) 19 | ├── Google::Auth::CredentialsError (class) 20 | ├── Google::Auth::AuthorizationError (class) 21 | ├── Google::Auth::UnexpectedStatusError (class) 22 | └── Google::Auth::ParseError (class) 23 | ``` 24 | 25 | ### ID Token Errors 26 | 27 | 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: 28 | 29 | ``` 30 | Google::Auth::Error (module) 31 | ├── Google::Auth::IDTokens::KeySourceError (class) 32 | └── Google::Auth::IDTokens::VerificationError (class) 33 | ├── ExpiredTokenError (class) 34 | ├── SignatureError (class) 35 | ├── IssuerMismatchError (class) 36 | ├── AudienceMismatchError (class) 37 | └── AuthorizedPartyMismatchError (class) 38 | ``` 39 | 40 | ### Error Module Types 41 | 42 | - **`Google::Auth::Error`**: Base module that all Google Auth errors include. Use this to catch any error from the library. 43 | 44 | - **`Google::Auth::DetailedError`**: Extends `Error` to include detailed information about the credential that caused the error, including the credential type and principal. 45 | 46 | ## Core Authentication Error Classes 47 | 48 | - **`InitializationError`**: Raised during credential initialization when required parameters are missing or invalid. 49 | 50 | - **`CredentialsError`**: Generic error raised during authentication flows. 51 | 52 | - **`AuthorizationError`**: Raised when a remote server refuses to authorize the client. Inherits from `Signet::AuthorizationError`. Is being raised where `Signet::AuthorizationError` was raised previously. 53 | 54 | - **`UnexpectedStatusError`**: Raised when a server returns an unexpected HTTP status code. Inherits from `Signet::UnexpectedStatusError`. Is being raised where `Signet::UnexpectedStatusError` was raised previously. 55 | 56 | - **`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. 57 | 58 | ## Detailed Error Information 59 | 60 | Errors that include the `DetailedError` module provide additional context about what went wrong: 61 | 62 | - **`credential_type_name`**: The class name of the credential that raised the error (e.g., `"Google::Auth::ServiceAccountCredentials"`) 63 | 64 | - **`principal`**: The identity associated with the credentials (e.g., an email address for service accounts, `:api_key` for API key credentials) 65 | 66 | ### Example: Catching and Handling Core Errors 67 | 68 | ```ruby 69 | begin 70 | credentials = Google::Auth::ServiceAccountCredentials.make_creds( 71 | json_key_io: File.open("your-key.json") 72 | ) 73 | # Use credentials... 74 | rescue Google::Auth::InitializationError => e 75 | puts "Failed to initialize credentials: #{e.message}" 76 | # e.g., Missing required fields in the service account key file 77 | rescue Google::Auth::DetailedError => e 78 | puts "Authorization failed: #{e.message}" 79 | puts "Credential type: #{e.credential_type_name}" 80 | puts "Principal: #{e.principal}" 81 | # e.g., Invalid or revoked service account 82 | rescue Google::Auth::Error => e 83 | puts "Unknown Google Auth error: #{e.message}" 84 | end 85 | ``` 86 | 87 | ## Backwards compatibility 88 | 89 | 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. 90 | 91 | ## ID Token Verification 92 | 93 | 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. 94 | 95 | ### ID Token Error Classes 96 | 97 | - **`KeySourceError`**: Raised when the library fails to obtain the keys needed to verify a token, typically from a JWKS (JSON Web Key Set) endpoint. 98 | 99 | - **`VerificationError`**: Base class for all errors related to token verification failures. 100 | 101 | - **`ExpiredTokenError`**: Raised when a token has expired according to its expiration time claim (`exp`). 102 | 103 | - **`SignatureError`**: Raised when a token's signature cannot be verified, indicating it might be tampered with or corrupted. 104 | 105 | - **`IssuerMismatchError`**: Raised when a token's issuer (`iss` claim) doesn't match the expected issuer. 106 | 107 | - **`AudienceMismatchError`**: Raised when a token's audience (`aud` claim) doesn't match the expected audience. 108 | 109 | - **`AuthorizedPartyMismatchError`**: Raised when a token's authorized party (`azp` claim) doesn't match the expected client ID. 110 | 111 | ### Example: Handling ID Token Verification Errors 112 | 113 | ```ruby 114 | require "googleauth/id_tokens" 115 | 116 | begin 117 | # Verify the provided ID token 118 | payload = Google::Auth::IDTokens.verify_oidc( 119 | id_token, 120 | audience: "expected-audience-12345.apps.googleusercontent.com" 121 | ) 122 | 123 | # Use the verified token payload 124 | user_email = payload["email"] 125 | 126 | rescue Google::Auth::IDTokens::ExpiredTokenError => e 127 | puts "The token has expired. Please obtain a new one." 128 | 129 | rescue Google::Auth::IDTokens::SignatureError => e 130 | puts "Invalid token signature." 131 | 132 | rescue Google::Auth::IDTokens::IssuerMismatchError => e 133 | puts "Invalid token issuer." 134 | 135 | rescue Google::Auth::IDTokens::AudienceMismatchError => e 136 | puts "This token is not intended for this application (invalid audience)." 137 | 138 | rescue Google::Auth::IDTokens::AuthorizedPartyMismatchError => e 139 | puts "Invalid token authorized party." 140 | 141 | rescue Google::Auth::IDTokens::VerificationError => e 142 | puts "Token verification failed: #{e.message}" 143 | # Generic verification error handling 144 | 145 | rescue Google::Auth::IDTokens::KeySourceError => e 146 | puts "Unable to retrieve verification keys: #{e.message}" 147 | 148 | rescue Google::Auth::Error => e 149 | puts "Unknown Google Auth error: #{e.message}" 150 | # This will catch any Google Auth error 151 | end 152 | ``` 153 | -------------------------------------------------------------------------------- /lib/googleauth/external_account/identity_pool_credentials.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "time" 16 | require "googleauth/errors" 17 | require "googleauth/external_account/base_credentials" 18 | require "googleauth/external_account/external_account_utils" 19 | 20 | module Google 21 | # Module Auth provides classes that provide Google-specific authorization used to access Google APIs. 22 | module Auth 23 | module ExternalAccount 24 | # This module handles the retrieval of credentials from Google Cloud by utilizing the any 3PI 25 | # provider then exchanging the credentials for a short-lived Google Cloud access token. 26 | class IdentityPoolCredentials 27 | include Google::Auth::ExternalAccount::BaseCredentials 28 | include Google::Auth::ExternalAccount::ExternalAccountUtils 29 | extend CredentialsLoader 30 | 31 | # Will always be nil, but method still gets used. 32 | attr_reader :client_id 33 | 34 | # Initialize from options map. 35 | # 36 | # @param [Hash] options Configuration options 37 | # @option options [String] :audience The audience for the token 38 | # @option options [Hash{Symbol => Object}] :credential_source A hash containing either source file or url. 39 | # credential_source_format is either text or json to define how to parse the credential response. 40 | # @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing, 41 | # contains ambiguous sources, or is missing required fields 42 | # 43 | def initialize options = {} 44 | base_setup options 45 | 46 | @audience = options[:audience] 47 | @credential_source = options[:credential_source] || {} 48 | @credential_source_file = @credential_source[:file] 49 | @credential_source_url = @credential_source[:url] 50 | @credential_source_headers = @credential_source[:headers] || {} 51 | @credential_source_format = @credential_source[:format] || {} 52 | @credential_source_format_type = @credential_source_format[:type] || "text" 53 | validate_credential_source 54 | end 55 | 56 | # Implementation of BaseCredentials retrieve_subject_token! 57 | # 58 | # @return [String] The subject token 59 | # @raise [Google::Auth::CredentialsError] If the token can't be parsed from JSON or is missing 60 | def retrieve_subject_token! 61 | content, resource_name = token_data 62 | if @credential_source_format_type == "text" 63 | token = content 64 | else 65 | begin 66 | response_data = MultiJson.load content, symbolize_keys: true 67 | token = response_data[@credential_source_field_name.to_sym] 68 | rescue StandardError 69 | raise CredentialsError, "Unable to parse subject_token from JSON resource #{resource_name} " \ 70 | "using key #{@credential_source_field_name}" 71 | end 72 | end 73 | raise CredentialsError, "Missing subject_token in the credential_source file/response." unless token 74 | token 75 | end 76 | 77 | private 78 | 79 | # Validates input 80 | # 81 | # @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing, 82 | # contains ambiguous sources, or is missing required fields 83 | def validate_credential_source 84 | # `environment_id` is only supported in AWS or dedicated future external account credentials. 85 | unless @credential_source[:environment_id].nil? 86 | raise InitializationError, "Invalid Identity Pool credential_source field 'environment_id'" 87 | end 88 | unless ["json", "text"].include? @credential_source_format_type 89 | raise InitializationError, "Invalid credential_source format #{@credential_source_format_type}" 90 | end 91 | # for JSON types, get the required subject_token field name. 92 | @credential_source_field_name = @credential_source_format[:subject_token_field_name] 93 | if @credential_source_format_type == "json" && @credential_source_field_name.nil? 94 | raise InitializationError, "Missing subject_token_field_name for JSON credential_source format" 95 | end 96 | # check file or url must be fulfilled and mutually exclusiveness. 97 | if @credential_source_file && @credential_source_url 98 | raise InitializationError, "Ambiguous credential_source. 'file' is mutually exclusive with 'url'." 99 | end 100 | return unless (@credential_source_file || @credential_source_url).nil? 101 | raise InitializationError, "Missing credential_source. A 'file' or 'url' must be provided." 102 | end 103 | 104 | def token_data 105 | @credential_source_file.nil? ? url_data : file_data 106 | end 107 | 108 | # Reads data from a file source 109 | # 110 | # @return [Array(String, String)] The file content and file path 111 | # @raise [Google::Auth::CredentialsError] If the source file doesn't exist 112 | def file_data 113 | unless File.exist? @credential_source_file 114 | raise CredentialsError, 115 | "File #{@credential_source_file} was not found." 116 | end 117 | content = File.read @credential_source_file, encoding: "utf-8" 118 | [content, @credential_source_file] 119 | end 120 | 121 | # Fetches data from a URL source 122 | # 123 | # @return [Array(String, String)] The response body and URL 124 | # @raise [Google::Auth::CredentialsError] If there's an error retrieving data from the URL 125 | # or if the response is not successful 126 | def url_data 127 | begin 128 | response = connection.get @credential_source_url do |req| 129 | req.headers.merge! @credential_source_headers 130 | end 131 | rescue Faraday::Error => e 132 | raise CredentialsError, "Error retrieving from credential url: #{e}" 133 | end 134 | unless response.success? 135 | raise CredentialsError, 136 | "Unable to retrieve Identity Pool subject token #{response.body}" 137 | end 138 | [response.body, @credential_source_url] 139 | end 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/googleauth/bearer_token.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/base_client" 16 | require "googleauth/errors" 17 | 18 | module Google 19 | module Auth 20 | ## 21 | # Implementation of Bearer Token authentication scenario. 22 | # 23 | # Bearer tokens are strings representing an authorization grant. 24 | # They can be OAuth2 ("ya.29") tokens, JWTs, IDTokens -- anything 25 | # that is sent as a `Bearer` in an `Authorization` header. 26 | # 27 | # Not all 'authentication' strings can be used with this class, 28 | # e.g. an API key cannot since API keys are sent in a 29 | # `x-goog-api-key` header or as a query parameter. 30 | # 31 | # This class should be used when the end-user is managing the 32 | # authentication token separately, e.g. with a separate service. 33 | # This means that tasks like tracking the lifetime of and 34 | # refreshing the token are outside the scope of this class. 35 | # 36 | # There is no JSON representation for this type of credentials. 37 | # If the end-user has credentials in JSON format they should typically 38 | # use the corresponding credentials type, e.g. ServiceAccountCredentials 39 | # with the service account JSON. 40 | # 41 | class BearerTokenCredentials 42 | include Google::Auth::BaseClient 43 | 44 | # @private Authorization header name 45 | AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY 46 | 47 | # @return [String] The token to be sent as a part of Bearer claim 48 | attr_reader :token 49 | # The following aliasing is needed for BaseClient since it sends :token_type 50 | alias bearer_token token 51 | 52 | # @return [Time, nil] The token expiration time provided by the end-user. 53 | attr_reader :expires_at 54 | 55 | # @return [String] The universe domain of the universe 56 | # this token is for 57 | attr_accessor :universe_domain 58 | 59 | class << self 60 | # Create the BearerTokenCredentials. 61 | # 62 | # @param [Hash] options The credentials options 63 | # @option options [String] :token The bearer token to use. 64 | # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user. 65 | # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch. 66 | # If `expires_at` is `nil`, it is treated as "token never expires". 67 | # @option options [String] :universe_domain The universe domain of the universe 68 | # this token is for (defaults to googleapis.com) 69 | # @return [Google::Auth::BearerTokenCredentials] 70 | def make_creds options = {} 71 | new options 72 | end 73 | end 74 | 75 | # Initialize the BearerTokenCredentials. 76 | # 77 | # @param [Hash] options The credentials options 78 | # @option options [String] :token The bearer token to use. 79 | # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user. 80 | # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch. 81 | # If `expires_at` is `nil`, it is treated as "token never expires". 82 | # @option options [String] :universe_domain The universe domain of the universe 83 | # this token is for (defaults to googleapis.com) 84 | # @raise [ArgumentError] If the bearer token is nil or empty 85 | def initialize options = {} 86 | raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty? 87 | @token = options[:token] 88 | @expires_at = case options[:expires_at] 89 | when Time 90 | options[:expires_at] 91 | when Numeric 92 | Time.at options[:expires_at] 93 | end 94 | 95 | @universe_domain = options[:universe_domain] || "googleapis.com" 96 | end 97 | 98 | # Determines if the credentials object has expired. 99 | # 100 | # @param [Numeric] seconds The optional timeout in seconds. 101 | # @return [Boolean] True if the token has expired, false otherwise, or 102 | # if the expires_at was not provided. 103 | def expires_within? seconds 104 | return false if @expires_at.nil? # Treat nil expiration as "never expires" 105 | Time.now + seconds >= @expires_at 106 | end 107 | 108 | # Creates a duplicate of these credentials. 109 | # 110 | # @param [Hash] options Additional options for configuring the credentials 111 | # @option options [String] :token The bearer token to use. 112 | # @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time 113 | # object or a number of seconds since epoch. 114 | # @option options [String] :universe_domain The universe domain (defaults to googleapis.com) 115 | # @return [Google::Auth::BearerTokenCredentials] 116 | def duplicate options = {} 117 | self.class.new( 118 | token: options[:token] || @token, 119 | expires_at: options[:expires_at] || @expires_at, 120 | universe_domain: options[:universe_domain] || @universe_domain 121 | ) 122 | end 123 | 124 | # For credentials that are initialized with a token without a principal, 125 | # the type of that token should be returned as a principal instead 126 | # @private 127 | # @return [Symbol] the token type in lieu of the principal 128 | def principal 129 | token_type 130 | end 131 | 132 | protected 133 | 134 | ## 135 | # BearerTokenCredentials do not support fetching a new token. 136 | # 137 | # If the token has an expiration time and is expired, this method will 138 | # raise an error. 139 | # 140 | # @param [Hash] _options Options for fetching a new token (not used). 141 | # @return [nil] Always returns nil. 142 | # @raise [Google::Auth::CredentialsError] If the token is expired. 143 | def fetch_access_token! _options = {} 144 | if @expires_at && Time.now >= @expires_at 145 | raise CredentialsError.with_details( 146 | "Bearer token has expired.", 147 | credential_type_name: self.class.name, 148 | principal: principal 149 | ) 150 | end 151 | 152 | nil 153 | end 154 | 155 | private 156 | 157 | def token_type 158 | :bearer_token 159 | end 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/principal_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "helper" 16 | 17 | require "stringio" 18 | require "multi_json" 19 | 20 | require "googleauth/api_key" 21 | require "googleauth/bearer_token" 22 | require "googleauth/client_id" 23 | require "googleauth/compute_engine" 24 | require "googleauth/external_account" 25 | require "googleauth/iam" 26 | require "googleauth/service_account" 27 | require "googleauth/service_account_jwt_header" 28 | require "googleauth/impersonated_service_account" 29 | require "googleauth/user_refresh" 30 | require "googleauth/user_authorizer" 31 | 32 | describe "Principal methods" do 33 | describe "APIKeyCredentials" do 34 | it "returns :api_key as principal" do 35 | creds = Google::Auth::APIKeyCredentials.new api_key: "test-api-key" 36 | _(creds.principal).must_equal :api_key 37 | end 38 | end 39 | 40 | describe "BearerTokenCredentials" do 41 | it "returns :bearer_token as principal" do 42 | creds = Google::Auth::BearerTokenCredentials.new token: "test-token" 43 | _(creds.principal).must_equal :bearer_token 44 | end 45 | end 46 | 47 | describe "GCECredentials" do 48 | it "returns :gce_metadata as principal" do 49 | creds = Google::Auth::GCECredentials.new 50 | _(creds.principal).must_equal :gce_metadata 51 | end 52 | end 53 | 54 | describe "IAMCredentials" do 55 | it "returns the selector as principal" do 56 | selector = "test-selector" 57 | creds = Google::Auth::IAMCredentials.new selector, "test-token" 58 | _(creds.principal).must_equal selector 59 | end 60 | end 61 | 62 | describe "ServiceAccountCredentials" do 63 | it "returns the issuer as principal" do 64 | test_email = "test-service-account@example.project.iam.gserviceaccount.com" 65 | json = { 66 | private_key: @key = OpenSSL::PKey::RSA.new(2048).to_pem, 67 | client_email: test_email, 68 | type: "service_account" 69 | } 70 | key_io = StringIO.new MultiJson.dump(json) 71 | creds = Google::Auth::ServiceAccountCredentials.make_creds json_key_io: key_io 72 | _(creds.principal).must_equal test_email 73 | end 74 | end 75 | 76 | describe "ServiceAccountJwtHeaderCredentials" do 77 | it "returns the issuer as principal" do 78 | test_email = "test-service-account@example.project.iam.gserviceaccount.com" 79 | json = { 80 | private_key: @key = OpenSSL::PKey::RSA.new(2048).to_pem, 81 | client_email: test_email 82 | } 83 | key_io = StringIO.new MultiJson.dump(json) 84 | creds = Google::Auth::ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io 85 | _(creds.principal).must_equal test_email 86 | end 87 | end 88 | 89 | describe "ImpersonatedServiceAccountCredentials" do 90 | it "returns the source principal when source has a principal method" do 91 | source_creds = Object.new 92 | def source_creds.updater_proc 93 | proc { |a_hash, _opts = {}| a_hash } 94 | end 95 | 96 | def source_creds.principal 97 | :custom_principal 98 | end 99 | 100 | test_impersonation_url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@example.com:generateAccessToken" 101 | test_scope = ["https://www.googleapis.com/auth/userinfo.email"] 102 | creds = Google::Auth::ImpersonatedServiceAccountCredentials.new( 103 | source_credentials: source_creds, 104 | impersonation_url: test_impersonation_url, 105 | scope: test_scope 106 | ) 107 | _(creds.principal).must_equal :custom_principal 108 | end 109 | 110 | it "returns :unknown when source doesn't have a principal method" do 111 | source_creds = Object.new 112 | def source_creds.updater_proc 113 | proc { |a_hash, _opts = {}| a_hash } 114 | end 115 | 116 | test_impersonation_url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@example.com:generateAccessToken" 117 | test_scope = ["https://www.googleapis.com/auth/userinfo.email"] 118 | creds = Google::Auth::ImpersonatedServiceAccountCredentials.new( 119 | source_credentials: source_creds, 120 | impersonation_url: test_impersonation_url, 121 | scope: test_scope 122 | ) 123 | _(creds.principal).must_equal :unknown 124 | end 125 | end 126 | 127 | describe "UserRefreshCredentials" do 128 | it "returns the client_id as principal when available" do 129 | test_client_id = "test-client-id.apps.googleusercontent.com" 130 | json = { 131 | client_id: test_client_id, 132 | client_secret: "notsosecret", 133 | refresh_token: "refreshing-token", 134 | type: "authorized_user" 135 | } 136 | key_io = StringIO.new MultiJson.dump(json) 137 | creds = Google::Auth::UserRefreshCredentials.make_creds json_key_io: key_io 138 | _(creds.principal).must_equal test_client_id 139 | end 140 | 141 | it "returns :user_refresh when client_id not available" do 142 | # This isn't a typical initialization path, but we need to test the fallback 143 | creds = Google::Auth::UserRefreshCredentials.new 144 | _(creds.principal).must_equal :user_refresh 145 | end 146 | end 147 | 148 | describe "UserAuthorizer" do 149 | let :expected_client_id do 150 | "test-client-id.apps.googleusercontent.com" 151 | end 152 | 153 | let :client_id do 154 | Google::Auth::ClientId.new( 155 | expected_client_id, 156 | "notsosecret" 157 | ) 158 | end 159 | 160 | it "returns the client id as principal" do 161 | scope = ["https://www.googleapis.com/auth/userinfo.email"] 162 | token_store = TestTokenStore.new 163 | authorizer = Google::Auth::UserAuthorizer.new( 164 | client_id, 165 | scope, 166 | token_store 167 | ) 168 | _(authorizer.principal).must_equal expected_client_id 169 | end 170 | end 171 | 172 | describe "WebUserAuthorizer" do 173 | it "should return :web_user_authorization as the principal" do 174 | _(Google::Auth::WebUserAuthorizer.principal).must_equal :web_user_authorization 175 | end 176 | end 177 | 178 | describe "External Account Base Credentials" do 179 | it "returns audience as principal" do 180 | # Create a test class inline that includes the module 181 | test_class = Class.new do 182 | include Google::Auth::ExternalAccount::BaseCredentials 183 | 184 | attr_reader :audience 185 | 186 | def initialize audience 187 | @audience = audience 188 | end 189 | end 190 | 191 | test_audience = "//iam.googleapis.com/projects/test-project/locations/global/workforce-pools/test" 192 | creds = test_class.new test_audience 193 | _(creds.principal).must_equal test_audience 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /lib/googleauth/service_account_jwt_header.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "google/logging/message" 16 | require "googleauth/credentials_loader" 17 | require "googleauth/json_key_reader" 18 | require "jwt" 19 | 20 | module Google 21 | # Module Auth provides classes that provide Google-specific authorization 22 | # used to access Google APIs. 23 | module Auth 24 | # Authenticates requests using Google's Service Account credentials via 25 | # JWT Header. 26 | # 27 | # This class allows authorizing requests for service accounts directly 28 | # from credentials from a json key file downloaded from the developer 29 | # console (via 'Generate new Json Key'). It is not part of any OAuth2 30 | # flow, rather it creates a JWT and sends that as a credential. 31 | # 32 | # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production) 33 | class ServiceAccountJwtHeaderCredentials 34 | JWT_AUD_URI_KEY = :jwt_aud_uri 35 | AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY 36 | TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze 37 | SIGNING_ALGORITHM = "RS256".freeze 38 | EXPIRY = 60 39 | 40 | extend CredentialsLoader 41 | extend JsonKeyReader 42 | 43 | attr_reader :project_id 44 | attr_reader :quota_project_id 45 | attr_accessor :universe_domain 46 | attr_accessor :logger 47 | 48 | # Create a ServiceAccountJwtHeaderCredentials. 49 | # 50 | # @param json_key_io [IO] An IO object containing the JSON key 51 | # @param scope [string|array|nil] the scope(s) to access 52 | def self.make_creds options = {} 53 | json_key_io, scope = options.values_at :json_key_io, :scope 54 | new json_key_io: json_key_io, scope: scope 55 | end 56 | 57 | # Initializes a ServiceAccountJwtHeaderCredentials. 58 | # 59 | # @param json_key_io [IO] An IO object containing the JSON key 60 | def initialize options = {} 61 | json_key_io = options[:json_key_io] 62 | if json_key_io 63 | @private_key, @issuer, @project_id, @quota_project_id, @universe_domain = 64 | self.class.read_json_key json_key_io 65 | else 66 | @private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR] 67 | @issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR] 68 | @project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR] 69 | @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id 70 | @universe_domain = options[:universe_domain] if options.key? :universe_domain 71 | end 72 | @universe_domain ||= "googleapis.com" 73 | @project_id ||= CredentialsLoader.load_gcloud_project_id 74 | @signing_key = OpenSSL::PKey::RSA.new @private_key 75 | @scope = options[:scope] if options.key? :scope 76 | @logger = options[:logger] if options.key? :logger 77 | end 78 | 79 | # Creates a duplicate of these credentials 80 | # 81 | # @param options [Hash] Overrides for the credentials parameters. 82 | # The following keys are recognized 83 | # * `private key` the private key in string form 84 | # * `issuer` the SA issuer 85 | # * `scope` the scope(s) to access 86 | # * `project_id` the project id to use during the authentication 87 | # * `quota_project_id` the quota project id to use 88 | # * `universe_domain` the universe domain of the credentials 89 | def duplicate options = {} 90 | options = deep_hash_normalize options 91 | 92 | options = { 93 | private_key: @private_key, 94 | issuer: @issuer, 95 | scope: @scope, 96 | project_id: project_id, 97 | quota_project_id: quota_project_id, 98 | universe_domain: universe_domain, 99 | logger: logger 100 | }.merge(options) 101 | 102 | self.class.new options 103 | end 104 | 105 | # Construct a jwt token if the JWT_AUD_URI key is present in the input 106 | # hash. 107 | # 108 | # The jwt token is used as the value of a 'Bearer '. 109 | def apply! a_hash, opts = {} 110 | jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY 111 | return a_hash if jwt_aud_uri.nil? && @scope.nil? 112 | jwt_token = new_jwt_token jwt_aud_uri, opts 113 | a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}" 114 | logger&.debug do 115 | hash = Digest::SHA256.hexdigest jwt_token 116 | Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})" 117 | end 118 | a_hash 119 | end 120 | 121 | # Returns a clone of a_hash updated with the authorization header 122 | def apply a_hash, opts = {} 123 | a_copy = a_hash.clone 124 | apply! a_copy, opts 125 | a_copy 126 | end 127 | 128 | # Returns a reference to the #apply method, suitable for passing as 129 | # a closure 130 | def updater_proc 131 | proc { |a_hash, opts = {}| apply a_hash, opts } 132 | end 133 | 134 | # Creates a jwt uri token. 135 | def new_jwt_token jwt_aud_uri = nil, options = {} 136 | now = Time.new 137 | skew = options[:skew] || 60 138 | assertion = { 139 | "iss" => @issuer, 140 | "sub" => @issuer, 141 | "exp" => (now + EXPIRY).to_i, 142 | "iat" => (now - skew).to_i 143 | } 144 | 145 | jwt_aud_uri = nil if @scope 146 | 147 | assertion["scope"] = Array(@scope).join " " if @scope 148 | assertion["aud"] = jwt_aud_uri if jwt_aud_uri 149 | 150 | logger&.debug do 151 | Google::Logging::Message.from message: "JWT assertion: #{assertion}" 152 | end 153 | 154 | JWT.encode assertion, @signing_key, SIGNING_ALGORITHM 155 | end 156 | 157 | # Duck-types the corresponding method from BaseClient 158 | def needs_access_token? 159 | false 160 | end 161 | 162 | # Returns the client email as the principal for service account JWT header credentials 163 | # @private 164 | # @return [String] the email address of the service account 165 | def principal 166 | @issuer 167 | end 168 | 169 | private 170 | 171 | def deep_hash_normalize old_hash 172 | sym_hash = {} 173 | old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v } 174 | sym_hash 175 | end 176 | 177 | # Convert all keys in this hash (nested) to symbols for uniform retrieval 178 | def recursive_hash_normalize_keys val 179 | if val.is_a? Hash 180 | deep_hash_normalize val 181 | else 182 | val 183 | end 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /Credentials.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The closest thing to a base credentials class is the `BaseClient` module. 4 | 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. 5 | 6 | Many credentials classes inherit from `Signet::OAuth2::Client` (`lib/googleauth/signet.rb`) class which provides OAuth-based authentication. 7 | The `Signet::OAuth2::Client` includes the `BaseClient` functionality. 8 | 9 | Most credential types either inherit from `Signet::OAuth2::Client` or include the `BaseClient` module directly. 10 | 11 | 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 12 | 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. 13 | 14 | # List of credentials types 15 | 16 | ## Simple Authentication (non-OAuth) 17 | 18 | **Google::Auth::APIKeyCredentials** - `lib/googleauth/api_key.rb` 19 | - Includes `Google::Auth::BaseClient` module 20 | - Implements Google API Key authentication 21 | - API Keys are text strings that don't have an associated JSON file 22 | - API Keys provide project information but don't reference an IAM principal 23 | - They do not expire and cannot be refreshed 24 | - Can be loaded from the `GOOGLE_API_KEY` environment variable 25 | 26 | 2. **Google::Auth::BearerTokenCredentials** - `lib/googleauth/bearer_token.rb` 27 | - Includes `Google::Auth::BaseClient` module 28 | - Implements Bearer Token authentication 29 | - Bearer tokens are strings representing an authorization grant 30 | - Can be OAuth2 tokens, JWTs, ID tokens, or any token sent as a `Bearer` in an `Authorization` header 31 | - Used when the end-user is managing the token separately (e.g., with another service) 32 | - Token lifetime tracking and refresh are outside this class's scope 33 | - No JSON representation for this type of credentials 34 | 35 | ## GCP-Specialized authentication 36 | 37 | 3. **Google::Auth::GCECredentials < Signet::OAuth2::Client** - `lib/googleauth/compute_engine.rb` 38 | - For obtaining authentication tokens from GCE metadata server 39 | - Used automatically when code is running on Google Compute Engine 40 | - Fetches tokens from the metadata server with no additional configuration needed 41 | - This credential type does not have a supported JSON form 42 | 43 | 4. **Google::Auth::IAMCredentials < Signet::OAuth2::Client** - `lib/googleauth/iam.rb` 44 | - For IAM-based authentication (e.g. service-to-service) 45 | - Implements authentication-as-a-service for systems already authenticated 46 | - Exchanges existing credentials for a short-lived access token 47 | - This credential type does not have a supported JSON form 48 | 49 | ## Service Account Authentication 50 | 51 | 5. **Google::Auth::ServiceAccountCredentials < Signet::OAuth2::Client** - `lib/googleauth/service_account.rb` 52 | - Authenticates requests using Service Account credentials via an OAuth access token 53 | - 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"`. 54 | - Supports both OAuth access tokens and self-signed JWT authentication 55 | - Can specify scopes for access token requests 56 | 57 | 6. **Google::Auth::ServiceAccountJwtHeaderCredentials** - `lib/googleauth/service_account_jwt_header.rb` 58 | - Authenticates using Service Account credentials with JWT headers 59 | - Typically used via `ServiceAccountCredentials` and not by itself 60 | - Creates JWT directly for making authenticated calls 61 | - Does not require a round trip to the authorization server 62 | - Doesn't support OAuth scopes - uses audience (target API) instead 63 | 64 | 7. **Google::Auth::ImpersonatedServiceAccountCredentials < Signet::OAuth2::Client** - `lib/googleauth/impersonated_service_account.rb` 65 | - For service account impersonation 66 | - Allows a GCP principal identified by a set of source credentials to impersonate a service account 67 | - Useful for delegation of authority and managing permissions across service accounts 68 | - Source credentials must have the Service Account Token Creator role on the target 69 | - This credential type supports JSON configuration. The JSON form of this credential type has a `"type"` field with the value `"impersonated_service_account"`. 70 | 71 | ## User Authentication 72 | 73 | 8. **Google::Auth::UserRefreshCredentials < Signet::OAuth2::Client** - `lib/googleauth/user_refresh.rb` 74 | - For user refresh token authentication (from 3-legged OAuth flow) 75 | - Authenticates on behalf of a user who has authorized the application 76 | - Handles token refresh when original access token expires 77 | - Typically obtained through web or installed application flow. The JSON form of this credential type has a `"type"` field with the value `"authorized_user"`. 78 | 79 | `Google::Auth::UserAuthorizer` (`lib/googleauth/user_authorizer.rb`) and `Google::Auth::WebUserAuthorizer` (`lib/googleauth/web_user_authorizer.rb`) 80 | are used to facilitate user authentication. The `UserAuthorizer` handles interactive 3-Legged-OAuth2 (3LO) user consent authorization for command-line applications. 81 | The `WebUserAuthorizer` is a variation of UserAuthorizer adapted for Rack-based web applications that manages OAuth state and provides callback handling. 82 | 83 | ## External Account Authentication 84 | `Google::Auth::ExternalAccount::Credentials` (`lib/googleauth/external_account.rb`) is not a credentials type, it is a module 85 | that procides an entry point for External Account credentials. It also serves as a factory that creates appropriate credential 86 | types based on credential source (similar to `Google::Auth::get_application_default`). 87 | It is included in all External Account credentials types, and it itself includes `Google::Auth::BaseClient` module so all External 88 | Account credentials types include `Google::Auth::BaseClient`. 89 | The JSON form of this credential type has a `"type"` field with the value `"external_account"`. 90 | 91 | 9. **Google::Auth::ExternalAccount::AwsCredentials** - `lib/googleauth/external_account/aws_credentials.rb` 92 | - Includes `Google::Auth::BaseClient` module 93 | - Includes `ExternalAccount::BaseCredentials` module 94 | - Uses AWS credentials to authenticate to Google Cloud 95 | - Exchanges temporary AWS credentials for Google access tokens 96 | - Used for workloads running on AWS that need to access Google Cloud 97 | 98 | 10. **Google::Auth::ExternalAccount::IdentityPoolCredentials** - `lib/googleauth/external_account/identity_pool_credentials.rb` 99 | - Includes `Google::Auth::BaseClient` module 100 | - Includes `ExternalAccount::BaseCredentials` module 101 | - Authenticates using external identity pool 102 | - Exchanges external identity tokens for Google access tokens 103 | - Supports file-based and URL-based credential sources 104 | 105 | 11. **Google::Auth::ExternalAccount::PluggableCredentials** - `lib/googleauth/external_account/pluggable_credentials.rb` 106 | - Includes `Google::Auth::BaseClient` module 107 | - Includes `ExternalAccount::BaseCredentials` module 108 | - Supports executable-based credential sources 109 | - Executes external programs to retrieve credentials 110 | - Allows for custom authentication mechanisms via external executables 111 | -------------------------------------------------------------------------------- /lib/googleauth/user_refresh.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "googleauth/credentials_loader" 16 | require "googleauth/errors" 17 | require "googleauth/scope_util" 18 | require "googleauth/signet" 19 | require "multi_json" 20 | 21 | module Google 22 | # Module Auth provides classes that provide Google-specific authorization 23 | # used to access Google APIs. 24 | module Auth 25 | # Authenticates requests using User Refresh credentials. 26 | # 27 | # This class allows authorizing requests from user refresh tokens. 28 | # 29 | # This the end of the result of a 3LO flow. E.g, the end result of 30 | # 'gcloud auth login' saves a file with these contents in well known 31 | # location 32 | # 33 | # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production) 34 | class UserRefreshCredentials < Signet::OAuth2::Client 35 | TOKEN_CRED_URI = "https://oauth2.googleapis.com/token".freeze 36 | AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/auth".freeze 37 | REVOKE_TOKEN_URI = "https://oauth2.googleapis.com/revoke".freeze 38 | extend CredentialsLoader 39 | attr_reader :project_id 40 | attr_reader :quota_project_id 41 | 42 | # @private 43 | # @type [::String] The type name for this credential. 44 | CREDENTIAL_TYPE_NAME = "authorized_user".freeze 45 | 46 | # Create a UserRefreshCredentials. 47 | # 48 | # @param json_key_io [IO] An IO object containing the JSON key 49 | # @param scope [string|array|nil] the scope(s) to access 50 | def self.make_creds options = {} 51 | json_key_io, scope = options.values_at :json_key_io, :scope 52 | user_creds = if json_key_io 53 | CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME 54 | read_json_key json_key_io 55 | else 56 | { 57 | "client_id" => ENV[CredentialsLoader::CLIENT_ID_VAR], 58 | "client_secret" => ENV[CredentialsLoader::CLIENT_SECRET_VAR], 59 | "refresh_token" => ENV[CredentialsLoader::REFRESH_TOKEN_VAR], 60 | "project_id" => ENV[CredentialsLoader::PROJECT_ID_VAR], 61 | "quota_project_id" => nil, 62 | "universe_domain" => nil 63 | } 64 | end 65 | new(token_credential_uri: TOKEN_CRED_URI, 66 | client_id: user_creds["client_id"], 67 | client_secret: user_creds["client_secret"], 68 | refresh_token: user_creds["refresh_token"], 69 | project_id: user_creds["project_id"], 70 | quota_project_id: user_creds["quota_project_id"], 71 | scope: scope, 72 | universe_domain: user_creds["universe_domain"] || "googleapis.com") 73 | .configure_connection(options) 74 | end 75 | 76 | # Reads a JSON key from an IO object and extracts required fields. 77 | # 78 | # @param [IO] json_key_io An IO object containing the JSON key 79 | # @return [Hash] The parsed JSON key 80 | # @raise [Google::Auth::InitializationError] If the JSON is missing required fields 81 | def self.read_json_key json_key_io 82 | json_key = MultiJson.load json_key_io.read 83 | wanted = ["client_id", "client_secret", "refresh_token"] 84 | wanted.each do |key| 85 | raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key 86 | end 87 | json_key 88 | end 89 | 90 | def initialize options = {} 91 | options ||= {} 92 | options[:token_credential_uri] ||= TOKEN_CRED_URI 93 | options[:authorization_uri] ||= AUTHORIZATION_URI 94 | @project_id = options[:project_id] 95 | @project_id ||= CredentialsLoader.load_gcloud_project_id 96 | @quota_project_id = options[:quota_project_id] 97 | super options 98 | end 99 | 100 | # Creates a duplicate of these credentials 101 | # without the Signet::OAuth2::Client-specific 102 | # transient state (e.g. cached tokens) 103 | # 104 | # @param options [Hash] Overrides for the credentials parameters. 105 | # The following keys are recognized in addition to keys in the 106 | # Signet::OAuth2::Client 107 | # * `project_id` the project id to use during the authentication 108 | # * `quota_project_id` the quota project id to use 109 | # during the authentication 110 | def duplicate options = {} 111 | options = deep_hash_normalize options 112 | super( 113 | { 114 | project_id: @project_id, 115 | quota_project_id: @quota_project_id 116 | }.merge(options) 117 | ) 118 | end 119 | 120 | # Revokes the credential 121 | # 122 | # @param [Hash] options Options for revoking the credential 123 | # @option options [Faraday::Connection] :connection The connection to use 124 | # @raise [Google::Auth::AuthorizationError] If the revocation request fails 125 | def revoke! options = {} 126 | c = options[:connection] || Faraday.default_connection 127 | 128 | retry_with_error do 129 | resp = c.post(REVOKE_TOKEN_URI, token: refresh_token || access_token) 130 | case resp.status 131 | when 200 132 | self.access_token = nil 133 | self.refresh_token = nil 134 | self.expires_at = 0 135 | else 136 | raise AuthorizationError.with_details( 137 | "Unexpected error code #{resp.status}", 138 | credential_type_name: self.class.name, 139 | principal: principal 140 | ) 141 | end 142 | end 143 | end 144 | 145 | # Verifies that a credential grants the requested scope 146 | # 147 | # @param [Array, String] required_scope 148 | # Scope to verify 149 | # @return [Boolean] 150 | # True if scope is granted 151 | def includes_scope? required_scope 152 | missing_scope = Google::Auth::ScopeUtil.normalize(required_scope) - 153 | Google::Auth::ScopeUtil.normalize(scope) 154 | missing_scope.empty? 155 | end 156 | 157 | # Destructively updates these credentials 158 | # 159 | # This method is called by `Signet::OAuth2::Client`'s constructor 160 | # 161 | # @param options [Hash] Overrides for the credentials parameters. 162 | # The following keys are recognized in addition to keys in the 163 | # Signet::OAuth2::Client 164 | # * `project_id` the project id to use during the authentication 165 | # * `quota_project_id` the quota project id to use 166 | # during the authentication 167 | # @return [Google::Auth::UserRefreshCredentials] 168 | def update! options = {} 169 | # Normalize all keys to symbols to allow indifferent access. 170 | options = deep_hash_normalize options 171 | 172 | @project_id = options[:project_id] if options.key? :project_id 173 | @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id 174 | 175 | super(options) 176 | 177 | self 178 | end 179 | 180 | # Returns the client ID as the principal for user refresh credentials 181 | # @private 182 | # @return [String, Symbol] the client ID or :user_refresh if not available 183 | def principal 184 | @client_id || :user_refresh 185 | end 186 | end 187 | end 188 | end 189 | --------------------------------------------------------------------------------