Skip to content

Commit a254aa2

Browse files
authored
fix: Fixed parsing of expiration timestamp from ID tokens (#492)
1 parent 4b81240 commit a254aa2

File tree

4 files changed

+96
-15
lines changed

4 files changed

+96
-15
lines changed

lib/googleauth/compute_engine.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def fetch_access_token _options = {}
123123
def build_token_hash body, content_type, retrieval_time
124124
hash =
125125
if ["text/html", "application/text"].include? content_type
126-
{ token_type.to_s => body }
126+
parse_encoded_token body
127127
else
128128
Signet::OAuth2.parse_credentials body, content_type
129129
end
@@ -143,6 +143,15 @@ def build_token_hash body, content_type, retrieval_time
143143
end
144144
hash
145145
end
146+
147+
def parse_encoded_token body
148+
hash = { token_type.to_s => body }
149+
if token_type == :id_token
150+
expires_at = expires_at_from_id_token body
151+
hash["expires_at"] = expires_at if expires_at
152+
end
153+
hash
154+
end
146155
end
147156
end
148157
end

lib/googleauth/signet.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
require "base64"
16+
require "json"
1517
require "signet/oauth_2/client"
1618
require "googleauth/base_client"
1719

@@ -29,6 +31,8 @@ class Client
2931

3032
def update_token! options = {}
3133
options = deep_hash_normalize options
34+
id_token_expires_at = expires_at_from_id_token options[:id_token]
35+
options[:expires_at] = id_token_expires_at if id_token_expires_at
3236
update_token_signet_base options
3337
self.universe_domain = options[:universe_domain] if options.key? :universe_domain
3438
self
@@ -89,6 +93,19 @@ def retry_with_error max_retry_count = 5
8993
end
9094
end
9195
end
96+
97+
private
98+
99+
def expires_at_from_id_token id_token
100+
match = /^[\w=-]+\.([\w=-]+)\.[\w=-]+$/.match id_token.to_s
101+
return unless match
102+
json = JSON.parse Base64.urlsafe_decode64 match[1]
103+
return unless json.key? "exp"
104+
Time.at json["exp"].to_i
105+
rescue StandardError
106+
# Shouldn't happen unless we get a garbled ID token
107+
nil
108+
end
92109
end
93110
end
94111
end

spec/googleauth/compute_engine_spec.rb

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def make_auth_stubs opts
8484
expiry = @client.expires_at
8585
sleep 1
8686
@client.fetch_access_token!
87-
expect(@client.expires_at.to_f).to be_within(0.1).of(expiry.to_f)
87+
expect(@client.expires_at.to_f).to be_within(0.2).of(expiry.to_f)
8888
end
8989
end
9090

@@ -107,7 +107,7 @@ def make_auth_stubs opts
107107
expiry = @client.expires_at
108108
sleep 1
109109
@client.fetch_access_token!
110-
expect(@client.expires_at.to_f).to be_within(0.1).of(expiry.to_f)
110+
expect(@client.expires_at.to_f).to be_within(0.2).of(expiry.to_f)
111111
end
112112
end
113113

@@ -152,16 +152,46 @@ def make_auth_stubs opts
152152
end
153153
end
154154

155-
context "metadata is unavailable" do
155+
context "metadata is available" do
156156
describe "#fetch_access_token" do
157-
it "should pass scopes when requesting an access token" do
157+
it "should pass scopes" do
158158
scopes = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/bigtable.data"]
159159
stub = make_auth_stubs access_token: "1/abcdef1234567890", scope: scopes
160160
@client = GCECredentials.new(scope: scopes)
161161
@client.fetch_access_token!
162162
expect(stub).to have_been_requested
163163
end
164+
end
165+
166+
describe "Fetch ID tokens" do
167+
it "should parse out expiration time" do
168+
expiry_time = 1608886800
169+
header = {
170+
alg: "RS256",
171+
kid: "1234567890123456789012345678901234567890",
172+
typ: "JWT"
173+
}
174+
payload = {
175+
aud: "http://www.example.com",
176+
azp: "67890",
177+
email: "googleapis-test@developer.gserviceaccount.com",
178+
email_verified: true,
179+
exp: expiry_time,
180+
iat: expiry_time - 3600,
181+
iss: "https://accounts.google.com",
182+
sub: "12345"
183+
}
184+
token = "#{Base64.urlsafe_encode64 JSON.dump header}.#{Base64.urlsafe_encode64 JSON.dump payload}.xxxxx"
185+
stub = make_auth_stubs id_token: token
186+
@id_client.fetch_access_token!
187+
expect(stub).to have_been_requested
188+
expect(@id_client.expires_at.to_i).to eq(expiry_time)
189+
end
190+
end
191+
end
164192

193+
context "metadata is unavailable" do
194+
describe "#fetch_access_token" do
165195
it "should fail if the metadata request returns a 404" do
166196
stub = stub_request(:get, MD_ACCESS_URI)
167197
.to_return(status: 404,
@@ -214,13 +244,6 @@ def make_auth_stubs opts
214244
end
215245

216246
describe "Fetch ID tokens" do
217-
it "should pass scopes when requesting an ID token" do
218-
scopes = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/bigtable.data"]
219-
stub = make_auth_stubs id_token: "1/abcdef1234567890", scope: scopes
220-
@id_client.fetch_access_token!
221-
expect(stub).to have_been_requested
222-
end
223-
224247
it "should fail if the metadata request returns a 404" do
225248
stub = stub_request(:get, MD_ID_URI)
226249
.to_return(status: 404,

spec/googleauth/service_account_spec.rb

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,14 @@ def expect_is_encoded_jwt hdr
137137
end
138138

139139
def make_auth_stubs opts
140-
body_fields = { "token_type" => "Bearer", "expires_in" => 3600 }
141-
body_fields["access_token"] = opts[:access_token] if opts[:access_token]
142-
body_fields["id_token"] = opts[:id_token] if opts[:id_token]
140+
body_fields =
141+
if opts[:access_token]
142+
{ "access_token" => opts[:access_token], "token_type" => "Bearer", "expires_in" => 3600 }
143+
elsif opts[:id_token]
144+
{ "id_token" => opts[:id_token] }
145+
else
146+
raise "Expected access_token or id_token"
147+
end
143148
body = MultiJson.dump body_fields
144149
blk = proc do |request|
145150
params = Addressable::URI.form_unencode request.body
@@ -217,6 +222,33 @@ def cred_json_text_with_universe_domain
217222
it_behaves_like "jwt header auth", nil
218223
end
219224

225+
context "when target_audience is set" do
226+
it "retrieves an ID token with expiration" do
227+
expiry_time = 1608886800
228+
header = {
229+
alg: "RS256",
230+
kid: "1234567890123456789012345678901234567890",
231+
typ: "JWT"
232+
}
233+
payload = {
234+
aud: "http://www.example.com",
235+
azp: "67890",
236+
email: "googleapis-test@developer.gserviceaccount.com",
237+
email_verified: true,
238+
exp: expiry_time,
239+
iat: expiry_time - 3600,
240+
iss: "https://accounts.google.com",
241+
sub: "12345"
242+
}
243+
id_token = "#{Base64.urlsafe_encode64 JSON.dump header}.#{Base64.urlsafe_encode64 JSON.dump payload}.xxxxx"
244+
stub = make_auth_stubs id_token: id_token
245+
@id_client.fetch_access_token!
246+
expect(stub).to have_been_requested
247+
expect(@id_client.id_token).to eq(id_token)
248+
expect(@id_client.expires_at.to_i).to eq(expiry_time)
249+
end
250+
end
251+
220252
describe "#from_env" do
221253
before :example do
222254
@var_name = ENV_VAR

0 commit comments

Comments
 (0)