diff --git a/components/authentication/oauth/lib/microsoft_kiota_authentication_oauth/oauth_access_token_provider.rb b/components/authentication/oauth/lib/microsoft_kiota_authentication_oauth/oauth_access_token_provider.rb index 0833973..c1496e9 100644 --- a/components/authentication/oauth/lib/microsoft_kiota_authentication_oauth/oauth_access_token_provider.rb +++ b/components/authentication/oauth/lib/microsoft_kiota_authentication_oauth/oauth_access_token_provider.rb @@ -27,7 +27,7 @@ def initialize(token_request_context, allowed_hosts = [], scopes = []) 'Parameter token_request_context must be an instance of one of our grant flow context classes.' end - @cached_token = nil + @cached_tokens = {} @host_validator = if allowed_hosts.nil? || allowed_hosts.empty? MicrosoftKiotaAbstractions::AllowedHostsValidator.new([]) @@ -38,7 +38,7 @@ def initialize(token_request_context, allowed_hosts = [], scopes = []) @scopes = if scopes.nil? [] else - scopes + scopes.dup end end @@ -54,27 +54,32 @@ def get_authorization_token(uri, _additional_properties = {}) raise StandardError, 'Only https is supported' if parsed_url.scheme != 'https' - @scopes << "#{parsed_url.scheme}://#{parsed_url.host}/.default" if @scopes.empty? - @token_request_context.initialize_scopes(@scopes) + effective_scopes = @scopes.empty? ? ["#{parsed_url.scheme}://#{parsed_url.host}/.default"] : @scopes.dup + cache_key = effective_scopes.freeze Fiber.new do - if @cached_token - token = OAuth2::AccessToken.from_hash(@token_request_context.oauth_provider, @cached_token) - token.token unless token.nil? || token.expired? - - if token.expired? + @token_request_context.initialize_scopes(effective_scopes) + token_value = nil + cached_token = @cached_tokens[cache_key] + unless cached_token.nil? + token = OAuth2::AccessToken.from_hash(@token_request_context.oauth_provider, cached_token) + if token && !token.expired? + token_value = token.token + elsif token token = token.refresh! - @cached_token = token.to_hash - token.token + @cached_tokens[cache_key] = token.to_hash + token_value = token.token end end - nil - token = @token_request_context.get_token - - unless token.nil? - @cached_token = token.to_hash - token.token + if token_value.nil? + token = @token_request_context.get_token + unless token.nil? + @cached_tokens[cache_key] = token.to_hash + token_value = token.token + end end + + token_value end end @@ -82,6 +87,6 @@ def get_authorization_token(uri, _additional_properties = {}) protected - attr_writer :host_validator, :token_credential, :scopes, :cached_token + attr_writer :host_validator, :token_credential, :scopes, :cached_tokens end end diff --git a/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb b/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb index 393b698..884971d 100644 --- a/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb +++ b/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb @@ -45,6 +45,54 @@ end end + context 'with request-derived default scopes' do + let(:allowed_hosts) { %w[one.example.com two.example.com] } + let(:provider) { described_class.new(context, allowed_hosts) } + let(:tokens) do + [ + instance_double(OAuth2::AccessToken, token: 'one_token', to_hash: {}), + instance_double(OAuth2::AccessToken, token: 'two_token', to_hash: {}) + ] + end + + it 'derives the default scope for each request without mutating configured scopes' do + allow(context).to receive(:get_token).and_return(*tokens) + + expect(provider.get_authorization_token('https://one.example.com').resume).to eq('one_token') + expect(provider.get_authorization_token('https://two.example.com').resume).to eq('two_token') + + expect(context).to have_received(:initialize_scopes).with(['https://one.example.com/.default']) + expect(context).to have_received(:initialize_scopes).with(['https://two.example.com/.default']) + expect(context).to have_received(:get_token).twice + expect(provider.scopes).to eq([]) + end + + it 'reuses a cached token only for its effective scopes' do + one_token = OAuth2::AccessToken.new(context.oauth_provider, 'one_token', expires_in: 3600) + two_token = OAuth2::AccessToken.new(context.oauth_provider, 'two_token', expires_in: 3600) + allow(context).to receive(:get_token).and_return(one_token, two_token) + + expect(provider.get_authorization_token('https://one.example.com').resume).to eq('one_token') + expect(provider.get_authorization_token('https://two.example.com').resume).to eq('two_token') + expect(provider.get_authorization_token('https://one.example.com').resume).to eq('one_token') + + expect(context).to have_received(:get_token).twice + end + + it 'does not mutate explicitly configured scopes' do + configured_scopes = ['https://resource.example.com/.default'] + explicit_provider = described_class.new(context, allowed_hosts, configured_scopes) + + expect(explicit_provider.scopes).to eq(configured_scopes) + expect(explicit_provider.scopes).not_to equal(configured_scopes) + explicit_provider.get_authorization_token('https://one.example.com').resume + + expect(explicit_provider.scopes).to eq(configured_scopes) + expect(configured_scopes).to eq(['https://resource.example.com/.default']) + expect(context).to have_received(:initialize_scopes).with(configured_scopes) + end + end + it 'leaves a request to a host outside the allowed hosts without an authorization header' do authentication_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( context, allowed_hosts, ['https://api.example.com/.default']