From adc38da374f49eb06175fbb21740a70576e232e5 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Wed, 21 Oct 2026 11:00:34 +0530 Subject: [PATCH 1/8] fix(oauth): allow http scheme for localhost and return nil on invalid host --- .../oauth_access_token_provider.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 fbb3b60..bbac398 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 @@ -12,6 +12,8 @@ module MicrosoftKiotaAuthenticationOAuth # Access Token Provider class implementation class OAuthAccessTokenProvider + LOCALHOST_STRINGS = ['localhost', '::1', '[::1]', '127.0.0.1'].freeze + # This is the initializer for OAuthAccessTokenProvider. # :params # token_request_context: a instance of one of our token request context or a custom implementation @@ -48,11 +50,11 @@ def initialize(token_request_context, allowed_hosts = [], scopes = []) # additional_params: hash of symbols to string values, ie { response_mode: 'fragment', prompt: 'login' } # default is empty hash def get_authorization_token(uri, _additional_properties = {}) - nil if !uri || !@host_validator.url_host_valid?(uri) + return nil if !uri || !@host_validator.url_host_valid?(uri) parsed_url = URI(uri) - raise StandardError, 'Only https is supported' if parsed_url.scheme != 'https' + raise StandardError, 'Only https is supported' if parsed_url.scheme != 'https' && !localhost?(parsed_url) @scopes << "#{parsed_url.scheme}://#{parsed_url.host}/.default" if @scopes.empty? @token_request_context.initialize_scopes(@scopes) @@ -83,5 +85,14 @@ def get_authorization_token(uri, _additional_properties = {}) protected attr_writer :host_validator, :token_credential, :scopes, :cached_token + + private + + def localhost?(parsed_url) + host = parsed_url.hostname || parsed_url.host + return false if host.nil? + + LOCALHOST_STRINGS.include?(host.downcase) + end end end From 47cc58f19bff2f99d69d217e328a76fd636e12e9 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Wed, 21 Oct 2026 11:00:41 +0530 Subject: [PATCH 2/8] test(oauth): add coverage for localhost http and host validation --- ...crosoft_kiota_authentication_oauth_spec.rb | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb index 9499491..bdd6a62 100644 --- a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb +++ b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb @@ -90,4 +90,39 @@ expect(MicrosoftKiotaAuthenticationOAuth::AuthorizationCodeContext.new('tenant_id', 'client_id', 'client_secret', 'redirect_uri').is_a?(MicrosoftKiotaAuthenticationOAuth::OAuthContext)).to eq(true) end + + it 'returns nil when uri is nil or invalid for allowed hosts' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) + + expect(provider.get_authorization_token(nil)).to be_nil + expect(provider.get_authorization_token('https://untrusted.com/v1.0')).to be_nil + end + + it 'allows http on localhost and loopback addresses' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) + + ['http://localhost:8080/v1.0', 'http://127.0.0.1:8080/v1.0', 'http://[::1]:8080/v1.0'].each do |url| + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) + fiber = provider.get_authorization_token(url) + expect(fiber).to be_a(Fiber) + expect(fiber.resume).to eq('test_token') + end + end + + it 'raises error for non-https on non-localhost hosts' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) + + expect do + provider.get_authorization_token('http://graph.microsoft.com/v1.0') + end.to raise_error(StandardError, 'Only https is supported') + end end From 3ce36fd75f75b074f70d87fdf54d485836362245 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Wed, 21 Oct 2026 20:02:35 +0530 Subject: [PATCH 3/8] fix(oauth): separate host validation, strictly enforce http scheme on localhost, and preserve fiber contract --- .../authentication/allowed_hosts_validator.rb | 10 +-- ...se_bearer_token_authentication_provider.rb | 3 +- .../spec/microsoft_kiota_abstractions_spec.rb | 10 +++ .../spec/request_information_uri_spec.rb | 9 +++ .../oauth_access_token_provider.rb | 6 +- ...crosoft_kiota_authentication_oauth_spec.rb | 72 ++++++++++++++++--- 6 files changed, 95 insertions(+), 15 deletions(-) diff --git a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb index 4463bac..b22ce0d 100644 --- a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb +++ b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb @@ -24,18 +24,20 @@ def allowed_hosts=(val) # checks whether the provided host is valid def url_host_valid?(url) + return false if url.nil? return true if @allowed_hosts.empty? parsed_url = URI(url) return false if parsed_url.host.nil? - return false unless parsed_url.is_a?(URI::HTTPS) - hostname = parsed_url.host.downcase - return true if @allowed_hosts.key? hostname + return true if @allowed_hosts.key?(hostname) + + stripped_host = hostname.delete_prefix('[').delete_suffix(']') + return true if @allowed_hosts.key?(stripped_host) @allowed_hosts.any? { |allowed_host, _| allowed_host.start_with?('.') && hostname.end_with?(allowed_host) } - rescue URI::InvalidURIError + rescue URI::InvalidURIError, ArgumentError false end diff --git a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/base_bearer_token_authentication_provider.rb b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/base_bearer_token_authentication_provider.rb index 6d476d0..7bbe83c 100644 --- a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/base_bearer_token_authentication_provider.rb +++ b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/base_bearer_token_authentication_provider.rb @@ -24,7 +24,8 @@ def authenticate_request(request, additional_properties = {}) end else Fiber.new do - token = @access_token_provider.get_authorization_token(request.uri, additional_properties).resume + auth_fiber = @access_token_provider.get_authorization_token(request.uri, additional_properties) + token = auth_fiber.resume if auth_fiber request.headers.add(AUTHORIZATION_HEADER_KEY, "Bearer #{token}") unless token.nil? || token.empty? end end diff --git a/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb b/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb index 6218035..70cf163 100644 --- a/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb +++ b/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb @@ -47,6 +47,16 @@ expect(request_obj.headers.get(MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY).at(0)).to eq "Bearer #{AccessTokenProviderMock::DUMMY_TOKEN}" end + it 'safely handles nil token or nil fiber from access token provider' do + nil_provider = double('AccessTokenProvider', get_authorization_token: nil) + token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(nil_provider) + request_obj = MicrosoftKiotaAbstractions::RequestInformation.new + request_obj.uri = 'https://example.com' + header_key = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY + expect { token_provider.authenticate_request(request_obj).resume }.not_to raise_error + expect(request_obj.headers.get_all.key?(header_key)).to be(false) + end + it 'returns the raw URI when set via setter' do request_obj = MicrosoftKiotaAbstractions::RequestInformation.new request_obj.path_parameters['term'] = 'search' diff --git a/components/abstractions/spec/request_information_uri_spec.rb b/components/abstractions/spec/request_information_uri_spec.rb index 994c362..9fff23b 100644 --- a/components/abstractions/spec/request_information_uri_spec.rb +++ b/components/abstractions/spec/request_information_uri_spec.rb @@ -64,4 +64,13 @@ def uri_for(template, path_parameters = {}, query_parameters = {}) expect { validator.allowed_hosts = ['https://bad.com'] }.to raise_error(ArgumentError) expect(validator.url_host_valid?('https://anything.com')).to be(false) end + + it 'validates host regardless of scheme' do + validator = described_class.new(['example.com', 'localhost']) + expect(validator.url_host_valid?('http://example.com')).to be(true) + expect(validator.url_host_valid?('http://localhost:8080')).to be(true) + expect(validator.url_host_valid?('https://example.com')).to be(true) + expect(validator.url_host_valid?('https://other.com')).to be(false) + expect(validator.url_host_valid?('http://other.com')).to be(false) + end end 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 bbac398..c9ed8c1 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 @@ -50,11 +50,13 @@ def initialize(token_request_context, allowed_hosts = [], scopes = []) # additional_params: hash of symbols to string values, ie { response_mode: 'fragment', prompt: 'login' } # default is empty hash def get_authorization_token(uri, _additional_properties = {}) - return nil if !uri || !@host_validator.url_host_valid?(uri) + return Fiber.new { nil } if !uri || uri.to_s.empty? || !@host_validator.url_host_valid?(uri) parsed_url = URI(uri) - raise StandardError, 'Only https is supported' if parsed_url.scheme != 'https' && !localhost?(parsed_url) + is_https = parsed_url.scheme == 'https' + is_http_localhost = parsed_url.scheme == 'http' && localhost?(parsed_url) + raise StandardError, 'Only https is supported' unless is_https || is_http_localhost @scopes << "#{parsed_url.scheme}://#{parsed_url.host}/.default" if @scopes.empty? @token_request_context.initialize_scopes(@scopes) diff --git a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb index bdd6a62..aa5138e 100644 --- a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb +++ b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb @@ -91,27 +91,53 @@ 'redirect_uri').is_a?(MicrosoftKiotaAuthenticationOAuth::OAuthContext)).to eq(true) end - it 'returns nil when uri is nil or invalid for allowed hosts' do + it 'returns a fiber resolving to nil when uri is nil or invalid for allowed hosts' do context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( 'tenant_id', 'client_id', 'client_secret', {} ) provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) - expect(provider.get_authorization_token(nil)).to be_nil - expect(provider.get_authorization_token('https://untrusted.com/v1.0')).to be_nil + fiber_nil = provider.get_authorization_token(nil) + expect(fiber_nil).to be_a(Fiber) + expect(fiber_nil.resume).to be_nil + + fiber_untrusted = provider.get_authorization_token('https://untrusted.com/v1.0') + expect(fiber_untrusted).to be_a(Fiber) + expect(fiber_untrusted.resume).to be_nil end - it 'allows http on localhost and loopback addresses' do + it 'allows http on localhost and loopback addresses with empty or explicit allowed hosts' do context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( 'tenant_id', 'client_id', 'client_secret', {} ) allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) ['http://localhost:8080/v1.0', 'http://127.0.0.1:8080/v1.0', 'http://[::1]:8080/v1.0'].each do |url| - provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) - fiber = provider.get_authorization_token(url) - expect(fiber).to be_a(Fiber) - expect(fiber.resume).to eq('test_token') + # Test with default/empty allowed hosts + provider_empty = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) + fiber_empty = provider_empty.get_authorization_token(url) + expect(fiber_empty).to be_a(Fiber) + expect(fiber_empty.resume).to eq('test_token') + + # Test with explicitly allowed host + host = URI(url).hostname || URI(url).host + provider_explicit = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, [host]) + fiber_explicit = provider_explicit.get_authorization_token(url) + expect(fiber_explicit).to be_a(Fiber) + expect(fiber_explicit.resume).to eq('test_token') + end + end + + it 'raises error for unsupported schemes even on localhost' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) + + ['ftp://localhost:8080/v1.0', 'ws://localhost:8080/v1.0', '//localhost:8080/v1.0'].each do |url| + expect do + provider.get_authorization_token(url) + end.to raise_error(StandardError, 'Only https is supported') end end @@ -125,4 +151,34 @@ provider.get_authorization_token('http://graph.microsoft.com/v1.0') end.to raise_error(StandardError, 'Only https is supported') end + + it 'leaves request unauthenticated when host is not allowed via OAuthAuthenticationProvider' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + auth_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( + context, ['graph.microsoft.com'], [] + ) + request = MicrosoftKiotaAbstractions::RequestInformation.new + request.uri = 'https://untrusted.com/v1.0' + + expect { auth_provider.authenticate_request(request).resume }.not_to raise_error + expect(request.headers.get_all.key?('Authorization')).to be(false) + end + + it 'authenticates http localhost request via OAuthAuthenticationProvider' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) + + auth_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( + context, ['localhost'], [] + ) + request = MicrosoftKiotaAbstractions::RequestInformation.new + request.uri = 'http://localhost:8080/v1.0' + + auth_provider.authenticate_request(request).resume + expect(request.headers.get('Authorization').first).to eq('Bearer test_token') + end end From ffb6526d9eb70151956530531b9ad27c31466bf8 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Wed, 21 Oct 2026 21:59:17 +0530 Subject: [PATCH 4/8] test(specs): split authentication provider specs to comply with RuboCop BlockLength --- ...arer_token_authentication_provider_spec.rb | 39 ++++++++ .../spec/microsoft_kiota_abstractions_spec.rb | 36 ------- ...crosoft_kiota_authentication_oauth_spec.rb | 90 ----------------- .../spec/oauth_access_token_provider_spec.rb | 96 +++++++++++++++++++ 4 files changed, 135 insertions(+), 126 deletions(-) create mode 100644 components/abstractions/spec/base_bearer_token_authentication_provider_spec.rb create mode 100644 components/authentication/oauth/spec/oauth_access_token_provider_spec.rb diff --git a/components/abstractions/spec/base_bearer_token_authentication_provider_spec.rb b/components/abstractions/spec/base_bearer_token_authentication_provider_spec.rb new file mode 100644 index 0000000..a7fee58 --- /dev/null +++ b/components/abstractions/spec/base_bearer_token_authentication_provider_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'uri' +require 'microsoft_kiota_abstractions' +require_relative 'access_token_provider_mock' + +RSpec.describe MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider do + it 'creates a token authentication provider with existing authorization header' do + access_token_provider = AccessTokenProviderMock.new + token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(access_token_provider) + expect(token_provider).not_to be nil + request_obj = MicrosoftKiotaAbstractions::RequestInformation.new + request_obj.headers.add( + MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY, 'SAMPLE' + ) + token_provider.authenticate_request(request_obj).resume + expect(request_obj.headers.get(MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY).at(0)).to eq 'SAMPLE' + end + + it 'creates a token authentication provider with mock access token' do + access_token_provider = AccessTokenProviderMock.new + token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(access_token_provider) + expect(token_provider).not_to be nil + request_obj = MicrosoftKiotaAbstractions::RequestInformation.new + request_obj.uri = 'http://sample.com' + token_provider.authenticate_request(request_obj).resume + expect(request_obj.headers.get(MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY).at(0)).to eq "Bearer #{AccessTokenProviderMock::DUMMY_TOKEN}" + end + + it 'safely handles nil token or nil fiber from access token provider' do + nil_provider = double('AccessTokenProvider', get_authorization_token: nil) + token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(nil_provider) + request_obj = MicrosoftKiotaAbstractions::RequestInformation.new + request_obj.uri = 'https://example.com' + header_key = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY + expect { token_provider.authenticate_request(request_obj).resume }.not_to raise_error + expect(request_obj.headers.get_all.key?(header_key)).to be(false) + end +end diff --git a/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb b/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb index 70cf163..3002227 100644 --- a/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb +++ b/components/abstractions/spec/microsoft_kiota_abstractions_spec.rb @@ -21,42 +21,6 @@ token_provider.authenticate_request(nil).resume end - it 'creates a token authentication provider with existing authorization header' do - access_token_provider = AccessTokenProviderMock.new - token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(access_token_provider) - expect(token_provider).not_to be nil - request_obj = MicrosoftKiotaAbstractions::RequestInformation.new - request_obj.headers.add( - MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY, 'SAMPLE' - ) - # Authentication does nothing besides returning an empty Fiber - token_provider.authenticate_request(request_obj).resume - # The request header should not have changed - expect(request_obj.headers.get(MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY).at(0)).to eq 'SAMPLE' - end - - it 'creates a token authentication provider with mock access token' do - access_token_provider = AccessTokenProviderMock.new - token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(access_token_provider) - expect(token_provider).not_to be nil - request_obj = MicrosoftKiotaAbstractions::RequestInformation.new - # We have to set a URI, otherwise the code will fail ("undefined method 'chars' for nil") - request_obj.uri = 'http://sample.com' - token_provider.authenticate_request(request_obj).resume - # The request header should now contain the "DUMMY_TOKEN" value: - expect(request_obj.headers.get(MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY).at(0)).to eq "Bearer #{AccessTokenProviderMock::DUMMY_TOKEN}" - end - - it 'safely handles nil token or nil fiber from access token provider' do - nil_provider = double('AccessTokenProvider', get_authorization_token: nil) - token_provider = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider.new(nil_provider) - request_obj = MicrosoftKiotaAbstractions::RequestInformation.new - request_obj.uri = 'https://example.com' - header_key = MicrosoftKiotaAbstractions::BaseBearerTokenAuthenticationProvider::AUTHORIZATION_HEADER_KEY - expect { token_provider.authenticate_request(request_obj).resume }.not_to raise_error - expect(request_obj.headers.get_all.key?(header_key)).to be(false) - end - it 'returns the raw URI when set via setter' do request_obj = MicrosoftKiotaAbstractions::RequestInformation.new request_obj.path_parameters['term'] = 'search' diff --git a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb index aa5138e..301668f 100644 --- a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb +++ b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb @@ -91,94 +91,4 @@ 'redirect_uri').is_a?(MicrosoftKiotaAuthenticationOAuth::OAuthContext)).to eq(true) end - it 'returns a fiber resolving to nil when uri is nil or invalid for allowed hosts' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) - - fiber_nil = provider.get_authorization_token(nil) - expect(fiber_nil).to be_a(Fiber) - expect(fiber_nil.resume).to be_nil - - fiber_untrusted = provider.get_authorization_token('https://untrusted.com/v1.0') - expect(fiber_untrusted).to be_a(Fiber) - expect(fiber_untrusted.resume).to be_nil - end - - it 'allows http on localhost and loopback addresses with empty or explicit allowed hosts' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) - - ['http://localhost:8080/v1.0', 'http://127.0.0.1:8080/v1.0', 'http://[::1]:8080/v1.0'].each do |url| - # Test with default/empty allowed hosts - provider_empty = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) - fiber_empty = provider_empty.get_authorization_token(url) - expect(fiber_empty).to be_a(Fiber) - expect(fiber_empty.resume).to eq('test_token') - - # Test with explicitly allowed host - host = URI(url).hostname || URI(url).host - provider_explicit = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, [host]) - fiber_explicit = provider_explicit.get_authorization_token(url) - expect(fiber_explicit).to be_a(Fiber) - expect(fiber_explicit.resume).to eq('test_token') - end - end - - it 'raises error for unsupported schemes even on localhost' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) - - ['ftp://localhost:8080/v1.0', 'ws://localhost:8080/v1.0', '//localhost:8080/v1.0'].each do |url| - expect do - provider.get_authorization_token(url) - end.to raise_error(StandardError, 'Only https is supported') - end - end - - it 'raises error for non-https on non-localhost hosts' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) - - expect do - provider.get_authorization_token('http://graph.microsoft.com/v1.0') - end.to raise_error(StandardError, 'Only https is supported') - end - - it 'leaves request unauthenticated when host is not allowed via OAuthAuthenticationProvider' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - auth_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( - context, ['graph.microsoft.com'], [] - ) - request = MicrosoftKiotaAbstractions::RequestInformation.new - request.uri = 'https://untrusted.com/v1.0' - - expect { auth_provider.authenticate_request(request).resume }.not_to raise_error - expect(request.headers.get_all.key?('Authorization')).to be(false) - end - - it 'authenticates http localhost request via OAuthAuthenticationProvider' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) - - auth_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( - context, ['localhost'], [] - ) - request = MicrosoftKiotaAbstractions::RequestInformation.new - request.uri = 'http://localhost:8080/v1.0' - - auth_provider.authenticate_request(request).resume - expect(request.headers.get('Authorization').first).to eq('Bearer test_token') - 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 new file mode 100644 index 0000000..e64a3b7 --- /dev/null +++ b/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'uri' +require 'microsoft_kiota_abstractions' +require_relative 'spec_helper' + +RSpec.describe MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider do + it 'returns a fiber resolving to nil when uri is nil or invalid for allowed hosts' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) + + fiber_nil = provider.get_authorization_token(nil) + expect(fiber_nil).to be_a(Fiber) + expect(fiber_nil.resume).to be_nil + + fiber_untrusted = provider.get_authorization_token('https://untrusted.com/v1.0') + expect(fiber_untrusted).to be_a(Fiber) + expect(fiber_untrusted.resume).to be_nil + end + + it 'allows http on localhost and loopback addresses with empty or explicit allowed hosts' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) + + ['http://localhost:8080/v1.0', 'http://127.0.0.1:8080/v1.0', 'http://[::1]:8080/v1.0'].each do |url| + provider_empty = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) + fiber_empty = provider_empty.get_authorization_token(url) + expect(fiber_empty).to be_a(Fiber) + expect(fiber_empty.resume).to eq('test_token') + + host = URI(url).hostname || URI(url).host + provider_explicit = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, [host]) + fiber_explicit = provider_explicit.get_authorization_token(url) + expect(fiber_explicit).to be_a(Fiber) + expect(fiber_explicit.resume).to eq('test_token') + end + end + + it 'raises error for unsupported schemes even on localhost' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) + + ['ftp://localhost:8080/v1.0', 'ws://localhost:8080/v1.0', '//localhost:8080/v1.0'].each do |url| + expect do + provider.get_authorization_token(url) + end.to raise_error(StandardError, 'Only https is supported') + end + end + + it 'raises error for non-https on non-localhost hosts' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) + + expect do + provider.get_authorization_token('http://graph.microsoft.com/v1.0') + end.to raise_error(StandardError, 'Only https is supported') + end + + it 'leaves request unauthenticated when host is not allowed via OAuthAuthenticationProvider' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + auth_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( + context, ['graph.microsoft.com'], [] + ) + request = MicrosoftKiotaAbstractions::RequestInformation.new + request.uri = 'https://untrusted.com/v1.0' + + expect { auth_provider.authenticate_request(request).resume }.not_to raise_error + expect(request.headers.get_all.key?('Authorization')).to be(false) + end + + it 'authenticates http localhost request via OAuthAuthenticationProvider' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) + + auth_provider = MicrosoftKiotaAuthenticationOAuth::OAuthAuthenticationProvider.new( + context, ['localhost'], [] + ) + request = MicrosoftKiotaAbstractions::RequestInformation.new + request.uri = 'http://localhost:8080/v1.0' + + auth_provider.authenticate_request(request).resume + expect(request.headers.get('Authorization').first).to eq('Bearer test_token') + end +end From 2732a7a4fb65fe8abd4ca8193dc9ed3905702149 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Wed, 21 Oct 2026 22:43:56 +0530 Subject: [PATCH 5/8] style(oauth): remove extra empty line at block body end to satisfy RuboCop --- .../oauth/spec/microsoft_kiota_authentication_oauth_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb index 301668f..9499491 100644 --- a/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb +++ b/components/authentication/oauth/spec/microsoft_kiota_authentication_oauth_spec.rb @@ -90,5 +90,4 @@ expect(MicrosoftKiotaAuthenticationOAuth::AuthorizationCodeContext.new('tenant_id', 'client_id', 'client_secret', 'redirect_uri').is_a?(MicrosoftKiotaAuthenticationOAuth::OAuthContext)).to eq(true) end - end From d51b090deccea46b233f79e031385a550872b388 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Wed, 21 Oct 2026 23:08:33 +0530 Subject: [PATCH 6/8] fix(oauth): preserve AllowedHostsValidator https enforcement and skip host validation on localhost in provider --- .../authentication/allowed_hosts_validator.rb | 10 +++---- .../spec/request_information_uri_spec.rb | 6 ++--- .../oauth_access_token_provider.rb | 15 ++++++++--- .../spec/oauth_access_token_provider_spec.rb | 26 +++++++------------ 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb index b22ce0d..4463bac 100644 --- a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb +++ b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb @@ -24,20 +24,18 @@ def allowed_hosts=(val) # checks whether the provided host is valid def url_host_valid?(url) - return false if url.nil? return true if @allowed_hosts.empty? parsed_url = URI(url) return false if parsed_url.host.nil? - hostname = parsed_url.host.downcase - return true if @allowed_hosts.key?(hostname) + return false unless parsed_url.is_a?(URI::HTTPS) - stripped_host = hostname.delete_prefix('[').delete_suffix(']') - return true if @allowed_hosts.key?(stripped_host) + hostname = parsed_url.host.downcase + return true if @allowed_hosts.key? hostname @allowed_hosts.any? { |allowed_host, _| allowed_host.start_with?('.') && hostname.end_with?(allowed_host) } - rescue URI::InvalidURIError, ArgumentError + rescue URI::InvalidURIError false end diff --git a/components/abstractions/spec/request_information_uri_spec.rb b/components/abstractions/spec/request_information_uri_spec.rb index 9fff23b..ec58e8b 100644 --- a/components/abstractions/spec/request_information_uri_spec.rb +++ b/components/abstractions/spec/request_information_uri_spec.rb @@ -65,10 +65,10 @@ def uri_for(template, path_parameters = {}, query_parameters = {}) expect(validator.url_host_valid?('https://anything.com')).to be(false) end - it 'validates host regardless of scheme' do + it 'validates host with https scheme and rejects non-https' do validator = described_class.new(['example.com', 'localhost']) - expect(validator.url_host_valid?('http://example.com')).to be(true) - expect(validator.url_host_valid?('http://localhost:8080')).to be(true) + expect(validator.url_host_valid?('http://example.com')).to be(false) + expect(validator.url_host_valid?('http://localhost:8080')).to be(false) expect(validator.url_host_valid?('https://example.com')).to be(true) expect(validator.url_host_valid?('https://other.com')).to be(false) expect(validator.url_host_valid?('http://other.com')).to be(false) 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 c9ed8c1..d032644 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 @@ -50,12 +50,21 @@ def initialize(token_request_context, allowed_hosts = [], scopes = []) # additional_params: hash of symbols to string values, ie { response_mode: 'fragment', prompt: 'login' } # default is empty hash def get_authorization_token(uri, _additional_properties = {}) - return Fiber.new { nil } if !uri || uri.to_s.empty? || !@host_validator.url_host_valid?(uri) + return Fiber.new { nil } if !uri || uri.to_s.empty? - parsed_url = URI(uri) + begin + parsed_url = URI(uri) + rescue URI::InvalidURIError + return Fiber.new { nil } + end + + return Fiber.new { nil } if parsed_url.nil? || parsed_url.host.nil? + + is_localhost = localhost?(parsed_url) + return Fiber.new { nil } if !is_localhost && !@host_validator.url_host_valid?(uri) is_https = parsed_url.scheme == 'https' - is_http_localhost = parsed_url.scheme == 'http' && localhost?(parsed_url) + is_http_localhost = parsed_url.scheme == 'http' && is_localhost raise StandardError, 'Only https is supported' unless is_https || is_http_localhost @scopes << "#{parsed_url.scheme}://#{parsed_url.host}/.default" if @scopes.empty? 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 e64a3b7..a65e5e0 100644 --- a/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb +++ b/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb @@ -18,6 +18,10 @@ fiber_untrusted = provider.get_authorization_token('https://untrusted.com/v1.0') expect(fiber_untrusted).to be_a(Fiber) expect(fiber_untrusted.resume).to be_nil + + fiber_http_external = provider.get_authorization_token('http://graph.microsoft.com/v1.0') + expect(fiber_http_external).to be_a(Fiber) + expect(fiber_http_external.resume).to be_nil end it 'allows http on localhost and loopback addresses with empty or explicit allowed hosts' do @@ -32,11 +36,12 @@ expect(fiber_empty).to be_a(Fiber) expect(fiber_empty.resume).to eq('test_token') - host = URI(url).hostname || URI(url).host - provider_explicit = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, [host]) - fiber_explicit = provider_explicit.get_authorization_token(url) - expect(fiber_explicit).to be_a(Fiber) - expect(fiber_explicit.resume).to eq('test_token') + provider_external_only = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new( + context, ['graph.microsoft.com'] + ) + fiber_external = provider_external_only.get_authorization_token(url) + expect(fiber_external).to be_a(Fiber) + expect(fiber_external.resume).to eq('test_token') end end @@ -53,17 +58,6 @@ end end - it 'raises error for non-https on non-localhost hosts' do - context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( - 'tenant_id', 'client_id', 'client_secret', {} - ) - provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) - - expect do - provider.get_authorization_token('http://graph.microsoft.com/v1.0') - end.to raise_error(StandardError, 'Only https is supported') - end - it 'leaves request unauthenticated when host is not allowed via OAuthAuthenticationProvider' do context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( 'tenant_id', 'client_id', 'client_secret', {} From 6c131f5ae23aa9ff8932525d328d9f951d5a610f Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Thu, 22 Oct 2026 00:49:10 +0530 Subject: [PATCH 7/8] fix(oauth): enforce host allowlist on loopbacks and separate host validation from scheme enforcement --- .../authentication/allowed_hosts_validator.rb | 26 ++++++++++-- .../spec/request_information_uri_spec.rb | 4 +- .../oauth_access_token_provider.rb | 8 ++-- .../spec/oauth_access_token_provider_spec.rb | 40 ++++++++++++++----- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb index 4463bac..94cb0f5 100644 --- a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb +++ b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb @@ -6,6 +6,8 @@ module MicrosoftKiotaAbstractions # Maintains a list of valid hosts and allows authentication providers to check whether # a host is valid before authenticating a request class AllowedHostsValidator + LOCALHOST_STRINGS = ['localhost', '::1', '[::1]', '127.0.0.1'].freeze + # creates a new AllocatedHostsValidator with provided values def initialize(allowed_hosts) self.allowed_hosts = allowed_hosts @@ -24,15 +26,22 @@ def allowed_hosts=(val) # checks whether the provided host is valid def url_host_valid?(url) - return true if @allowed_hosts.empty? + return false if url.nil? parsed_url = URI(url) return false if parsed_url.host.nil? - return false unless parsed_url.is_a?(URI::HTTPS) + is_https = parsed_url.is_a?(URI::HTTPS) + is_http_localhost = parsed_url.is_a?(URI::HTTP) && localhost?(parsed_url) + return false unless is_https || is_http_localhost + + return true if @allowed_hosts.empty? + + hostname = (parsed_url.hostname || parsed_url.host).downcase + return true if @allowed_hosts.key?(hostname) - hostname = parsed_url.host.downcase - return true if @allowed_hosts.key? hostname + stripped_host = hostname.delete_prefix('[').delete_suffix(']') + return true if @allowed_hosts.key?(stripped_host) @allowed_hosts.any? { |allowed_host, _| allowed_host.start_with?('.') && hostname.end_with?(allowed_host) } rescue URI::InvalidURIError @@ -41,5 +50,14 @@ def url_host_valid?(url) # gets the list of valid hosts attr_reader :allowed_hosts + + private + + def localhost?(parsed_url) + host = parsed_url.hostname || parsed_url.host + return false if host.nil? + + LOCALHOST_STRINGS.include?(host.downcase) + end end end diff --git a/components/abstractions/spec/request_information_uri_spec.rb b/components/abstractions/spec/request_information_uri_spec.rb index ec58e8b..fbd5027 100644 --- a/components/abstractions/spec/request_information_uri_spec.rb +++ b/components/abstractions/spec/request_information_uri_spec.rb @@ -65,10 +65,10 @@ def uri_for(template, path_parameters = {}, query_parameters = {}) expect(validator.url_host_valid?('https://anything.com')).to be(false) end - it 'validates host with https scheme and rejects non-https' do + it 'validates host with https scheme and allows http for localhost' do validator = described_class.new(['example.com', 'localhost']) expect(validator.url_host_valid?('http://example.com')).to be(false) - expect(validator.url_host_valid?('http://localhost:8080')).to be(false) + expect(validator.url_host_valid?('http://localhost:8080')).to be(true) expect(validator.url_host_valid?('https://example.com')).to be(true) expect(validator.url_host_valid?('https://other.com')).to be(false) expect(validator.url_host_valid?('http://other.com')).to be(false) 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 d032644..0d94b94 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 @@ -54,15 +54,17 @@ def get_authorization_token(uri, _additional_properties = {}) begin parsed_url = URI(uri) - rescue URI::InvalidURIError + rescue URI::InvalidURIError, ArgumentError return Fiber.new { nil } end return Fiber.new { nil } if parsed_url.nil? || parsed_url.host.nil? - is_localhost = localhost?(parsed_url) - return Fiber.new { nil } if !is_localhost && !@host_validator.url_host_valid?(uri) + https_version = parsed_url.dup + https_version.scheme = 'https' + return Fiber.new { nil } unless @host_validator.url_host_valid?(https_version.to_s) + is_localhost = localhost?(parsed_url) is_https = parsed_url.scheme == 'https' is_http_localhost = parsed_url.scheme == 'http' && is_localhost raise StandardError, 'Only https is supported' unless is_https || is_http_localhost 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 a65e5e0..b70f196 100644 --- a/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb +++ b/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb @@ -5,7 +5,7 @@ require_relative 'spec_helper' RSpec.describe MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider do - it 'returns a fiber resolving to nil when uri is nil or invalid for allowed hosts' do + it 'returns a fiber resolving to nil when uri is nil or not in allowed hosts' do context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( 'tenant_id', 'client_id', 'client_secret', {} ) @@ -19,29 +19,47 @@ expect(fiber_untrusted).to be_a(Fiber) expect(fiber_untrusted.resume).to be_nil - fiber_http_external = provider.get_authorization_token('http://graph.microsoft.com/v1.0') - expect(fiber_http_external).to be_a(Fiber) - expect(fiber_http_external.resume).to be_nil + fiber_http_untrusted = provider.get_authorization_token('http://untrusted.com/v1.0') + expect(fiber_http_untrusted).to be_a(Fiber) + expect(fiber_http_untrusted.resume).to be_nil + + fiber_loopback_unapproved = provider.get_authorization_token('http://localhost:8080/v1.0') + expect(fiber_loopback_unapproved).to be_a(Fiber) + expect(fiber_loopback_unapproved.resume).to be_nil end - it 'allows http on localhost and loopback addresses with empty or explicit allowed hosts' do + it 'raises error for allowlisted external HTTP target' do + context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( + 'tenant_id', 'client_id', 'client_secret', {} + ) + provider = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, ['graph.microsoft.com']) + + expect do + provider.get_authorization_token('http://graph.microsoft.com/v1.0') + end.to raise_error(StandardError, 'Only https is supported') + end + + it 'allows http on localhost and loopback addresses when allowed hosts is empty or includes loopback' do context = MicrosoftKiotaAuthenticationOAuth::ClientCredentialContext.new( 'tenant_id', 'client_id', 'client_secret', {} ) allow(context).to receive(:get_token).and_return(double('token', to_hash: {}, token: 'test_token')) - ['http://localhost:8080/v1.0', 'http://127.0.0.1:8080/v1.0', 'http://[::1]:8080/v1.0'].each do |url| + ['localhost', '127.0.0.1', '[::1]'].each do |host| + url = "http://#{host}:8080/v1.0" + provider_empty = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new(context, []) fiber_empty = provider_empty.get_authorization_token(url) expect(fiber_empty).to be_a(Fiber) expect(fiber_empty.resume).to eq('test_token') - provider_external_only = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new( - context, ['graph.microsoft.com'] + stripped_host = host.delete_prefix('[').delete_suffix(']') + provider_allowlisted = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new( + context, [stripped_host] ) - fiber_external = provider_external_only.get_authorization_token(url) - expect(fiber_external).to be_a(Fiber) - expect(fiber_external.resume).to eq('test_token') + fiber_allowlisted = provider_allowlisted.get_authorization_token(url) + expect(fiber_allowlisted).to be_a(Fiber) + expect(fiber_allowlisted.resume).to eq('test_token') end end From 8af8838efa2fa36af42af95bc330007160c0d9a8 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Fri, 23 Oct 2026 18:56:40 +0530 Subject: [PATCH 8/8] fix(abstractions): match parsed_url.host first to preserve bracketed IPv6 allowlist entries --- .../authentication/allowed_hosts_validator.rb | 18 +++++++++++++----- .../spec/request_information_uri_spec.rb | 14 ++++++++++++++ .../oauth_access_token_provider.rb | 9 +++++++-- .../spec/oauth_access_token_provider_spec.rb | 15 ++++++++------- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb index 94cb0f5..4c403a0 100644 --- a/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb +++ b/components/abstractions/lib/microsoft_kiota_abstractions/authentication/allowed_hosts_validator.rb @@ -37,12 +37,15 @@ def url_host_valid?(url) return true if @allowed_hosts.empty? - hostname = (parsed_url.hostname || parsed_url.host).downcase - return true if @allowed_hosts.key?(hostname) + host = parsed_url.host.downcase + return true if @allowed_hosts.key?(host) - stripped_host = hostname.delete_prefix('[').delete_suffix(']') + stripped_host = host.delete_prefix('[').delete_suffix(']') return true if @allowed_hosts.key?(stripped_host) + hostname = (parsed_url.hostname || host).downcase + return true if @allowed_hosts.key?(hostname) + @allowed_hosts.any? { |allowed_host, _| allowed_host.start_with?('.') && hostname.end_with?(allowed_host) } rescue URI::InvalidURIError false @@ -54,10 +57,15 @@ def url_host_valid?(url) private def localhost?(parsed_url) - host = parsed_url.hostname || parsed_url.host + host = parsed_url.host return false if host.nil? - LOCALHOST_STRINGS.include?(host.downcase) + return true if LOCALHOST_STRINGS.include?(host.downcase) + + hostname = parsed_url.hostname + return false if hostname.nil? + + LOCALHOST_STRINGS.include?(hostname.downcase) end end end diff --git a/components/abstractions/spec/request_information_uri_spec.rb b/components/abstractions/spec/request_information_uri_spec.rb index fbd5027..ecd49ef 100644 --- a/components/abstractions/spec/request_information_uri_spec.rb +++ b/components/abstractions/spec/request_information_uri_spec.rb @@ -73,4 +73,18 @@ def uri_for(template, path_parameters = {}, query_parameters = {}) expect(validator.url_host_valid?('https://other.com')).to be(false) expect(validator.url_host_valid?('http://other.com')).to be(false) end + + it 'validates IPv6 loopback hosts with both bracketed and unbracketed allowlist entries' do + bracketed_validator = described_class.new(['[::1]']) + expect(bracketed_validator.url_host_valid?('https://[::1]:8080/v1.0')).to be(true) + expect(bracketed_validator.url_host_valid?('http://[::1]:8080/v1.0')).to be(true) + expect(bracketed_validator.url_host_valid?('http://example.com')).to be(false) + expect(bracketed_validator.url_host_valid?('https://example.com')).to be(false) + + unbracketed_validator = described_class.new(['::1']) + expect(unbracketed_validator.url_host_valid?('https://[::1]:8080/v1.0')).to be(true) + expect(unbracketed_validator.url_host_valid?('http://[::1]:8080/v1.0')).to be(true) + expect(unbracketed_validator.url_host_valid?('http://example.com')).to be(false) + expect(unbracketed_validator.url_host_valid?('https://example.com')).to be(false) + end end 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 0d94b94..206d4f0 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 @@ -102,10 +102,15 @@ def get_authorization_token(uri, _additional_properties = {}) private def localhost?(parsed_url) - host = parsed_url.hostname || parsed_url.host + host = parsed_url.host return false if host.nil? - LOCALHOST_STRINGS.include?(host.downcase) + return true if LOCALHOST_STRINGS.include?(host.downcase) + + hostname = parsed_url.hostname + return false if hostname.nil? + + LOCALHOST_STRINGS.include?(hostname.downcase) end 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 b70f196..a0caf9b 100644 --- a/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb +++ b/components/authentication/oauth/spec/oauth_access_token_provider_spec.rb @@ -53,13 +53,14 @@ expect(fiber_empty).to be_a(Fiber) expect(fiber_empty.resume).to eq('test_token') - stripped_host = host.delete_prefix('[').delete_suffix(']') - provider_allowlisted = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new( - context, [stripped_host] - ) - fiber_allowlisted = provider_allowlisted.get_authorization_token(url) - expect(fiber_allowlisted).to be_a(Fiber) - expect(fiber_allowlisted.resume).to eq('test_token') + [host, host.delete_prefix('[').delete_suffix(']')].uniq.each do |allowed_entry| + provider_allowlisted = MicrosoftKiotaAuthenticationOAuth::OAuthAccessTokenProvider.new( + context, [allowed_entry] + ) + fiber_allowlisted = provider_allowlisted.get_authorization_token(url) + expect(fiber_allowlisted).to be_a(Fiber) + expect(fiber_allowlisted.resume).to eq('test_token') + end end end