top of page

A deeper dive into Business Central API and OAuth2 tokens



This post is a story of an almost detective investigation which once again demonstrates that a small typo in code can result in a sleepless night of debugging with a ton of documentation, but in the end can enrich the developer’s knowledge.

This somewhat adventurous dive started from a long debugging session – search for a bug in an Azure function app which was acquiring an OAuth token to authenticate in Azure AD and call a Business Central API. The authentication request was successful, and the app received a token, but here the flow stopped. API call returned the status 401 – Unauthorized.

When something is going wrong in an API client, Postman is the ultimate debugging tool. So I did the same setup in Postman, and – with the same result. Token is generated, but the Azure AD tenant refuses to accept it.


<error xmlns="http://docs.oasis-open.org/odata/ns/metadata">
    <code>Unauthorized</code>
    <message>The credentials provided are incorrect</message>
</error>

After checking the API request parameters for the fiftieth time, I concluded that something must be wrong with the token. Or rather – with the way I form the authorization request. The best start into finding out the reason is to check the structure of the token itself. Format of the OAuth2 access token follows the rfc 7519 JSON Web Token (JWT), which is nothing but a Base64-encoded JSON string with a signature. Therefore, it’s very easy to transform it into a human-readable form – just feed the value to a Base64 decoder, https://www.base64decode.org for example.


Be careful, though. Token contains all the information required to identify the resource which it authorizes. It is a key with a tag which has the address of the door this key opens written on it. Don’t send active tokens to unknown resources.

Best resources for OAuth token decoding which I use are https://jwt.io by Okta and https://jwt.ms which is recommended by Microsoft and can give some insights into tokens issued by the Microsoft identity platform.

A JWT object consists of three parts: header, payload, and the signature, each represented by a separate JSON object within the JWT structure.

The header (JOSE Header, or JSON Object Signing and Encryption) carries service information about the token itself:


  • typ: the token type, which is always “JWT” for OAuth access tokens.

  • alg: the signing algorithm which is used to sign the token and verify the signature

  • kid: thumbprint of the public key used to validate the signature

  • x5t: same as “kid”, but emitted only in v1.0 tokens, not used and left for compatibility only

This is a sample header of one of my tokens.

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "-WI3Q9nNR7bRouxmiZaSqrHZLew",
  "kid": "-WI3Q9nNR7bRouxmiZaSqrHZLew"
}

Together with the signature, the header provides the token recipient information sufficient to verify the token authenticity.

After the header comes the payload section which has a similar structure – it is a JSON object with a collection of JSON values called claims which convey the information about the token lifespan, the resource being accessed, authorization scope, and basically can contain any access details that developers want to add here.


{
  "aud": "https://api.businesscentral.dynamics.com",
  "iss": "https://sts.windows.net/2f809507-eb25-4d2f-aaf6-bf4dba6053cf/",
…
}

I won’t list the whole collection of registered JWT claims defined in the RFC – anyone interested can read the original document or Microsoft documentation that describes a specific implementation of JWT by the MS Identity Platform.

I only want to mention the first two claims highlighted above: “aud” and “iss”. The first, “aud” claim specifies the audience of the token – the identifier of the resource server. “iss” is the Security Token Issuer (STS). Base URI for the Azure STS is https://sts.windows.net, and this URI is followed by the tenant ID where the requested resource (Business Central API in my case) is located.

Why is this important for my story? Because the first thing I noticed when I decoded the token was a strange value of the audience claim


"aud": "00000002-0000-0000-c000-000000000000"

Some googling confirmed that this is indeed a wrong target audience for an access token, and closely correlated with access issues similar to what I faced. Long story short – after another round of googling and reading docs, I found a recommendation to add the intended audience in the resource request parameter. Postman has a token configuration parameter Resource under the Advanced Options tab where I entered my resource name.



And – miracle happened! The new token had the valid audience claim, and the API call succeeded.

It also worked in the Azure function app – adding the resource parameter fixed the bug, and the API connection finally created my sales orders in BC.



private static async Task<Token> GetAccessToken(HttpClient client)
{
    string baseAddress = 
      "https://login.microsoftonline.com/<Tenant ID>/oauth2/token";
    string grantType = "client_credentials";
    string clientId = "<Client ID>";
    string clientSecret = "<Secret>";
    string scope = "https://api.businesscentral.dynamics.com/.default";
    string resource = "https://api.businesscentral.dynamics.com";

    var form = new Dictionary<string, string>
    {
        {"grant_type", grantType},
        {"client_id", clientId},
        {"client_secret", clientSecret},
        {"scope", scope},
        {"resource", resource}
    };

    HttpResponseMessage tokenResponse =
        await client.PostAsync(baseAddress, new FormUrlEncodedContent(form));
    var jsonContent = await tokenResponse.Content.ReadAsStringAsync();
    Token tok = JsonConvert.DeserializeObject<Token>(jsonContent);

    return tok;
}

This is the function that requests the correct token, which does its work – authorizes the BC API request. And this could have been the end of the story, except that it’s not.


The correct solution


My googling took me to a documentation page which I already referenced above: Microsoft identity platform ID tokens. And in this page, I happened to find a bit which I overlooked at first.


ID tokens have differences in the information they carry. The version is based on the endpoint from where it was requested. While existing applications likely use the Azure AD endpoint (v1.0), new applications should use the "Microsoft identity platform" endpoint(v2.0).
v1.0: Azure AD endpoint: https://login.microsoftonline.com/common/oauth2/authorize
v2.0: Microsoft identity Platform endpoint: https://login.microsoftonline.com/common/oauth2/v2.0/authorize

And this was actually the solution to my problem. Remember, in the beginning of the post I wrote that this is a story about a typo that causes a lot of headache, but in the end enriches our knowledge?


My Azure function code and the Postman configuration were sending requests to the v1.0 endpoint and receiving a v1.0 access token. Once again quoting Microsoft documentation.

For ADAL (Azure AD v1.0) endpoint with a v1.0 access token (the only possible), aud=resource
For MSAL (Microsoft identity platform) asking an access token for a resource accepting v2.0 tokens, aud=resource.AppId
For MSAL (v2.0 endpoint) asking an access token for a resource that accepts a v1.0 access token (which is the case above), Azure AD parses the desired audience from the requested scope by taking everything before the last slash and using it as the resource identifier. Therefore, if https://database.windows.net expects an audience of https://database.windows.net/, you'll need to request a scope of https://database.windows.net//.default.

So finally, the correct solution was not to add the resource parameter to the request, but simply change the authorization endpoint to v2.0:


string baseAddress = 
      "https://login.microsoftonline.com/<Tenant ID>/oauth2/v2.0/token";

This endpoint does not support the resource parameter, so it must be also deleted in the code sample above:


    var form = new Dictionary<string, string>
    {
        {"grant_type", grantType},
        {"client_id", clientId},
        {"client_secret", clientSecret},
        {"scope", scope}
    };

Requests with the resource parameter instead (or along with) the scope will be rejected on the v2.0 endpoint.


Conclusion


Carefully check the endpoint from which you are requesting the OAuth token. V2.0 endpoint


https:// login.microsoftonline.com/<Tenant ID>/oauth2/v2.0/token

is preferred, and it identifies the target audience from the scope parameter. Resource parameter is not supported in v2.0.


If for some reason, you need to send the request to the v1.0 endpoint, don’t forget to include the resource URI in the request. Requests missing the resource identifier succeed and yield a valid token, but the token missing the audience claim is not of much use in practice, since it will be rejected by the target server.


To be continued


Yet the story doesn’t end here. My function app works, but the curiosity is not satisfied. During this dive into the token structure, I found that the v2.0 authorization endpoint actually issues v1.0 tokens despite what is said in the conclusion above. So the debugging night continued. More on the Microsoft identity platform tokens in the next episode.

1,031 views0 comments

Kommentare


bottom of page