2

I am getting an invalid token when trying to get it from a C# code (console app). Let me explain.

Scenario 1 (from a Frontend application):

  1. User enters his/ her credentials in a React app.
  2. A call is made to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize endpoint.
  3. A token id is retrieved (so far so good)
  4. When the authenticated user makes a request to any endpoint to a private Web API, a second call to the following endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token is made attaching the token id from step 3.
  5. A new JWT token is retrieved
  6. This JWT token is attached to the headers of the request made to the Web API
  7. A response is return to the React app.

This is the regular flow we are using when interacting between the React app and the Web API. Here is a visual flow from MS docs

Now, here comes the a new scenario:

Scenario 2 (try to do/ simulate the same from a c# code):

I am trying to get the JWT token to be attached to the header from C# code, I am using MSAL.NET for that. Oficial doc

For my testing I am using a console application:

private static async Task RunAsync()
    {
        string clientId = "client Id of the application that I have registered using azure app registration in Azure B2C";
        string clientSecret = "client secret of the application that I have registered using azure app registration in Azure B2C";
        string instance = "https://login.microsoftonline.com/{0}/";
        string tenantId = "Tenant Id that I can see when I open the application that I have registered using azure app registration in Azure B2C";
        string webAppUri = "web app domain";

        // For Web applications that use OpenID Connect Authorization Code flow, use IConfidentialClientApplication
        IConfidentialClientApplication app;

        app = ConfidentialClientApplicationBuilder
                .Create(clientId)
                .WithClientSecret(clientSecret)
                .WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
                .WithLegacyCacheCompatibility(false)
                .Build();

        // For confidential clients, this value should use a format similar to {Application ID URI}/.default.
        // https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-netcore-daemon#requesting-tokens
        string[] scopes = new string[] { $"{webAppUri}/.default" };

        AuthenticationResult result = null;

        try
        {
            result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("Token acquired");
            Console.WriteLine($"{result.AccessToken}");

            Console.ResetColor();
        }
        catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011"))
        {
            // Invalid scope. The scope has to be of the form "https://resourceurl/.default"
            // Mitigation: change the scope to be as expected
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("Scope provided is not supported");
            Console.ResetColor();
        }
        

        if (result != null)
        {
            var httpClient = new HttpClient();
            var apiCaller = new ProtectedApiCallHelper(httpClient);

            string webApiUrl = "http://localhost:12345/mycustomwebapi/list";

            var defaultRequetHeaders = httpClient.DefaultRequestHeaders;
            if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json"))
            {
                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            }
            defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            HttpResponseMessage response = await httpClient.GetAsync(webApiUrl);
            if (response.IsSuccessStatusCode)
            {
                string json = await response.Content.ReadAsStringAsync();
                var jsonResult = JsonConvert.DeserializeObject<List<JObject>>(json);
            }
            else
            {
                Console.WriteLine($"Failed to call the Web Api: {response.StatusCode}");
                string content = await response.Content.ReadAsStringAsync();
            }
            Console.ResetColor();
        }
    }

The problem I am having with the above code is that I am getting a well formatted JWT token and attaching it to the headers. But when making the call to the custom/ secure web api, I am getting a 401 Unauthorized response.

So, I am having a different thoughts on this:

  1. I am not quite sure if I am missing a step in this C# code vs the steps in Scenario 1.
  2. Or if I need to configure any special access/ permission in Azure for the web api which is already registered in Azure AD.
  3. Should I try to do the same steps in C#? call first: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize endpoint and the https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token to get a valid JWT token?

One additional thing, is that when comparing the JWT tokens (https://jwt.io) from C# vs the one obtain in the Frontend App, there are some different attributes.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
MikePR
  • 2,786
  • 5
  • 31
  • 64
  • so you're trying to call an api from a backend onbehalf of a user ? https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow – Thomas Aug 19 '21 at 00:17
  • 1
    Reading the article, I would say that's the use case I was looking for. Thanks for sharing it – MikePR Aug 19 '21 at 02:53

1 Answers1

1

Acquiring tokens for confidential client applications is used in scenarios where the current app is a middle-tier service which was called with a token representing an end user. The app can use the token oboAssertion to request another token to access downstream web API, on behalf of that user. Refer OAuth2.0 On-Behalf-Of flow | Microsoft Docs as suggested by thomas-Thank you @Thomas

For confidential clients requested scopes use format similar to {Application ID URI}/.default.

api://{api app client id}/.default

For custom web APIs, {Application ID URI} is defined in the Azure portal, under Application Registration (Preview) > Expose an API.

Most cases, 401 error means that the audience of your token does not match your api. so one must make sure to set the scope to your api when you request the token. check the aud claim, and make sure if that is the api you want. When you expose an api protected by Azure, then you need to set the scope to your custom api, usually api://{api app client id}/scope name, and then you need to add the client application to the api application.

Other references: reference1 , reference2

kavyaS
  • 8,026
  • 1
  • 7
  • 19