// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using Resources;
using Xunit;
using Xunit.Abstractions;
namespace Examples;
public class Example22_OpenAIPlugin_AzureKeyVault : BaseTest
{
private const string SecretName = "Foo";
private const string SecretValue = "Bar";
///
/// This example demonstrates how to connect an Azure Key Vault plugin to the Semantic Kernel.
/// To use this example, there are a few requirements:
/// 1. Register a client application with the Microsoft identity platform.
/// https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
///
/// 2. Create an Azure Key Vault
/// https://learn.microsoft.com/en-us/azure/key-vault/general/quick-create-portal
///
/// 3. Add a permission for Azure Key Vault to your client application
/// https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis
///
/// 4. Set your Key Vault endpoint, client ID, and client secret as user secrets using:
/// dotnet user-secrets set "KeyVault:Endpoint" "your_endpoint"
/// dotnet user-secrets set "KeyVault:ClientId" "your_client_id"
/// dotnet user-secrets set "KeyVault:ClientSecret" "your_secret"
///
/// 5. Replace your tenant ID with the "TENANT_ID" placeholder in dotnet/samples/KernelSyntaxExamples/Resources/22-ai-plugin.json
///
[Fact(Skip = "Setup credentials")]
public async Task RunAsync()
{
var authenticationProvider = new OpenAIAuthenticationProvider(
new Dictionary>()
{
{
"login.microsoftonline.com",
new Dictionary()
{
{ "client_id", TestConfiguration.KeyVault.ClientId },
{ "client_secret", TestConfiguration.KeyVault.ClientSecret },
{ "grant_type", "client_credentials" }
}
}
}
);
Kernel kernel = new();
var openApiSpec = EmbeddedResource.Read("22-openapi.json");
using var messageStub = new HttpMessageHandlerStub(openApiSpec);
using var httpClient = new HttpClient(messageStub);
// Import Open AI Plugin
var openAIManifest = EmbeddedResource.ReadStream("22-ai-plugin.json");
var plugin = await kernel.ImportPluginFromOpenAIAsync(
"AzureKeyVaultPlugin",
openAIManifest!,
new OpenAIFunctionExecutionParameters
{
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
HttpClient = httpClient,
EnableDynamicPayload = true,
ServerUrlOverride = new Uri(TestConfiguration.KeyVault.Endpoint)
});
await AddSecretToAzureKeyVaultAsync(kernel, plugin);
await GetSecretFromAzureKeyVaultWithRetryAsync(kernel, plugin);
}
private async Task AddSecretToAzureKeyVaultAsync(Kernel kernel, KernelPlugin plugin)
{
// Add arguments for required parameters, arguments for optional ones can be skipped.
var arguments = new KernelArguments
{
["secret-name"] = SecretName,
["value"] = SecretValue,
["api-version"] = "7.0",
["enabled"] = "true",
};
// Run
var functionResult = await kernel.InvokeAsync(plugin["SetSecret"], arguments);
var result = functionResult.GetValue();
Console.WriteLine("SetSecret function result: {0}", result?.Content?.ToString());
}
private static async Task GetSecretFromAzureKeyVaultWithRetryAsync(Kernel kernel, KernelPlugin plugin)
{
// Add arguments for required parameters, arguments for optional ones can be skipped.
var arguments = new KernelArguments();
arguments["secret-name"] = SecretName;
arguments["api-version"] = "7.0";
// Run
var functionResult = await kernel.InvokeAsync(plugin["GetSecret"], arguments);
var result = functionResult.GetValue();
Console.WriteLine("GetSecret function result: {0}", result?.Content?.ToString());
}
public Example22_OpenAIPlugin_AzureKeyVault(ITestOutputHelper output) : base(output)
{
}
}
#region Utility Classes
///
/// Provides authentication for HTTP requests to OpenAI using OAuth or verification tokens.
///
internal sealed class OpenAIAuthenticationProvider
{
private readonly Dictionary> _oAuthValues;
private readonly Dictionary _credentials;
///
/// Creates an instance of the class.
///
/// A dictionary containing OAuth values for each authentication scheme.
/// A dictionary containing credentials for each authentication scheme.
public OpenAIAuthenticationProvider(Dictionary>? oAuthValues = null, Dictionary? credentials = null)
{
this._oAuthValues = oAuthValues ?? new();
this._credentials = credentials ?? new();
}
///
/// Applies the authentication content to the provided HTTP request message.
///
/// The HTTP request message.
/// Name of the plugin
/// The used to authenticate.
/// The cancellation token.
public async Task AuthenticateRequestAsync(HttpRequestMessage request, string pluginName, OpenAIAuthenticationConfig openAIAuthConfig, CancellationToken cancellationToken = default)
{
if (openAIAuthConfig.Type == OpenAIAuthenticationType.None)
{
return;
}
string scheme = "";
string credential = "";
if (openAIAuthConfig.Type == OpenAIAuthenticationType.OAuth)
{
var domainOAuthValues = this._oAuthValues[openAIAuthConfig.AuthorizationUrl!.Host]
?? throw new KernelException("No OAuth values found for the provided authorization URL.");
var values = new Dictionary(domainOAuthValues) {
{ "scope", openAIAuthConfig.Scope ?? "" },
};
using HttpContent? requestContent = openAIAuthConfig.AuthorizationContentType switch
{
"application/x-www-form-urlencoded" => new FormUrlEncodedContent(values),
"application/json" => new StringContent(JsonSerializer.Serialize(values), Encoding.UTF8, "application/json"),
_ => throw new KernelException($"Unsupported authorization content type: {openAIAuthConfig.AuthorizationContentType}"),
};
// Request the token
using var client = new HttpClient();
using var authRequest = new HttpRequestMessage(HttpMethod.Post, openAIAuthConfig.AuthorizationUrl) { Content = requestContent };
var response = await client.SendAsync(authRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// Read the token
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
OAuthTokenResponse? tokenResponse;
try
{
tokenResponse = JsonSerializer.Deserialize(responseContent);
}
catch (JsonException)
{
throw new KernelException($"Failed to deserialize token response from {openAIAuthConfig.AuthorizationUrl}.");
}
// Get the token type and value
scheme = tokenResponse?.TokenType ?? throw new KernelException("No token type found in the response.");
credential = tokenResponse?.AccessToken ?? throw new KernelException("No access token found in the response.");
}
else
{
var token = openAIAuthConfig.VerificationTokens?[pluginName]
?? throw new KernelException("No verification token found for the provided plugin name.");
scheme = openAIAuthConfig.AuthorizationType.ToString();
credential = token;
}
request.Headers.Authorization = new AuthenticationHeaderValue(scheme, credential);
}
}
///
/// Represents the authentication section for an OpenAI plugin.
///
internal sealed class OAuthTokenResponse
{
///
/// The type of access token.
///
[JsonPropertyName("token_type")]
public string TokenType { get; set; } = "";
///
/// The authorization scope.
///
[JsonPropertyName("access_token")]
public string AccessToken { get; set; } = "";
}
internal sealed class HttpMessageHandlerStub : DelegatingHandler
{
public HttpResponseMessage ResponseToReturn { get; set; }
public HttpMessageHandlerStub(string responseToReturn)
{
this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(responseToReturn, Encoding.UTF8, MediaTypeNames.Application.Json)
};
}
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.RequestUri!.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
{
return this.ResponseToReturn;
}
using var httpClient = new HttpClient();
using var newRequest = new HttpRequestMessage() // construct a new request because the same one cannot be sent twice
{
Content = request.Content,
Method = request.Method,
RequestUri = request.RequestUri,
};
foreach (var header in request.Headers)
{
newRequest.Headers.Add(header.Key, header.Value);
}
return await httpClient.SendAsync(newRequest, cancellationToken).ConfigureAwait(false);
}
}
#endregion