Home

Using AzureOpenAIClient with LINQPadTokenCredential

Hi,

I'm trying to get the sample from the new Azure OpenAI library for .NET (here) going in LINQPad using creadentials (rather than the api call).

The problem I'm hitting is that when GetTokenAsync is called and the authentication window pops up it is blank:

If I open dev tools on the window and hit reload, I can see a request error which says that I don't have the proper consent.

I'm not clear on whether I need to add scopes and if so how I would integrate that into LINQPadTokenCredential as it's the ChatClient.CompleteChat call that appears to invoke GetToken?

Am I doing something obviously wrong here?

Many thanks

John

Pasting code below:

// using Azure.AI.OpenAI package version 2.0.0

void Main()
{
    // Based on:
    // https://devblogs.microsoft.com/azure-sdk/announcing-the-stable-release-of-the-azure-openai-library-for-net/

    string authEndPoint = Util.AzureCloud.PublicCloud.AuthenticationEndpoint;
    string tenantID = "<my-tenant-domain>";
    string userHint = $"<my-name-part>@{tenantID}";
    string azureOpenAiUri = "https://<my-azure-openai-resource.com>";

    //AzureOpenAIClient azureClient = new(
    //new Uri("https://your-azure-openai-resource.com"),
    //new DefaultAzureCredential());

    var credential = new LINQPadTokenCredential(authEndPoint + tenantID, userHint);

    AzureOpenAIClient azureClient = new(new Uri(azureOpenAiUri), credential);

    ChatClient chatClient = azureClient.GetChatClient("<my-model-deployment-name>");

    ChatCompletion completion = chatClient.CompleteChat(
        [
            // System messages represent instructions or other guidance about how the assistant should behave
            new SystemChatMessage("You are a helpful assistant that talks like a pirate."),
            // User messages represent user input, whether historical or the most recent input
            new UserChatMessage("Hi, can you help me?"),
            // Assistant messages in a request represent conversation history for responses
            new AssistantChatMessage("Arrr! Of course, me hearty! What can I do for ye?"),
            new UserChatMessage("What's the best way to train a parrot?"),
        ]);

    Console.WriteLine($"{completion.Role}: {completion.Content[0].Text}");
    completion.Dump();
}


class LINQPadTokenCredential : TokenCredential
{
    public readonly string Authority, UserIDHint;

    public LINQPadTokenCredential(string authority, string userIDHint) => (Authority, UserIDHint) = (authority, userIDHint);

    public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
        => GetTokenAsync(requestContext, cancellationToken).Result;

    public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext,
                                                                CancellationToken cancelToken)
    {
        // Call LINQPad's AcquireTokenAsync method to authenticate interactively, and cache token in the LINQPad GUI.
        var auth = await Util.MSAL.AcquireTokenAsync(Authority, requestContext.Scopes, UserIDHint).ConfigureAwait(false);
        return new AccessToken(auth.AccessToken, auth.ExpiresOn);
    }
}

Comments

  • Thanks for your reply Joe. I did try this before and it's where I discovered LINQPadTokenCredential. I've discovered that a single scope of "https://cognitiveservices.azure.com/.default" is being passed in with the context, but I'm still hitting this 65002 error via dev tools. In Entra sign-in logs I can see the errror (65002) which has the following details:

    Failure reason - Consent between first party application '{applicationId}' and first party resource '{resourceId}' must be configured via preauthorization - applications owned and operated by Microsoft must get approval from the API owner before requesting tokens for that API.

    Additional details - A developer in your tenant may be attempting to reuse an App ID owned by Microsoft. This error prevents them from impersonating a Microsoft application to call other APIs. They must move to another app ID they register in portal.azure.com.

    Currently I'm not passing in an app ID so I'm not clear where that comes from. Anyway, I'll keep bashing at it.

    Best regards

    John

  • So thank you again. That thread did help and I'm now up and running.

    I now understand that there were a number of things I was not clear on:

    • As per the other thread I needed to add the https://login.microsoftonline.com/common/oauth2/nativeclient url to the redirect list under App Registrations / MyApp / Authentication (if you have no redirects you hit ADDSTS500113 - No reply address is registered). This matches the redirect in the request which I'm assuming is built by Util.MSAL?

    • I needed to pass in the clientID (for 'MyApp') in the Util.MSAL.AcquireTokenAsync call and without this I got the blank screen above with the AADSTS65002 error (via dev tools).

    • Once these two items were in place I was able to authenticate and get a token, but then hit a 401 PermissionDenied, so I went back to the Azure OpenAI resource and added "Cognitive Services OpenAI Contributor" and "Cognitive Services OpenAI User" in additiona to the "Cognitive Services Contributor" that I had previously added.

    So below is my working final code (where I set the prompt to require consent each time....so I could see where I was).

    Thanks again Joe and other thread for your help.

    Best regards

    John

    void Main()
    {
        // Based on:
        // https://devblogs.microsoft.com/azure-sdk/announcing-the-stable-release-of-the-azure-openai-library-for-net/
    
        string authEndPoint = Util.AzureCloud.PublicCloud.AuthenticationEndpoint;
        string tenantID = "<my-tenant-name";
        string userHint = $"my-name@{tenantID}";
        var azureOpenAiUri = new Uri("https://<my-openai-resource-endpoint>.openai.azure.com");
        string clientID = Util.GetPassword("<my-app-registration-clientid>");
    
        var credential = new LINQPadTokenCredential($"{authEndPoint}{tenantID}", userHint, clientID);
    
        AzureOpenAIClient azureClient = new(azureOpenAiUri, credential);
    
        ChatClient chatClient = azureClient.GetChatClient("<my-model-deployment-name>");
    
        ChatCompletion completion = chatClient.CompleteChat(
            [
                // System messages represent instructions or other guidance about how the assistant should behave
                new SystemChatMessage("You are a helpful assistant that talks like a pirate."),
                // User messages represent user input, whether historical or the most recent input
                new UserChatMessage("Hi, can you help me?"),
                // Assistant messages in a request represent conversation history for responses
                new AssistantChatMessage("Arrr! Of course, me hearty! What can I do for ye?"),
                new UserChatMessage("What's the best way to train a parrot?"),
            ]);
    
        Console.WriteLine($"{completion.Role}: {completion.Content[0].Text}");
        completion.Dump();  
    }
    
    
    class LINQPadTokenCredential : TokenCredential
    {
        public readonly string Authority, UserIDHint, ClientID;
    
        public LINQPadTokenCredential(string authority, string userIDHint, string clientId) => (Authority, UserIDHint, ClientID) = (authority, userIDHint, clientId);
    
        public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
            => GetTokenAsync(requestContext, cancellationToken).Result;
    
        public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, 
                                                                   CancellationToken cancelToken)
        {
            // Call LINQPad's AcquireTokenAsync method to authenticate interactively, and cache token in the LINQPad GUI.
            var auth = await Util.MSAL.AcquireTokenAsync(authority: Authority,
                                                         scopes:    requestContext.Scopes,
                                                         userID:    UserIDHint,
                                                         prompt:    Util.MSAL.Prompt.Consent,
                                                         clientID:  ClientID)
                                                         .ConfigureAwait(false);
    
            this.Dump();
            requestContext.Dump();
            auth.Dump();
    
            return new AccessToken(auth.AccessToken, auth.ExpiresOn);
        }
    }
    
  • Thanks for sharing your solution.

Sign In or Register to comment.