OAuth 2.0 / JWT in AL

A lot of webservices use token-based authentication today. Among many other advantages, the authentication token has a certain lifetime, which results in the fact that the re-authentication must happen only very irregularly. This minimizes the authentication requests as the token could be reused. A standard protocol for this type of authentication flow is OAuth / JWT, which I would like to implement here in a quite simple example… I have already used this little pattern in several different API implementations.

Here’s a How-To, if you would like to use it in your Dynamic 365 Business Central extension.
There are few steps to authenticate using OAuth 2.0 / JWT in AL…

Attention: This is not one of the more complex authentication flow(s) used by Microsoft! The flow described in here is a quite simple implementation but widely used by many providers for their web services.

OAuth 2.0 in AL

Build an API setup

First, we build a setup to store our credentials and the token. We will talk about the procedure GetAuthenticationToken() later. It has a couple of fields.

table 79000 "Api Setup"
{
    DataClassification = ToBeClassified;

    fields
    {
        field(1; Code; Code[10])
        {
            DataClassification = CustomerContent;
        }
        field(5; "API URL"; Text[200])
        {
            DataClassification = CustomerContent;
        }
        field(10; "API User"; Text[150])
        {
            DataClassification = CustomerContent;
        }
        field(15; "API Password"; Text[150])
        {
            DataClassification = CustomerContent;
        }        
        field(20; "API Token"; Blob)
        {
            DataClassification = SystemMetadata;
        }
        field(25; "Token valid until"; DateTime)
        {
            DataClassification = SystemMetadata;
        }
    }

    keys
    {
        key(PK; Code)
        {
            Clustered = true;
        }
    }

    var
        ApiWebservice: Codeunit "Api Webservice";

    procedure GetAuthenticationToken(ForceRenewal: Boolean): Text
    begin
        //Hook into the webservice codeunit
        exit(ApiWebservice.GetAuthenticationToken(Rec, ForceRenewal));
    end;
}

Getting the token

In the next step, we have to get a fresh token from the “authentication server” of the service. Usually this is a simple endpoint like {SERVICE_ENDPOINT}/oauth/token. So we build a request using the native Business Central AL webclient objects. You might wrap these procedures into a “API Webservice ” codeunit.

local procedure GetFreshAuthenticationToken(ApiSetup: Record "API Setup"; var TokenExpiry: DateTime): Text
var
    AuthPayload: Text;   
    ResponseText: Text;
    TokenResponseText: Text;
    JObjectResult: JsonObject;
    JObjectRequest:  JsonObject;
    WebClient: HttpClient;
    RequestHeader: HttpHeaders;
    ResponseMessage: HttpResponseMessage;
    RequestMessage: HttpRequestMessage;
    RequestContent: HttpContent;
    TokenOutStream: OutStream;
begin
    //Create webservice call
    RequestMessage.Method := 'POST';
    RequestMessage.SetRequestUri(ApiSetup.URL + 'login');

    //Create webservice header
    RequestMessage.GetHeaders(RequestHeader);

    //Payload needed? This might as well be a different implementation!
    //It's just an example where the credentials are stored as a json payload

    //Create json payload
    JObjectRequest.Add('client_id', ApiSetup."API User");
    JObjectRequest.Add('client_secret', ApiSetup."API Password");
    JObjectRequest.WriteTo(AuthPayload);

    //Get Request Content
    RequestContent.WriteFrom(AuthPayload);

    RequestContent.GetHeaders(RequestHeader);
    RequestHeader.Remove('Content-Type');
    RequestHeader.Add('Content-Type', 'application/json');

    RequestMessage.Content := RequestContent;

    //Send webservice query
    WebClient.Send(RequestMessage, ResponseMessage);

    if ResponseMessage.IsSuccessStatusCode() then begin
        ResponseMessage.Content().ReadAs(ResponseText);

        if not JObjectResult.ReadFrom(ResponseText) then
            Error('Error Read JSON');
          
        TokenResponseText := GetJsonToken(JObjectResult, 'access_token').AsValue().AsText();
        TokenExpiry := GetJsonToken(JsonObjectResult, 'expiry_date').AsValue().AsDateTime();      
       
    end else
        Error('Webservice Error');
    
    exit(TokenResponseText);
