Sunday, February 20, 2022

OAuth Bearer Token Business Central

Problem Description

The current Microsoft implementation of Web Services authorization is a giant security hole. Anyone who knows the current NON-expiring user id and the access key should be able to read and tamper with the accounting data. Moreover, anyone with sufficient access in user card could generate a new access key.



Microsoft has deprecated the feature and in future releases will not make it available to the Cloud installations of Business Central. (On-premise installation could still use it though for the foreseeable future.)

The OAuth token has a limited lifetime and needs to be constantly refreshed. We could automatically refresh the token - forever. (Only generating the first token requires Azure active directory login.)

(Should we go to prod before Microsoft totally removes the current Web service Access Key method, we need to wipe out any existing web access keys used by the consultants or internal users.)

Register Azure app as described here in detail:

https://www.youtube.com/watch?v=lCzqg2N0vbA 

( Microsoft’s documentation is absurdly insufficient in implementing the plumbing - in this article they the general road map is listed:

https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/authenticate-web-services-using-oauth 
)

The already-registered app is shown below. Note that you don’t need to ever create the actual app. The registration is sufficient - the registration is a proxy to get the access and refresh tokens.

https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade 

In Prod you may have to either alter the registered app’s parameters to point to the Prod URLs or create a new registered app with the new Prod URLs.



Get the first token from PostMan:

Set the parameters and click on “Get New Access Token”. (I could provide you the Postman parameters on Teams. Basically they are derived from the registered app.)
Active Directory login will pop up and you need to supply your credentials.



Copy and paste the access token. and then scroll down and copy and paste the refresh token.

You will put the tokens into a SQL server database. But before executing the SQL, stop the instance of the PDP OAuth Token refresher scheduled task shown below.

update [dbo].[BusinessCentralOAuthToken] set [access_token]=‘YourNewAccessToken’,
[refresh_token]=‘yournewRefreshToken’

Token Refresher Micro Service

// refresh token and the acces stoken are sent as params in a custom object instance of type  BusinessCentralOAuthToken_Result       

private static void UpdateNewTokenFromRefreshToken(BusinessCentralOAuthToken_Result oInputModel)

        {


            // https://docs.microsoft.com/en-us/linkedin/shared/authentication/programmatic-refresh-tokens



            HttpResponseMessage responseMessage;

            using (HttpClient client = new HttpClient())

            {



// Before it expires, refresh the close-to-expiration token using the latest refresh token

                HttpRequestMessage tokenRequest = new HttpRequestMessage(HttpMethod.Post, oInputModel.AccessTokenURL);


                HttpContent httpContent = new FormUrlEncodedContent(

                        new[]

                        {

                                        new KeyValuePair<string, string>("grant_type", "refresh_token"),

                                        new KeyValuePair<string, string>("refresh_token", oInputModel.refresh_token),


                                        new KeyValuePair<string, string>("client_id",oInputModel.ClientID),

                                        new KeyValuePair<string, string>("client_secret",oInputModel.ClientSecret),

                        });

               // let the app server provide the date, in case the clocks do NOT match

               tokenRequest.Content = httpContent;

                responseMessage = client.SendAsync(tokenRequest).Result;

            }




            string oJsonReponse = responseMessage.Content.ReadAsStringAsync().Result;

            ResponseModel oModel = JsonConvert.DeserializeObject<ResponseModel>(oJsonReponse);

            if (string.IsNullOrEmpty(oModel.access_token))

            {

                ReportError(new Exception("access_token and refresh_token is no longer valid. Update [dbo].[PDPBusinessCentralOAuthToken] set access_token, refresh_token"), "manually (PostMan) generate a new access and refresh token");

            }

            else

            {

                // write the Model to DB with the new acsess token and the new refresh token

                // this database write code is not shown. Create a table to store the token url, access token, and refresh token and last update time



// this is a verification method to read the latest access token                

BCOAuthBearerToken(oModel.access_token);


            }


        // this is a verification call to insure refresh of a new token worked
        static void BCOAuthBearerToken(string access_token)
        {
            var client = new HttpClient();
            client.SetBearerToken(access_token);

            string result = client.GetStringAsync(ConfigurationManager.AppSettings["BCPlaygroundOdataChartofAccounts"]).Result;
        }


OAuthMicroService

All apps get the latest token from the database through a call to OAuthMicroService project which writes the latest refreshed token into the database

    private static string GetOAuthAccessToken()
    {
        var client = new HttpClient();

        Task<string> resultOrder = client.GetStringAsync(ConfigurationManager.AppSettings["OAuthMicroService"]);

        return resultOrder.Result;

// or get it from the database:
/*IntegrationEntities model = new IntegrationEntities();
var lst = model.usp_GetBusinessCentralOAuthToken().ToList();
return lst[0].access_token;*/
}


Since the token expires every hour or so by Microsoft, we have to get the common token from DB, and NOT form config file(s), and refresh it 24/7 .

(Extending the expiration period of the access token was not possible with an Azure cmdlet. Also the refresh token has a much longer expiration period than the access token. However since we are constantly refreshing with the BusinessCentralOAtuhTokenRefresher
task, our taken would be up-to-date indefinitely. I’ve tested this for days running though automation. )



No comments: