Skip to content

Commit a26407b

Browse files
authored
Allow to enable HTTP authorization on memory web service (#161)
When deploying the memory service, the instance and its data should protected requiring a client to be authorized. This change allows to enable authorization on all the endpoints, requiring a client to pass one of two valid API keys. When enabled, the API returns 401 is the API key is not provided, and 403 if an invalid API key is provided. By default API keys are passed using the `Authorization` header, although this is configurable. New configuration block in `appsettings.json`: ```json "ServiceAuthorization": { // Whether clients must provide some credentials to interact with the HTTP API "Enabled": false, // Currently "APIKey" is the only type supported "AuthenticationType": "APIKey", // HTTP header name to check "HttpHeaderName": "Authorization", // Define two separate API Keys, to allow key rotation. Both are active. // Keys must be different and case-sensitive, and at least 32 chars long. // Contain only alphanumeric chars and allowed symbols. // Symbols allowed: . _ - (dot, underscore, minus). "AccessKey1": "", "AccessKey2": "", }, ``` The code loading the two API keys is in Service/Program.cs, and can easily be customized to fetch keys from KeyVault or similar solution. OpenAPI Swagger UI updated to support API keys.
1 parent c483b63 commit a26407b

File tree

8 files changed

+247
-24
lines changed

8 files changed

+247
-24
lines changed

dotnet/ClientLib/MemoryWebClient.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,25 @@ public class MemoryWebClient : IKernelMemory
1919
{
2020
private readonly HttpClient _client;
2121

22-
public MemoryWebClient(string endpoint) : this(endpoint, new HttpClient())
22+
public MemoryWebClient(string endpoint, string apiKey = "", string apiKeyHeader = "Authorization")
23+
: this(endpoint, new HttpClient(), apiKey, apiKeyHeader)
2324
{
2425
}
2526

26-
public MemoryWebClient(string endpoint, HttpClient client)
27+
public MemoryWebClient(string endpoint, HttpClient client, string apiKey = "", string apiKeyHeader = "Authorization")
2728
{
2829
this._client = client;
2930
this._client.BaseAddress = new Uri(endpoint);
31+
32+
if (!string.IsNullOrEmpty(apiKey))
33+
{
34+
if (string.IsNullOrEmpty(apiKeyHeader))
35+
{
36+
throw new KernelMemoryException("The name of the HTTP header to pass the API Key is empty");
37+
}
38+
39+
this._client.DefaultRequestHeaders.Add(apiKeyHeader, apiKey);
40+
}
3041
}
3142

3243
/// <inheritdoc />
@@ -300,7 +311,10 @@ public async Task<MemoryAnswer> AskAsync(
300311

301312
#region private
302313

303-
private async Task<string> ImportInternalAsync(string index, DocumentUploadRequest uploadRequest, CancellationToken cancellationToken)
314+
private async Task<string> ImportInternalAsync(
315+
string index,
316+
DocumentUploadRequest uploadRequest,
317+
CancellationToken cancellationToken)
304318
{
305319
// Populate form with values and files from disk
306320
using MultipartFormDataContent formData = new();

dotnet/CoreLib/Configuration/KernelMemoryConfig.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ public class RetrievalConfig
100100
/// </summary>
101101
public string ImageOcrType { get; set; } = string.Empty;
102102

103+
/// <summary>
104+
/// HTTP service authorization settings.
105+
/// </summary>
106+
public ServiceAuthorizationConfig ServiceAuthorization { get; set; } = new();
107+
103108
/// <summary>
104109
/// Settings for the upload of documents and memory creation/update.
105110
/// </summary>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Linq;
5+
6+
namespace Microsoft.KernelMemory.Configuration;
7+
8+
public class ServiceAuthorizationConfig
9+
{
10+
public const string APIKeyAuthType = "APIKey";
11+
public const int AccessKeyMinLength = 32;
12+
13+
/// <summary>
14+
/// Whether clients must provide some credentials to interact with the HTTP API.
15+
/// </summary>
16+
public bool Enabled { get; set; } = false;
17+
18+
/// <summary>
19+
/// Currently "APIKey" is the only type supported
20+
/// </summary>
21+
public string AuthenticationType { get; set; } = APIKeyAuthType;
22+
23+
/// <summary>
24+
/// HTTP header name to check for the access key
25+
/// </summary>
26+
public string HttpHeaderName { get; set; } = "Authorization";
27+
28+
/// <summary>
29+
/// Access Key 1. Alphanumeric, "-" "_" "." allowed. Min 32 chars.
30+
/// Two different keys are always active, to allow secrets rotation.
31+
/// </summary>
32+
public string AccessKey1 { get; set; } = "";
33+
34+
/// <summary>
35+
/// Access Key 2. Alphanumeric, "-" "_" "." allowed. Min 32 chars.
36+
/// Two different keys are always active, to allow secrets rotation.
37+
/// </summary>
38+
public string AccessKey2 { get; set; } = "";
39+
40+
public void Validate()
41+
{
42+
if (!this.Enabled)
43+
{
44+
return;
45+
}
46+
47+
if (this.AuthenticationType != APIKeyAuthType)
48+
{
49+
throw new ConfigurationException($"The authorization type '{this.AuthenticationType}' is not supported. Please use '{APIKeyAuthType}'.");
50+
}
51+
52+
if (string.IsNullOrWhiteSpace(this.HttpHeaderName))
53+
{
54+
throw new ConfigurationException("The HTTP header name cannot be empty");
55+
}
56+
57+
ValidateAccessKey(this.AccessKey1, 1);
58+
ValidateAccessKey(this.AccessKey2, 2);
59+
60+
if (string.Equals(this.AccessKey1, this.AccessKey2, StringComparison.OrdinalIgnoreCase))
61+
{
62+
throw new ConfigurationException("Access keys 1 and 2 are the same. Please use two different keys.");
63+
}
64+
}
65+
66+
private static void ValidateAccessKey(string key, int keyNumber)
67+
{
68+
if (string.IsNullOrEmpty(key))
69+
{
70+
throw new ConfigurationException($"Memory Web Service Access Key {keyNumber} is empty.");
71+
}
72+
73+
if (key.Length < AccessKeyMinLength)
74+
{
75+
throw new ConfigurationException($"Memory Web Service Access Key {keyNumber} is too short, use at least {AccessKeyMinLength} chars.");
76+
}
77+
78+
if (!key.All(IsValidChar))
79+
{
80+
throw new ConfigurationException($"Memory Web Service Access Key {keyNumber} contains some invalid chars (allowed: A-B, a-b, 0-9, '.', '_', '-')");
81+
}
82+
}
83+
84+
private static bool IsValidChar(char c)
85+
{
86+
return char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '-';
87+
}
88+
}

dotnet/CoreLib/KernelMemoryBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public MemoryService BuildAsyncClient()
236236
return new MemoryService(orchestrator, searchClient);
237237
}
238238

239-
public static IKernelMemory BuildWebClient(string endpoint)
239+
public static IKernelMemory BuildWebClient(string endpoint, string apiKey = "", string apiKeyHeader = "Authorization")
240240
{
241241
if (string.IsNullOrWhiteSpace(endpoint))
242242
{
@@ -253,7 +253,7 @@ public static IKernelMemory BuildWebClient(string endpoint)
253253
throw new ConfigurationException("The endpoint is incomplete");
254254
}
255255

256-
return new MemoryWebClient(endpoint);
256+
return new MemoryWebClient(endpoint: endpoint, apiKey: apiKey, apiKeyHeader: apiKeyHeader);
257257
}
258258

259259
public KernelMemoryBuilder WithoutDefaultHandlers()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.KernelMemory.Configuration;
7+
8+
namespace Microsoft.KernelMemory.Service.Auth;
9+
10+
public class HttpAuthEndpointFilter : IEndpointFilter
11+
{
12+
private readonly ServiceAuthorizationConfig _config;
13+
14+
public HttpAuthEndpointFilter(ServiceAuthorizationConfig config)
15+
{
16+
this._config = config;
17+
}
18+
19+
public async ValueTask<object?> InvokeAsync(
20+
EndpointFilterInvocationContext context,
21+
EndpointFilterDelegate next)
22+
{
23+
if (this._config.Enabled)
24+
{
25+
if (!context.HttpContext.Request.Headers.TryGetValue(this._config.HttpHeaderName, out var apiKey))
26+
{
27+
return Results.Problem(detail: "API Key missing", statusCode: 401);
28+
}
29+
30+
if (!string.Equals(apiKey, this._config.AccessKey1, StringComparison.Ordinal)
31+
&& !string.Equals(apiKey, this._config.AccessKey2, StringComparison.Ordinal))
32+
{
33+
return Results.Problem(detail: "Invalid API Key", statusCode: 403);
34+
}
35+
}
36+
37+
return await next(context);
38+
}
39+
}

0 commit comments

Comments
 (0)