Skip to content

Commit

Permalink
align expiration of bearer token
Browse files Browse the repository at this point in the history
  • Loading branch information
viacheslav-rostovtsev committed Feb 14, 2025
1 parent f209a19 commit b842ab9
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 58 deletions.
2 changes: 1 addition & 1 deletion lib/googleauth/api_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def duplicate options = {}
)
end

# Updates the procided hash with the API Key header.
# Updates the provided hash with the API Key header.
#
# The `apply!` method modifies the provided hash in place, adding the
# `x-goog-api-key` header with the API Key value.
Expand Down
49 changes: 31 additions & 18 deletions lib/googleauth/bearer_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class BearerTokenCredentials
alias id_token token
alias bearer_token token

# @return [Time, nil] The token expiry time provided by the end-user.
attr_reader :expiry
# @return [Time, nil] The token expiration time provided by the end-user.
attr_reader :expires_at

# @return [Symbol] The token type. Allowed values are
# :access_token, :jwt, :id_token, and :bearer_token.
Expand All @@ -70,9 +70,9 @@ class << self
#
# @param [Hash] options The credentials options
# @option options [String] :token The bearer token to use.
# @option options [Time, Numeric, nil] :expiry The token expiry time provided by the end-user.
# @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
# Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
# If the expiry is `nil`, it is treated as "token never expires".
# If `expires_at` is `nil`, it is treated as "token never expires".
# @option options [Symbol] :token_type The token type. Allowed values are
# :access_token, :jwt, :id_token, and :bearer_token. Defaults to :bearer_token.
# @option options [String] :universe_domain The universe domain of the universe
Expand All @@ -87,21 +87,22 @@ def make_creds options = {}
#
# @param [Hash] options The credentials options
# @option options [String] :token The bearer token to use.
# @option options [Time, Numeric, nil] :expiry The token expiry time provided by the end-user.
# @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
# Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
# If the expiry is `nil`, it is treated as "token never expires".
# If `expires_at` is `nil`, it is treated as "token never expires".
# @option options [Symbol] :token_type The token type. Allowed values are
# :access_token, :jwt, :id_token, and :bearer_token. Defaults to :bearer_token.
# @option options [String] :universe_domain The universe domain of the universe
# this token is for (defaults to googleapis.com)
def initialize options = {}
raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
@token = options[:token]
@expiry = if options[:expiry].is_a? Time
options[:expiry]
elsif options[:expiry].is_a? Numeric
Time.at options[:expiry]
end
@expires_at = case options[:expires_at]
when Time
options[:expires_at]
when Numeric
Time.at options[:expires_at]
end

@token_type = options[:token_type] || :bearer_token
unless ALLOWED_TOKEN_TYPES.include? @token_type
Expand All @@ -115,17 +116,17 @@ def initialize options = {}
#
# @param [Numeric] seconds The optional timeout in seconds.
# @return [Boolean] True if the token has expired, false otherwise, or
# if the expiry was not provided.
# if the expires_at was not provided.
def expires_within? seconds
return false if @expiry.nil? # Treat nil expiry as "never expires"
Time.now + seconds >= @expiry
return false if @expires_at.nil? # Treat nil expiration as "never expires"
Time.now + seconds >= @expires_at
end

# Creates a duplicate of these credentials.
#
# @param [Hash] options Additional options for configuring the credentials
# @option options [String] :token The bearer token to use.
# @option options [Time, Numeric] :expiry The token expiry time. Can be a Time
# @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time
# object or a number of seconds since epoch.
# @option options [Symbol] :token_type The token type. Allowed values are
# :access_token, :jwt, :id_token, and :bearer_token. Defaults to :bearer_token.
Expand All @@ -134,17 +135,29 @@ def expires_within? seconds
def duplicate options = {}
self.class.new(
token: options[:token] || @token,
expiry: options[:expiry] || @expiry,
expires_at: options[:expires_at] || @expires_at,
token_type: options[:token_type] || @token_type,
universe_domain: options[:universe_domain] || @universe_domain
)
end

protected

# We don't need to fetch access tokens for bearer token auth
##
# BearerTokenCredentials do not support fetching a new token.
#
# If the token has an expiration time and is expired, this method will
# raise an error.
#
# @param [Hash] _options Options for fetching a new token (not used).
# @return [nil] Always returns nil.
# @raise [StandardError] If the token is expired.
def fetch_access_token! _options = {}
@token
if @expires_at && Time.now >= @expires_at
raise "Bearer token has expired."
end

nil
end

private
Expand Down
98 changes: 59 additions & 39 deletions test/bearer_token_test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Google LLC
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -20,7 +20,7 @@
describe Google::Auth::BearerTokenCredentials do
let(:token) { "test-bearer-token-12345" }
let(:example_universe_domain) { "example.com" }
let(:expiry) { Time.now + 3600 } # 1 hour from now
let(:expires_at) { Time.now + 3600 } # 1 hour from now

describe "#initialize" do
it "creates with token and proper defaults" do
Expand All @@ -38,15 +38,15 @@
_(creds.universe_domain).must_equal example_universe_domain
end

it "creates with expiry as Time object" do
creds = Google::Auth::BearerTokenCredentials.new(token: token, expiry: expiry)
_(creds.expiry).must_equal expiry
it "creates with expires_at as Time object" do
creds = Google::Auth::BearerTokenCredentials.new(token: token, expires_at: expires_at)
_(creds.expires_at).must_equal expires_at
end

it "creates with expiry as Numeric timestamp" do
expiry_seconds = expiry.to_i
creds = Google::Auth::BearerTokenCredentials.new(token: token, expiry: expiry_seconds)
_(creds.expiry).must_equal Time.at(expiry_seconds)
it "creates with expires_at as Numeric timestamp" do
expires_at_seconds = expires_at.to_i
creds = Google::Auth::BearerTokenCredentials.new(token: token, expires_at: expires_at_seconds)
_(creds.expires_at).must_equal Time.at(expires_at_seconds)
end