end;

//JSON Helper
procedure GetJsonToken(JsonObject: JsonObject; TokenKey: Text) JsonToken: JsonToken;
begin
     if not JsonObject.Get(TokenKey, JsonToken) then
          Error(StrSubstNo('Token %1 not found', TokenKey));
end;

Now, that we have obtained a valid token, we will persist it in a Blob field of our setup. For sure, you must create some kind of handling in case of errors instead of my hardcodes ones. This way, all users obtain the token from the setup, if it is valid. The moment the token’s validation has expired, we fetch a new one.

The authentication procedure

As we know how to get and use a token now, we can automate this into a little procedure. This automates the mentioned lifetime-check of the token and “always” returns a valid one. This is the method we hook into from our API setup table in the first step. Now every user can get a valid OAuth token from ApiSetup.GetAuthenticationToken(). The ForceRenewal parameter should be self-explaining 🙂

//Get a valid authentication token from setup. If token-age < expiry, get from blob, otherwise call API
procedure GetAuthenticationToken(ApiSetup: Record "API Setup"; ForceRenewal: Boolean): Text
var
    TokenResponseText: Text;
    TokenExpiry: DateTime;
    TokenOutStream: OutStream;
    TokenInStream: InStream;
    AuthPayload: Text;
begin
    if (ApiSetup."Token valid until" <= CurrentDateTime()) or ForceRenewal then begin
        //Get fresh Token 
        TokenResponseText := GetFreshAuthenticationToken(ApiSetup, TokenExpiry);
        
        //Write Token to Blob
        ApiSetup."API Token".CreateOutStream(TokenOutStream);
        TokenOutStream.WriteText(TokenResponseText);
        
        //Calculate the expriation date of the token. 
        //Should be defined by the API or even delivered in the response
        if TokenExpiry <> 0DT then
            ApiSetup."Token valid until" := TokenExpiry;
        ApiSetup.Modify();
    end else begin
        ApiSetup.CalcFields("API Token");

        //Read Token from Blob
        ApiSetup."API Token".CreateInStream(TokenInStream);
        TokenInStream.ReadText(TokenResponseText);
    end;

    //Return the token
    exit(TokenResponseText);
end;

Authenticate using the token

This is probably the easiest way to authenticate. We simply need to get the token from our setup and use in in a authentication header called “Bearer”.

Authorization: Bearer 123456789

In AL, an example call might look like this:

//OAuth using JWT in AL
//Request message using bearer token
local procedure CallWebservice(ApiSetup: Record "API Setup")
var
    RequestMessage: HttpRequestMessage;
    RequestContent: HttpContent;
    RequestHeader: HttpHeaders;
begin
    RequestMessage.Method := 'GET'; //or whatever you need
    RequestMessage.SetRequestUri(ApiSetup.URL + '/apifunction123');
    RequestMessage.GetHeaders(RequestHeader);

    //Use authentication token from setup
    RequestHeader.Add('Authorization', StrSubstNo('Bearer %1', ApiSetup.GetAuthenticationToken(false)));

   //Implement your request...
end;

... is a technical consultant and developer at Comsol Unternehmenslösungen AG in Kronberg/Taunus. Major tasks are the architecture and implementation of complex, usually cross-system applications in and around Microsoft Dynamics 365 Business Central.

2 comments

  1. Hi,
    I tried to implement the same code as you described above, but there is one Error I am facing with variable you are using JsonMgt, JsonMgt.GetJsonToken(

    Can you please tell me how to deal with it?

    Thanks

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *