From a254aa2df48d6be9c83e5e27319a740841cec789 Mon Sep 17 00:00:00 2001 From: Daniel Azuma Date: Fri, 4 Oct 2024 13:13:18 -0700 Subject: [PATCH] fix: Fixed parsing of expiration timestamp from ID tokens (#492) --- lib/googleauth/compute_engine.rb | 11 +++++- lib/googleauth/signet.rb | 17 ++++++++++ spec/googleauth/compute_engine_spec.rb | 45 +++++++++++++++++++------ spec/googleauth/service_account_spec.rb | 38 +++++++++++++++++++-- 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/lib/googleauth/compute_engine.rb b/lib/googleauth/compute_engine.rb index d7318e14..2c7ee021 100644 --- a/lib/googleauth/compute_engine.rb +++ b/lib/googleauth/compute_engine.rb @@ -123,7 +123,7 @@ def fetch_access_token _options = {} def build_token_hash body, content_type, retrieval_time hash = if ["text/html", "application/text"].include? content_type - { token_type.to_s => body } + parse_encoded_token body else Signet::OAuth2.parse_credentials body, content_type end @@ -143,6 +143,15 @@ def build_token_hash body, content_type, retrieval_time end hash end + + def parse_encoded_token body + hash = { token_type.to_s => body } + if token_type == :id_token + expires_at = expires_at_from_id_token body + hash["expires_at"] = expires_at if expires_at + end + hash + end end end end diff --git a/lib/googleauth/signet.rb b/lib/googleauth/signet.rb index 7de875e8..1c3016db 100644 --- a/lib/googleauth/signet.rb +++ b/lib/googleauth/signet.rb @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +require "base64" +require "json" require "signet/oauth_2/client" require "googleauth/base_client" @@ -29,6 +31,8 @@ class Client def update_token! options = {} options = deep_hash_normalize options + id_token_expires_at = expires_at_from_id_token options[:id_token] + options[:expires_at] = id_token_expires_at if id_token_expires_at update_token_signet_base options self.universe_domain = options[:universe_domain] if options.key? :universe_domain self @@ -89,6 +93,19 @@ def retry_with_error max_retry_count = 5 end end end + + private + + def expires_at_from_id_token id_token + match = /^[\w=-]+\.([\w=-]+)\.[\w=-]+$/.match id_token.to_s + return unless match + json = JSON.parse Base64.urlsafe_decode64 match[1] + return unless json.key? "exp" + Time.at json["exp"].to_i + rescue StandardError + # Shouldn't happen unless we get a garbled ID token + nil + end end end end diff --git a/spec/googleauth/compute_engine_spec.rb b/spec/googleauth/compute_engine_spec.rb index e0b46f09..ad308ec0 100644 --- a/spec/googleauth/compute_engine_spec.rb +++ b/spec/googleauth/compute_engine_spec.rb @@ -84,7 +84,7 @@ def make_auth_stubs opts expiry = @client.expires_at sleep 1 @client.fetch_access_token! - expect(@client.expires_at.to_f).to be_within(0.1).of(expiry.to_f) + expect(@client.expires_at.to_f).to be_within(0.2).of(expiry.to_f) end end @@ -107,7 +107,7 @@ def make_auth_stubs opts expiry = @client.expires_at sleep 1 @client.fetch_access_token! - expect(@client.expires_at.to_f).to be_within(0.1).of(expiry.to_f) + expect(@client.expires_at.to_f).to be_within(0.2).of(expiry.to_f) end end @@ -152,16 +152,46 @@ def make_auth_stubs opts end end - context "metadata is unavailable" do + context "metadata is available" do describe "#fetch_access_token" do - it "should pass scopes when requesting an access token" do + it "should pass scopes" do scopes = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/bigtable.data"] stub = make_auth_stubs access_token: "1/abcdef1234567890", scope: scopes @client = GCECredentials.new(scope: scopes) @client.fetch_access_token! expect(stub).to have_been_requested end + end + + describe "Fetch ID tokens" do + it "should parse out expiration time" do + expiry_time = 1608886800 + header = { + alg: "RS256", + kid: "1234567890123456789012345678901234567890", + typ: "JWT" + } + payload = { + aud: "http://www.example.com", + azp: "67890", + email: "googleapis-test@developer.gserviceaccount.com", + email_verified: true, + exp: expiry_time, + iat: expiry_time - 3600, + iss: "https://accounts.google.com", + sub: "12345" + } + token = "#{Base64.urlsafe_encode64 JSON.dump header}.#{Base64.urlsafe_encode64 JSON.dump payload}.xxxxx" + stub = make_auth_stubs id_token: token + @id_client.fetch_access_token! + expect(stub).to have_been_requested + expect(@id_client.expires_at.to_i).to eq(expiry_time) + end + end + end + context "metadata is unavailable" do + describe "#fetch_access_token" do it "should fail if the metadata request returns a 404" do stub = stub_request(:get, MD_ACCESS_URI) .to_return(status: 404, @@ -214,13 +244,6 @@ def make_auth_stubs opts end describe "Fetch ID tokens" do - it "should pass scopes when requesting an ID token" do - scopes = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/bigtable.data"] - stub = make_auth_stubs id_token: "1/abcdef1234567890", scope: scopes - @id_client.fetch_access_token! - expect(stub).to have_been_requested - end - it "should fail if the metadata request returns a 404" do stub = stub_request(:get, MD_ID_URI) .to_return(status: 404, diff --git a/spec/googleauth/service_account_spec.rb b/spec/googleauth/service_account_spec.rb index 6f031ce1..4c7dd5f0 100644 --- a/spec/googleauth/service_account_spec.rb +++ b/spec/googleauth/service_account_spec.rb @@ -137,9 +137,14 @@ def expect_is_encoded_jwt hdr end def make_auth_stubs opts - body_fields = { "token_type" => "Bearer", "expires_in" => 3600 } - body_fields["access_token"] = opts[:access_token] if opts[:access_token] - body_fields["id_token"] = opts[:id_token] if opts[:id_token] + body_fields = + if opts[:access_token] + { "access_token" => opts[:access_token], "token_type" => "Bearer", "expires_in" => 3600 } + elsif opts[:id_token] + { "id_token" => opts[:id_token] } + else + raise "Expected access_token or id_token" + end body = MultiJson.dump body_fields blk = proc do |request| params = Addressable::URI.form_unencode request.body @@ -217,6 +222,33 @@ def cred_json_text_with_universe_domain it_behaves_like "jwt header auth", nil end + context "when target_audience is set" do + it "retrieves an ID token with expiration" do + expiry_time = 1608886800 + header = { + alg: "RS256", + kid: "1234567890123456789012345678901234567890", + typ: "JWT" + } + payload = { + aud: "http://www.example.com", + azp: "67890", + email: "googleapis-test@developer.gserviceaccount.com", + email_verified: true, + exp: expiry_time, + iat: expiry_time - 3600, + iss: "https://accounts.google.com", + sub: "12345" + } + id_token = "#{Base64.urlsafe_encode64 JSON.dump header}.#{Base64.urlsafe_encode64 JSON.dump payload}.xxxxx" + stub = make_auth_stubs id_token: id_token + @id_client.fetch_access_token! + expect(stub).to have_been_requested + expect(@id_client.id_token).to eq(id_token) + expect(@id_client.expires_at.to_i).to eq(expiry_time) + end + end + describe "#from_env" do before :example do @var_name = ENV_VAR