it "creates with custom token type" do
Expand All @@ -60,7 +60,7 @@
end.must_raise ArgumentError
end

it "raises if bearer token is empty" do
it "raises if bearer token is empty" do
expect do
Google::Auth::BearerTokenCredentials.new(token: "")
end.must_raise ArgumentError
Expand All @@ -78,79 +78,99 @@

it "adds Authorization token header to hash" do
md = { foo: "bar" }
want = {:foo => "bar", Google::Auth::BearerTokenCredentials::AUTH_METADATA_KEY => "Bearer #{token}" }
want = { foo: "bar", Google::Auth::BearerTokenCredentials::AUTH_METADATA_KEY => "Bearer #{token}" }
md = creds.apply md
_(md).must_equal want
end

it "Token type does not influence the header value" do
creds = Google::Auth::BearerTokenCredentials.new token: token, token_type: :access_token
md = { foo: "bar" }
want = {:foo => "bar", Google::Auth::BearerTokenCredentials::AUTH_METADATA_KEY => "Bearer #{token}" }
md = creds.apply md
_(md).must_equal want
end
creds = Google::Auth::BearerTokenCredentials.new token: token, token_type: :access_token
md = { foo: "bar" }
want = { foo: "bar", Google::Auth::BearerTokenCredentials::AUTH_METADATA_KEY => "Bearer #{token}" }
md = creds.apply md
_(md).must_equal want
end

it "logs (hashed token) when a logger is set" do
it "logs (hashed token) when a logger is set, but not the raw token" do
strio = StringIO.new
logger = Logger.new strio
creds.logger = logger

creds.apply({})

_(strio.string).wont_be:empty?

hashed_token = Digest::SHA256.hexdigest(token)
_(strio.string).must_include hashed_token # Check if the hash is logged.
_(strio.string).wont_include token # Explicitly check that the raw token is NOT logged.
_(strio.string).must_include hashed_token
_(strio.string).wont_include token
end
end

describe "#token_type" do
it "defaults to :bearer_token" do
creds = Google::Auth::BearerTokenCredentials.new token: token
_(creds.token_type).must_equal :bearer_token
creds = Google::Auth::BearerTokenCredentials.new token: token
_(creds.token_type).must_equal :bearer_token
end

it "returns the provided token type" do
creds = Google::Auth::BearerTokenCredentials.new token: token, token_type: :access_token
_(creds.token_type).must_equal :access_token
creds = Google::Auth::BearerTokenCredentials.new token: token, token_type: :access_token
_(creds.token_type).must_equal :access_token
end
end

describe "#duplicate" do
let(:creds) { Google::Auth::BearerTokenCredentials.new token: token, expiry: expiry, token_type: :access_token}
let(:creds) { Google::Auth::BearerTokenCredentials.new token: token, expires_at: expires_at, token_type: :access_token }

it "creates a duplicate with same values" do
dup = creds.duplicate
_(dup.token).must_equal token
_(dup.expiry).must_equal expiry
_(dup.expires_at).must_equal expires_at
_(dup.token_type).must_equal :access_token
_(dup.universe_domain).must_equal "googleapis.com"
end

it "allows overriding values" do
new_expiry = Time.now + 7200
dup = creds.duplicate token: "new-token", expiry: new_expiry, token_type: :jwt, universe_domain: example_universe_domain
new_expires_at = Time.now + 7200
dup = creds.duplicate token: "new-token", expires_at: new_expires_at, token_type: :jwt, universe_domain: example_universe_domain
_(dup.token).must_equal "new-token"
_(dup.expiry).must_equal new_expiry
_(dup.expires_at).must_equal new_expires_at
_(dup.token_type).must_equal :jwt
_(dup.universe_domain).must_equal example_universe_domain
end
end

describe "#expires_within?" do
let(:creds) { Google::Auth::BearerTokenCredentials.new token: token, expiry: expiry }
let(:creds) { Google::Auth::BearerTokenCredentials.new token: token, expires_at: expires_at }

it "returns true if after expiration" do
_(creds.expires_within?(4000)).must_equal true # Check after expiration
end

it "returns false if before expiration" do
_(creds.expires_within?(3000)).must_equal false # Check before expiration
end

it "returns true if after expiry" do
_(creds.expires_within?(4000)).must_equal true # Check after expiry
it "returns false if no expiration is set" do
creds_no_expires_at = Google::Auth::BearerTokenCredentials.new token: token
_(creds_no_expires_at.expires_within?(3600)).must_equal false
end
end

it "returns false if before expiry" do
_(creds.expires_within?(3000)).must_equal false # Check before expiry
describe "#fetch_access_token!" do
it "returns nil if not expired" do
creds = Google::Auth::BearerTokenCredentials.new token: token, expires_at: expires_at
_(creds.send(:fetch_access_token!)).must_be_nil
end

it "returns false if no expiry is set" do
creds_no_expiry = Google::Auth::BearerTokenCredentials.new token: token
_(creds_no_expiry.expires_within?(3600)).must_equal false
it "raises if token is expired" do
expired_time = Time.now - 3600
creds = Google::Auth::BearerTokenCredentials.new token: token, expires_at: expired_time
expect do
creds.send(:fetch_access_token!)
end.must_raise StandardError
end

it "returns nil if no expiry is set" do
creds = Google::Auth::BearerTokenCredentials.new token: token
_(creds.send(:fetch_access_token!)).must_be_nil
end
end
end

0 comments on commit b842ab9

Please sign in to comment.