An alternative version of OpenAI authentication functionality

This commit is contained in:
SergeyMenshykh
2023-10-27 00:17:30 +01:00
parent cf4bfbd574
commit db249e5836
12 changed files with 342 additions and 33 deletions
@@ -49,7 +49,7 @@ public static class Example22_OpenApiPlugin_AzureKeyVault
var plugin = await kernel.ImportPluginFunctionsAsync(
PluginResourceNames.AzureKeyVault,
stream!,
new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync });
new OpenApiFunctionExecutionParameters { AuthenticateCallbackProvider = (manifest) => authenticationProvider.AuthenticateRequestAsync });
// Add arguments for required parameters, arguments for optional ones can be skipped.
var contextVariables = new ContextVariables();
@@ -80,7 +80,7 @@ public static class Example22_OpenApiPlugin_AzureKeyVault
stream!,
new OpenApiFunctionExecutionParameters
{
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
AuthenticateCallbackProvider = _ => authenticationProvider.AuthenticateRequestAsync,
EnableDynamicPayload = true
});
@@ -35,7 +35,7 @@ public static class Example23_OpenApiPlugin_GitHub
var plugin = await kernel.ImportPluginFunctionsAsync(
"GitHubPlugin",
"../../../../../../samples/dotnet/OpenApiPluginsExample/GitHubPlugin/openapi.json",
new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync });
new OpenApiFunctionExecutionParameters { AuthenticateCallbackProvider = _ => authenticationProvider.AuthenticateRequestAsync });
// Add arguments for required parameters, arguments for optional ones can be skipped.
var contextVariables = new ContextVariables();
@@ -69,7 +69,7 @@ public static class Example23_OpenApiPlugin_GitHub
var plugin = await kernel.ImportPluginFunctionsAsync(
"GitHubPlugin",
"../../../../../../samples/dotnet/OpenApiPluginsExample/GitHubPlugin/openapi.json",
new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync });
new OpenApiFunctionExecutionParameters { AuthenticateCallbackProvider = _ => authenticationProvider.AuthenticateRequestAsync });
// Add arguments for required parameters, arguments for optional ones can be skipped.
var contextVariables = new ContextVariables();
@@ -43,12 +43,12 @@ public static class Example24_OpenApiPlugin_Jira
if (useLocalFile)
{
var apiPluginFile = "./../../../Plugins/JiraPlugin/openapi.json";
jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: tokenProvider.AuthenticateRequestAsync));
jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallbackProvider: (_) => tokenProvider.AuthenticateRequestAsync));
}
else
{
var apiPluginRawFileURL = new Uri("https://raw.githubusercontent.com/microsoft/PowerPlatformConnectors/dev/certified-connectors/JIRA/apiDefinition.swagger.json");
jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(httpClient, tokenProvider.AuthenticateRequestAsync));
jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(authCallbackProvider: (_) => tokenProvider.AuthenticateRequestAsync));
}
// GetIssue Function
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
namespace Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;
/// <summary>
/// Represents a delegate that provides authentication callback based on plugin manifest auth config.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public delegate AuthenticateRequestAsyncCallback AuthenticateCallbackProvider(OpenAIAuthenticationManifest? manifest = null);
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;
/// <summary>
/// Represents the authentication configuration for an OpenAI plugin.
/// </summary>
public record OpenAIAuthenticationManifest
{
/// <summary>
/// The type of authentication.
/// </summary>
public OpenAIAuthenticationType Type { get; set; } = OpenAIAuthenticationType.None;
/// <summary>
/// The type of authorization.
/// </summary>
public OpenAIAuthorizationType? AuthorizationType { get; set; }
/// <summary>
/// The client URL.
/// </summary>
public Uri? ClientUrl { get; set; }
/// <summary>
/// The authorization URL.
/// </summary>
public Uri? AuthorizationUrl { get; set; }
/// <summary>
/// The authorization content type.
/// </summary>
public string? AuthorizationContentType { get; set; }
/// <summary>
/// The authorization scope.
/// </summary>
public string? Scope { get; set; }
/// <summary>
/// The verification tokens.
/// </summary>
public Dictionary<string, string>? VerificationTokens { get; set; }
}
/// <summary>
/// Represents the type of authentication for an OpenAI plugin.
/// </summary>
public enum OpenAIAuthenticationType
{
/// <summary>
/// No authentication.
/// </summary>
[EnumMember(Value = "none")]
None,
/// <summary>
/// User HTTP authentication.
/// </summary>
[EnumMember(Value = "user_http")]
UserHttp,
/// <summary>
/// Service HTTP authentication.
/// </summary>
[EnumMember(Value = "service_http")]
ServiceHttp,
/// <summary>
/// OAuth authentication.
/// </summary>
[EnumMember(Value = "oauth")]
OAuth
}
/// <summary>
/// Represents the type of authorization for an OpenAI plugin.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OpenAIAuthorizationType
{
/// <summary>
/// Basic authorization.
/// </summary>
[EnumMember(Value = "basic")]
Basic,
/// <summary>
/// Bearer authorization.
/// </summary>
[EnumMember(Value = "bearer")]
Bearer
}
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;
/// <summary>
/// An example of OpenAI auth provider.
/// </summary>
public class OpenAIAuthenticationProvider
{
private readonly OpenAIAuthenticationManifest _manifest;
/// <summary>
/// Creates an instance of the <see cref="BearerAuthenticationProvider"/> class.
/// </summary>
/// <param name="manifest">OpenAI auth manifest</param>
public OpenAIAuthenticationProvider(OpenAIAuthenticationManifest manifest)
{
this._manifest = manifest;
}
/// <summary>
/// Applies the token to the provided HTTP request message.
/// </summary>
/// <param name="request">The HTTP request message.</param>
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var token = "fake-token"; //Implement all the required logic to parse the manifest and issue an auth token according to the manifest.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
@@ -15,9 +15,13 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;
using Microsoft.SemanticKernel.Functions.OpenAPI.Model;
using Microsoft.SemanticKernel.Functions.OpenAPI.OpenApi;
using Microsoft.SemanticKernel.Orchestration;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Newtonsoft.Json;
namespace Microsoft.SemanticKernel.Functions.OpenAPI.Extensions;
@@ -119,8 +123,9 @@ public static class KernelAIPluginExtensions
var pluginContents = await LoadDocumentFromUriAsync(
kernel,
uri,
executionParameters,
httpClient,
executionParameters?.UserAgent,
executionParameters?.AuthenticateCallbackProvider?.Invoke(null),
cancellationToken).ConfigureAwait(false);
return await CompleteImportAsync(
@@ -178,15 +183,27 @@ public static class KernelAIPluginExtensions
Uri? documentUri = null,
CancellationToken cancellationToken = default)
{
var document = pluginContents;
AuthenticateRequestAsyncCallback? authCallback = null;
if (TryParseAIPluginForUrl(pluginContents, out var openApiUrl))
{
return await kernel
.ImportPluginFunctionsAsync(
pluginName,
new Uri(openApiUrl),
executionParameters,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (TryParseAIPluginForAuth(document, out var openApiManifestAuth) && openApiManifestAuth!.Type != OpenAIAuthenticationType.None)
{
if (executionParameters?.AuthenticateCallbackProvider != null)
{
authCallback = executionParameters.AuthenticateCallbackProvider(openApiManifestAuth);
}
}
document = await LoadDocumentFromUriAsync(
kernel,
new Uri(openApiUrl),
httpClient,
executionParameters?.UserAgent,
authCallback,
cancellationToken).ConfigureAwait(false);
}
return await LoadPluginAsync(
@@ -194,7 +211,8 @@ public static class KernelAIPluginExtensions
pluginName,
executionParameters,
httpClient,
pluginContents,
document,
authCallback,
documentUri,
cancellationToken).ConfigureAwait(false);
}
@@ -205,6 +223,7 @@ public static class KernelAIPluginExtensions
OpenApiFunctionExecutionParameters? executionParameters,
HttpClient httpClient,
string pluginJson,
AuthenticateRequestAsyncCallback? authCallback,
Uri? documentUri = null,
CancellationToken cancellationToken = default)
{
@@ -216,7 +235,7 @@ public static class KernelAIPluginExtensions
var runner = new RestApiOperationRunner(
httpClient,
executionParameters?.AuthCallback,
authCallback,
executionParameters?.UserAgent,
executionParameters?.EnableDynamicPayload ?? false,
executionParameters?.EnablePayloadNamespacing ?? false);
@@ -247,15 +266,20 @@ public static class KernelAIPluginExtensions
private static async Task<string> LoadDocumentFromUriAsync(
IKernel kernel,
Uri uri,
OpenApiFunctionExecutionParameters? executionParameters,
HttpClient httpClient,
string? userAgent,
AuthenticateRequestAsyncCallback? authCallback,
CancellationToken cancellationToken)
{
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri.ToString());
using var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString());
request.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(userAgent ?? Telemetry.HttpUserAgent));
requestMessage.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(executionParameters?.UserAgent ?? Telemetry.HttpUserAgent));
if (authCallback != null)
{
await authCallback(request).ConfigureAwait(false);
}
using var response = await httpClient.SendWithSuccessCheckAsync(requestMessage, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
return await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false);
}
@@ -322,6 +346,31 @@ public static class KernelAIPluginExtensions
}
}
private static bool TryParseAIPluginForAuth(string gptPluginJson, out OpenAIAuthenticationManifest? openApiManifestAuth)
{
try
{
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
JObject? gptPlugin = JsonConvert.DeserializeObject<JObject>(gptPluginJson, serializerSettings);
openApiManifestAuth = gptPlugin?["auth"]?.ToObject<OpenAIAuthenticationManifest>(JsonSerializer.Create(serializerSettings));
return openApiManifestAuth != null;
}
catch (System.Text.Json.JsonException)
{
openApiManifestAuth = null;
return false;
}
}
/// <summary>
/// Registers SKFunction for a REST API operation.
/// </summary>
@@ -20,8 +20,14 @@ public class OpenApiFunctionExecutionParameters
/// <summary>
/// Callback for adding authentication data to HTTP requests.
/// </summary>
[Obsolete("This property will be removed in one of the next releases. Please use the AuthenticateCallbackProvider property instead.")]
public AuthenticateRequestAsyncCallback? AuthCallback { get; set; }
/// <summary>
/// Provider to get authentication callback.
/// </summary>
public AuthenticateCallbackProvider? AuthenticateCallbackProvider { get; set; }
/// <summary>
/// Override for REST API operation server url.
/// </summary>
@@ -58,8 +64,8 @@ public class OpenApiFunctionExecutionParameters
/// Initializes a new instance of the <see cref="OpenApiFunctionExecutionParameters"/> class.
/// </summary>
/// <param name="httpClient">The HttpClient to use for sending HTTP requests.</param>
/// <param name="authCallback">The callback for adding authentication data to HTTP requests.</param>
/// <param name="serverUrlOverride">The override for the REST API operation server URL.</param>
/// <param name="authCallbackProvider">The provider to return callback for adding authentication data to HTTP requests.</param>
/// <param name="userAgent">Optional user agent header value.</param>
/// <param name="ignoreNonCompliantErrors">A flag indicating whether to ignore non-compliant errors or not
/// If set to true, the operation execution will not throw exceptions for non-compliant documents.
@@ -70,15 +76,15 @@ public class OpenApiFunctionExecutionParameters
/// Namespaces prevent naming conflicts by adding the parent parameter name as a prefix, separated by dots.</param>
public OpenApiFunctionExecutionParameters(
HttpClient? httpClient = null,
AuthenticateRequestAsyncCallback? authCallback = null,
Uri? serverUrlOverride = null,
AuthenticateCallbackProvider? authCallbackProvider = null,
string userAgent = Telemetry.HttpUserAgent,
bool ignoreNonCompliantErrors = false,
bool enableDynamicOperationPayload = false,
bool enablePayloadNamespacing = false)
{
this.HttpClient = httpClient;
this.AuthCallback = authCallback;
this.AuthenticateCallbackProvider = authCallbackProvider;
this.ServerUrlOverride = serverUrlOverride;
this.UserAgent = userAgent;
this.IgnoreNonCompliantErrors = ignoreNonCompliantErrors;
@@ -16,6 +16,7 @@
<ItemGroup>
<None Remove="Grpc\protoV3.proto" />
<None Remove="OpenAPI\TestPlugins\ai-plugin.json" />
<None Remove="OpenAPI\TestPlugins\documentV3_1.yaml" />
<None Remove="OpenAPI\TestPlugins\nonCompliant_documentV3_0.json" />
<None Remove="OpenAPI\TestPlugins\openapi.json" />
@@ -23,6 +24,7 @@
<ItemGroup>
<EmbeddedResource Include="Grpc\Protobuf\TestPlugins\protoV3.proto" />
<EmbeddedResource Include="OpenAPI\TestPlugins\ai-plugin.json" />
<EmbeddedResource Include="OpenAPI\TestPlugins\documentV2_0.json" />
<EmbeddedResource Include="OpenAPI\TestPlugins\nonCompliant_documentV3_0.json" />
<EmbeddedResource Include="OpenAPI\TestPlugins\documentV3_1.yaml" />
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -9,10 +10,13 @@ using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;
using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions;
using Microsoft.SemanticKernel.Functions.OpenAPI.Model;
using Microsoft.SemanticKernel.Functions.OpenAPI.OpenApi;
using Microsoft.SemanticKernel.Orchestration;
using Moq;
using SemanticKernel.Functions.UnitTests.OpenAPI.TestPlugins;
using Xunit;
@@ -47,6 +51,60 @@ public sealed class KernelAIPluginExtensionsTests : IDisposable
this._sut = new OpenApiDocumentParser();
}
[Fact]
public async Task ItUsesAuthFromOpenAiPluginManifestWhenFetchingOpenApiSpecAsync()
{
//Arrange
using var messageHandlerStub = new HttpMessageHandlerStub((HttpRequestMessage message) =>
{
if (message!.RequestUri!.AbsoluteUri == "https://fake-random-test-host/openai_manifest.json")
{
var response = new HttpResponseMessage();
response.Content = new StreamContent(ResourcePluginsProvider.LoadFromResource("ai-plugin.json"));
return response;
}
if (message!.RequestUri!.AbsoluteUri == "https://fake-random-test-host/openapi.json")
{
var response = new HttpResponseMessage();
response.Content = new StreamContent(this._openApiDocument);
return response;
}
throw new SKException($"Unexpected url - {message!.RequestUri!.AbsoluteUri}");
});
using var httpClient = new HttpClient(messageHandlerStub, false);
var providerActualArguments = new List<OpenAIAuthenticationManifest?>();
var authCallbackMock = new Mock<AuthenticateRequestAsyncCallback>();
var authCallbackProviderMock = new Mock<AuthenticateCallbackProvider>();
authCallbackProviderMock
.Setup(p => p.Invoke(It.IsAny<OpenAIAuthenticationManifest>()))
.Callback<OpenAIAuthenticationManifest>(c => providerActualArguments.Add(c))
.Returns(authCallbackMock.Object);
//Act
var plugin = await this._kernel.ImportPluginFunctionsAsync(
"fakePlugin",
new Uri("https://fake-random-test-host/openai_manifest.json"),
new OpenApiFunctionExecutionParameters { HttpClient = httpClient, AuthenticateCallbackProvider = authCallbackProviderMock.Object });
//Assert
var setSecretFunction = plugin["SetSecret"];
Assert.NotNull(setSecretFunction);
//Check provider is called two times: first time with null when loading the OpenAI manifest document and the second time with auth config when loading the OpenAPI document.
authCallbackProviderMock.Verify(target => target.Invoke(It.IsAny<OpenAIAuthenticationManifest>()), Times.Exactly(2));
Assert.Equal(2, providerActualArguments.Count);
Assert.Null(providerActualArguments[0]);
Assert.Equal("https://vault.azure.net/.default", providerActualArguments[1]?.Scope);
authCallbackMock.Verify(target => target.Invoke(It.IsAny<HttpRequestMessage>()), Times.Exactly(2));
}
[Fact]
public async Task ItCanIncludeOpenApiOperationParameterTypesIntoFunctionParametersViewAsync()
{
@@ -203,8 +261,8 @@ public sealed class KernelAIPluginExtensionsTests : IDisposable
public async Task ItShouldConvertPluginComplexResponseToStringToSaveItInContextAsync()
{
//Arrange
using var messageHandlerStub = new HttpMessageHandlerStub();
messageHandlerStub.ResponseToReturn.Content = new StringContent("fake-content", Encoding.UTF8, MediaTypeNames.Application.Json);
using var content = new StringContent("fake-content", Encoding.UTF8, MediaTypeNames.Application.Json);
using var messageHandlerStub = new HttpMessageHandlerStub(content);
using var httpClient = new HttpClient(messageHandlerStub, false);
@@ -23,24 +23,51 @@ internal sealed class HttpMessageHandlerStub : DelegatingHandler
public HttpMethod? Method { get; private set; }
public HttpResponseMessage ResponseToReturn { get; set; }
public Func<HttpRequestMessage, HttpResponseMessage> ResponseToReturnProvider { get; set; }
public HttpMessageHandlerStub()
{
this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
this.ResponseToReturn.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json);
this.ResponseToReturnProvider = (_) =>
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
response.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json);
return response;
};
}
public HttpMessageHandlerStub(Stream responseToReturn)
{
this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
this.ResponseToReturn.Content = new StreamContent(responseToReturn);
this.ResponseToReturnProvider = (_) =>
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
response.Content = new StreamContent(responseToReturn);
return response;
};
}
public HttpMessageHandlerStub(HttpContent content)
{
this.ResponseToReturnProvider = (_) =>
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
response.Content = content;
return response;
};
}
public HttpMessageHandlerStub(Func<HttpRequestMessage, HttpResponseMessage> responseToReturn)
{
this.ResponseToReturnProvider = responseToReturn;
}
public void ResetResponse()
{
this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
this.ResponseToReturn.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json);
this.ResponseToReturnProvider = (_) =>
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
response.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json);
return response;
};
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
@@ -51,6 +78,6 @@ internal sealed class HttpMessageHandlerStub : DelegatingHandler
this.RequestContent = request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken);
this.ContentHeaders = request.Content?.Headers;
return await Task.FromResult(this.ResponseToReturn);
return await Task.FromResult(this.ResponseToReturnProvider(request));
}
}
@@ -0,0 +1,24 @@
{
"schema_version": "v1",
"name_for_human": "AzureKeyVault",
"name_for_model": "AzureKeyVault",
"description_for_human": "Query and interact with Azure Key Vault",
"description_for_model": "Query and interact with Azure Key Vault",
"auth": {
"type": "oauth",
"client_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"scope": "https://vault.azure.net/.default",
"authorization_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"authorization_content_type": "application/x-www-form-urlencoded",
"verification_tokens": {
"openai": "00000000000000000000000000000000"
}
},
"api": {
"type": "openapi",
"url": "https://fake-random-test-host/openapi.json"
},
"logo_url": "https://contoso.com/logo.png",
"contact_email": "[email protected]",
"legal_info_url": "https://privacy.microsoft.com/en-US/privacystatement"
}