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.
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;
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
Hi Nitin, thanks for your Message, this is just a little helper method. I’ve added the method GetJsonToken() to https://www.j3ns.de/d365-business-central/oauth-2-0-in-al/#Getting_the_token