Skip to content

Commit e44bfb1

Browse files
authored
.Net: Google Gemini - Move API key from the URL to x-goog-api-key HTTP header (#12717)
### Motivation and Context - Resolves #12666 The implementation successfully addresses the security concern raised in GitHub issue #12666 by moving the Google API key from the URL query parameter to the secure x-goog-api-key HTTP header, preventing sensitive information from being logged or traced. - Modified ClientBase class to support API key in headers - Updated all Google AI clients (Chat, Streaming, Token Counter, Embeddings) to use header-based authentication - Removed API key from URLs to prevent exposure in logs and OTEL traces - Comprehensive Testing:
1 parent a1d70d2 commit e44bfb1

File tree

8 files changed

+144
-8
lines changed

8 files changed

+144
-8
lines changed

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,38 @@ public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync()
422422
Assert.Equal(expectedVersion, header);
423423
}
424424

425+
[Fact]
426+
public async Task ItCreatesPostRequestWithApiKeyInHeaderAsync()
427+
{
428+
// Arrange
429+
var client = this.CreateChatCompletionClient();
430+
var chatHistory = CreateSampleChatHistory();
431+
432+
// Act
433+
await client.GenerateChatMessageAsync(chatHistory);
434+
435+
// Assert
436+
Assert.NotNull(this._messageHandlerStub.RequestHeaders);
437+
var apiKeyHeader = this._messageHandlerStub.RequestHeaders.GetValues("x-goog-api-key").SingleOrDefault();
438+
Assert.NotNull(apiKeyHeader);
439+
Assert.Equal("fake-key", apiKeyHeader);
440+
}
441+
442+
[Fact]
443+
public async Task ItCreatesPostRequestWithoutApiKeyInUrlAsync()
444+
{
445+
// Arrange
446+
var client = this.CreateChatCompletionClient();
447+
var chatHistory = CreateSampleChatHistory();
448+
449+
// Act
450+
await client.GenerateChatMessageAsync(chatHistory);
451+
452+
// Assert
453+
Assert.NotNull(this._messageHandlerStub.RequestUri);
454+
Assert.DoesNotContain("key=", this._messageHandlerStub.RequestUri.ToString());
455+
}
456+
425457
[Fact]
426458
public async Task ItCreatesPostRequestWithResponseSchemaPropertyAsync()
427459
{

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,38 @@ public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync()
392392
Assert.Equal(expectedVersion, header);
393393
}
394394

395+
[Fact]
396+
public async Task ItCreatesPostRequestWithApiKeyInHeaderAsync()
397+
{
398+
// Arrange
399+
var client = this.CreateChatCompletionClient();
400+
var chatHistory = CreateSampleChatHistory();
401+
402+
// Act
403+
await client.StreamGenerateChatMessageAsync(chatHistory).ToListAsync();
404+
405+
// Assert
406+
Assert.NotNull(this._messageHandlerStub.RequestHeaders);
407+
var apiKeyHeader = this._messageHandlerStub.RequestHeaders.GetValues("x-goog-api-key").SingleOrDefault();
408+
Assert.NotNull(apiKeyHeader);
409+
Assert.Equal("fake-key", apiKeyHeader);
410+
}
411+
412+
[Fact]
413+
public async Task ItCreatesPostRequestWithoutApiKeyInUrlAsync()
414+
{
415+
// Arrange
416+
var client = this.CreateChatCompletionClient();
417+
var chatHistory = CreateSampleChatHistory();
418+
419+
// Act
420+
await client.StreamGenerateChatMessageAsync(chatHistory).ToListAsync();
421+
422+
// Assert
423+
Assert.NotNull(this._messageHandlerStub.RequestUri);
424+
Assert.DoesNotContain("key=", this._messageHandlerStub.RequestUri.ToString());
425+
}
426+
395427
private static ChatHistory CreateSampleChatHistory()
396428
{
397429
var chatHistory = new ChatHistory();

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiCountingTokensTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,36 @@ public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync()
116116
Assert.Equal(expectedVersion, header);
117117
}
118118

119+
[Fact]
120+
public async Task ItCreatesPostRequestWithApiKeyInHeaderAsync()
121+
{
122+
// Arrange
123+
var client = this.CreateTokenCounterClient();
124+
125+
// Act
126+
await client.CountTokensAsync("fake-text");
127+
128+
// Assert
129+
Assert.NotNull(this._messageHandlerStub.RequestHeaders);
130+
var apiKeyHeader = this._messageHandlerStub.RequestHeaders.GetValues("x-goog-api-key").SingleOrDefault();
131+
Assert.NotNull(apiKeyHeader);
132+
Assert.Equal("fake-key", apiKeyHeader);
133+
}
134+
135+
[Fact]
136+
public async Task ItCreatesPostRequestWithoutApiKeyInUrlAsync()
137+
{
138+
// Arrange
139+
var client = this.CreateTokenCounterClient();
140+
141+
// Act
142+
await client.CountTokensAsync("fake-text");
143+
144+
// Assert
145+
Assert.NotNull(this._messageHandlerStub.RequestUri);
146+
Assert.DoesNotContain("key=", this._messageHandlerStub.RequestUri.ToString());
147+
}
148+
119149
[Theory]
120150
[InlineData("https://malicious-site.com")]
121151
[InlineData("http://internal-network.local")]

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/GoogleAI/GoogleAIClientEmbeddingsGenerationTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,38 @@ public async Task ItCreatesPostRequestWithSemanticKernelVersionHeaderAsync()
142142
Assert.Equal(expectedVersion, header);
143143
}
144144

145+
[Fact]
146+
public async Task ItCreatesPostRequestWithApiKeyInHeaderAsync()
147+
{
148+
// Arrange
149+
var client = this.CreateEmbeddingsClient();
150+
IList<string> data = ["sample data"];
151+
152+
// Act
153+
await client.GenerateEmbeddingsAsync(data);
154+
155+
// Assert
156+
Assert.NotNull(this._messageHandlerStub.RequestHeaders);
157+
var apiKeyHeader = this._messageHandlerStub.RequestHeaders.GetValues("x-goog-api-key").SingleOrDefault();
158+
Assert.NotNull(apiKeyHeader);
159+
Assert.Equal("fake-key", apiKeyHeader);
160+
}
161+
162+
[Fact]
163+
public async Task ItCreatesPostRequestWithoutApiKeyInUrlAsync()
164+
{
165+
// Arrange
166+
var client = this.CreateEmbeddingsClient();
167+
IList<string> data = ["sample data"];
168+
169+
// Act
170+
await client.GenerateEmbeddingsAsync(data);
171+
172+
// Assert
173+
Assert.NotNull(this._messageHandlerStub.RequestUri);
174+
Assert.DoesNotContain("key=", this._messageHandlerStub.RequestUri.ToString());
175+
}
176+
145177
[Fact]
146178
public async Task ShouldIncludeDimensionsInAllRequestsAsync()
147179
{

dotnet/src/Connectors/Connectors.Google/Core/ClientBase.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Microsoft.SemanticKernel.Connectors.Google.Core;
1515
internal abstract class ClientBase
1616
{
1717
private readonly Func<ValueTask<string>>? _bearerTokenProvider;
18+
private readonly string? _apiKey;
1819

1920
protected ILogger Logger { get; }
2021

@@ -32,12 +33,14 @@ protected ClientBase(
3233

3334
protected ClientBase(
3435
HttpClient httpClient,
35-
ILogger? logger)
36+
ILogger? logger,
37+
string? apiKey = null)
3638
{
3739
Verify.NotNull(httpClient);
3840

3941
this.HttpClient = httpClient;
4042
this.Logger = logger ?? NullLogger.Instance;
43+
this._apiKey = apiKey;
4144
}
4245

4346
protected static void ValidateMaxTokens(int? maxTokens)
@@ -96,6 +99,10 @@ protected async Task<HttpRequestMessage> CreateHttpRequestAsync(object requestDa
9699
httpRequestMessage.Headers.Authorization =
97100
new AuthenticationHeaderValue("Bearer", bearerKey);
98101
}
102+
else if (!string.IsNullOrWhiteSpace(this._apiKey))
103+
{
104+
httpRequestMessage.Headers.Add("x-goog-api-key", this._apiKey);
105+
}
99106

100107
return httpRequestMessage;
101108
}

dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,17 @@ public GeminiChatCompletionClient(
100100
ILogger? logger = null)
101101
: base(
102102
httpClient: httpClient,
103-
logger: logger)
103+
logger: logger,
104+
apiKey: apiKey)
104105
{
105106
Verify.NotNullOrWhiteSpace(modelId);
106107
Verify.NotNullOrWhiteSpace(apiKey);
107108

108109
string versionSubLink = GetApiVersionSubLink(apiVersion);
109110

110111
this._modelId = modelId;
111-
this._chatGenerationEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._modelId}:generateContent?key={apiKey}");
112-
this._chatStreamingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._modelId}:streamGenerateContent?key={apiKey}&alt=sse");
112+
this._chatGenerationEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._modelId}:generateContent");
113+
this._chatStreamingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._modelId}:streamGenerateContent?alt=sse");
113114
}
114115

115116
/// <summary>

dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiTokenCounterClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,16 @@ public GeminiTokenCounterClient(
3333
ILogger? logger = null)
3434
: base(
3535
httpClient: httpClient,
36-
logger: logger)
36+
logger: logger,
37+
apiKey: apiKey)
3738
{
3839
Verify.NotNullOrWhiteSpace(modelId);
3940
Verify.NotNullOrWhiteSpace(apiKey);
4041

4142
string versionSubLink = GetApiVersionSubLink(apiVersion);
4243

4344
this._modelId = modelId;
44-
this._tokenCountingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._modelId}:countTokens?key={apiKey}");
45+
this._tokenCountingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._modelId}:countTokens");
4546
}
4647

4748
/// <summary>

dotnet/src/Connectors/Connectors.Google/Core/GoogleAI/GoogleAIEmbeddingClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ public GoogleAIEmbeddingClient(
3737
int? dimensions = null)
3838
: base(
3939
httpClient: httpClient,
40-
logger: logger)
40+
logger: logger,
41+
apiKey: apiKey)
4142
{
4243
Verify.NotNullOrWhiteSpace(modelId);
4344
Verify.NotNullOrWhiteSpace(apiKey);
4445

4546
string versionSubLink = GetApiVersionSubLink(apiVersion);
4647

4748
this._embeddingModelId = modelId;
48-
this._embeddingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._embeddingModelId}:batchEmbedContents?key={apiKey}");
49+
this._embeddingEndpoint = new Uri($"https://generativelanguage.googleapis.com/{versionSubLink}/models/{this._embeddingModelId}:batchEmbedContents");
4950
this._dimensions = dimensions;
5051
}
5152

0 commit comments

Comments
 (0)