Authentication with a token has at least two basic steps:
Plus the token usually expires after some time and must be renewed.
Let's have a simple example of the POST API authorization service on URL https://test.api.com/v3/Session. This service expects e-mail and password in JSON body:
and returns the access token in JSON response body:
The token expires after 1 day and must be renewed.
Tip: Access token requests in OAuth2 standard are using x-www-form-urlencoded body instead of JSON. To set it up, see Request Body Types guideline.
Access tokens are stored in Connections table which means that you have to create an Entity for them first.
Firstly add the Session
entity to Entity ID RWU
enum:
enumextension 50001 "Test Entity ID RWU" extends "Entity ID RWU"
{
value(50001; "TestProvider_Session")
{
Caption = 'Session (Test Provider)';
}
}
Now select your provider from the list and open Entities:
Create new Session
entity for storing the connections and tokens. Select the entity enum ID in Entity ID field :
Don't forget to Release the entity after you're done.
First add the Refresh Token
operation to Operation ID RWU
enum:
enumextension 50002 "Test Operation ID RWU" extends "Operation ID RWU"
{
value(50007; "TestProvider_RefreshToken")
{
Caption = 'Refresh Token (Test Provider)';
}
}
Now open the provider operations with Operations from the menu and create a new operation called Refresh Token
:
POST
./Session
in the Endpoint field – i.e. the part of the URL without Provider URL (see Setting up API Provider).Open
, meaning you can update the operation.Tip: This scenario assumes that the authorization service has the same base URL as the rest of provider operations. This may not always be the case. But if the authorization service has different base URL, just make a second RESTwithUS provider for it or use RESTwithUS Variables in provider URL.
Open the operation request body with Request Body action. Then import the JSON example with Payload / Import Payload function and finish some details:
For e-mail and password nodes fill in some variable names into Object Name column. You can then set the values of these variables in provider settings or even set them with code – see the guideline Using RESTwithUS Variables for details.
Open the operation response body with Response Body action. Then import the JSON example with Payload / Import Payload function.
email
and token
nodes, so you can safely delete unneeded JSON nodes like id
.The final response body schema can look like this:
Let's now run the operation – first in an "easy" mode assuming, that you are connecting always with the same user and password and they are stored in variables ApiUser
and ApiPassword
on a provider level:
For a refresh token function use following code:
procedure RefreshToken() _Token : Text
var
APIScriptRWU: Codeunit "API Script RWU";
EntityID: Enum "Entity ID RWU";
ProviderID: Enum "Provider ID RWU";
OperationID: Enum "Operation ID RWU";
ExpirateDateTime: DateTime;
begin
//Try to get valid token from connection
_Token := APIScriptRWU.TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, '');
if _Token <> '' then
exit(_Token);
//Start operation to get new access token, if it expired or none exists
APIScriptRWU.INIT(OperationID::TestProvider_RefreshToken);
APIScriptRWU.ENDPOINT(ProviderID::TestProvider, OperationID::TestProvider_RefreshToken);
APIScriptRWU.EXECUTE();
//Save the token with code and set the expiration date and time after 1 day
Token := APIScriptRWU.GET_RESPONSE_VALUE_BY_JPATH('$.token');
ExpirationDateTime := CreateDateTime(Today, Time) + (1440 * 60000);
APIScriptRWU.CREATE_TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, '', _Token, DT2Date(ExpirationDateTime), DT2Time(ExpirationDateTime));
//Clean batch entries afterwards for security reasons
APIScriptRWU.CLEAN_BATCH_ENTRIES();
//Get the new access token from a connection and return it
_Token := APIScriptRWU.TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, '');
if _Token = '' then
Error('Failed to retrieve authentication token.');
end;
If you are creating unique access tokens for different users, you need to make some changes. First pass the user's authentication e-mail to the function as a parameter:
procedure RefreshToken(_AuthenticationEmail : Text) _Token : Text
Then you must modify the part, where you save the token: Pass the authentication e-mail as token unique identifier.
APIScriptRWU.CREATE_TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, _AuthenticationEmail, _Token, DT2Date(ExpirationDateTime), DT2Time(ExpirationDateTime));
Next you need to modify all TOKEN
function calls and filter the available tokens by the e-mail.
_Token := APIScriptRWU.TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, _AuthenticationEmail);
And lastly you have to modify the API operation and pass the e-mail and password to it from code:
//Start operation to get new access token
APIScriptRWU.INIT(OperationID::TestProvider_RefreshToken);
APIScriptRWU.ENDPOINT(ProviderID::TestProvider, OperationID::TestProvider_RefreshToken);
//Pass e-mail and password for current user (obviously you would first need to get his password somehow)
if _AuthenticationEmail <> '' then begin
APIScriptRWU.ADD_VARIABLE('/email','ApiUser',_AuthenticationEmail);
APIScriptRWU.ADD_VARIABLE('/password','ApiPassword',Password);
end;
//Run the operation
APIScriptRWU.EXECUTE();
Tokens are maintained on an Entity level. To check them select your API Provider and choose Entities from the menu:
As you can see, we have one token entry here. Click on the number to see the details:
APIScriptRWU.TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, 'test@user.com')
.If you need further details, you can check them in Batch Entries. Go to Operations, select the operation from the list and open Batch Entries from the menu:
Here you can see all the details about the operation run like:
root
node value.Now that you have successfully retrieved an access token, you can start to use it in API operations. Tokens are usually sent in request headers, so you first need to add them there. Select your operation from the list, open Related / Headers and create new line for the token header there:
You would need to slightly modify your operation calls code too:
//Get valid token
UserToken := RefreshToken();
//Start the operation and pass the token to Token variable in headers
APIScriptRWU.INIT(OperationID::TestProvider_GetAllCustomers);
APIScriptRWU.ENDPOINT(ProviderID::TestProvider,OperationID::TestProvider_GetAllCustomers);
APIScriptRWU.ADD_VARIABLE('','Token',UserToken);
APIScriptRWU.EXECUTE();
Tokens can contain sensitive information which in some cases allow full access to the API service, so the information must be protected accordingly. RESTwithUS is always saving the token values to Isolated Storage
, but there are still some security options:
enumextension 50001 "Test Entity ID RWU" extends "Entity ID RWU"
{
value(50001; "TestProvider_Session")
{
Caption = 'Session (Test Provider)';
Implementation = "IEntityId RWU" = "Entity ID RWU";
}
}
Lastly, the information should not be available anywhere in the database or simple text files.
This means that at least in production environment you need to:
If you need to save the token with code, add following lines after the operation execution.
//Save the token with code and manually set the expiration date and time after 10 minutes
Token := APIScriptRWU.GET_RESPONSE_VALUE_BY_JPATH('$.token');
ExpirationDateTime := CreateDateTime(Today, Time) + (10 * 60000);
APIScriptRWU.CREATE_TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, 'optional-token-identifier', _Token, DT2Date(ExpirationDateTime), DT2Time(ExpirationDateTime));
//Clean batch entries afterwards
APIScriptRWU.CLEAN_BATCH_ENTRIES();
Tip: If the token should not expire, set the expiration date to
0D
and time to0T
.
Please note, that this way is still vulnerable and the token value can be retrieved from another extension in the same environment.
For the best security measures you need to meet two requirements:
Isolated Storage
(saving and retrieving token value) must be done in your extension only.First you need to add your custom implementation of IEndityId RWU
interface.
codeunit 4075374 "Entity ID Session RWU" implements "IEntityId RWU"
{
[NonDebuggable]
procedure GetToken(_CallerModuleInfo: ModuleInfo; _ProviderIDEnum: Enum "Provider ID RWU"; _EntityIDEnum: Enum "Entity ID RWU"; _TokenDescription: Text; _TokenKey: Text) _Token: Text
var
begin
exit (_TokenKey);
/*
//Function returns only isolated storage token key.
//To get the actual key, use code below
if not IsolatedStorage.Contains(_TokenKey, DataScope::Company) then
exit('');
IsolatedStorage.Get(_TokenKey, DataScope::Company, ReturnedToken);
*/
end;
[NonDebuggable]
procedure GetTokenPreview(_TokenKey: Text) _Token: Text
var
MappingMgtRWU: Codeunit "Mapping Management RWU";
begin
if not IsolatedStorage.Contains(_TokenKey, DataScope::Company) then
exit('');
IsolatedStorage.Get(_TokenKey, DataScope::Company, _Token);
_Token := MappingMgtRWU.MaskSensitiveInfo(_Token);
end;
[NonDebuggable]
procedure SetToken(_ProviderIDEnum: Enum "Provider ID RWU"; _EntityIDEnum: Enum "Entity ID RWU"; _ExternalDescriptionOrEmpty: Text; _Value: Text): Text
var
LocAPIEndpointRWU: Record "API Endpoint RWU";
LocAPIProviderRWU: Record "API Provider RWU";
MappingMgtRWU: Codeunit "Mapping Management RWU";
TokenKey: Text;
begin
LocAPIProviderRWU := LocAPIProviderRWU.GetAPIProviderByProviderIDEnum(_ProviderIDEnum);
LocAPIEndpointRWU := LocAPIEndpointRWU.GetAPIEndpointByEntityIDEnum(LocAPIProviderRWU.ID, _EntityIDEnum);
TokenKey := MappingMgtRWU.CreateIsolatedStorageKey(LocAPIEndpointRWU.Description, _ExternalDescriptionOrEmpty);
IsolatedStorage.Set(TokenKey, _Value, DataScope::Company);
exit (TokenKey);
end;
}
Then modify the entity enum extension, which will implement your custom interface. This ensures, that all Isolated Storage
operations with tokens are done in the context of your extension.
enumextension 50001 "Test Entity ID RWU" extends "Entity ID RWU"
{
value(50001; "TestProvider_Session")
{
Caption = 'Session (Test Provider)';
Implementation = "IEntityId RWU" = "Entity ID Session RWU";
}
}
Lastly modify the RefreshToken
function in a following way and make it a local function, that will be called just from your integration codeunit. This way you ensure, that the token can't be retrieved from another extension in the environment.
var
APIScriptRWU: Codeunit "API Script RWU";
EntityID: Enum "Entity ID RWU";
ProviderID: Enum "Provider ID RWU";
OperationID: Enum "Operation ID RWU";
local procedure GetTokenValue(_ExtDescription: Text) _Token: Text
var
TokenKey: Text;
begin
//Try to get valid token key from database
TokenKey := APIScriptRWU.TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, _ExtDescription);
//Function will just return a key for token Isolated Storage and you must get it from there
if TokenKey <> '' then
if IsolatedStorage.Contains(TokenKey, DataScope::Company) then
IsolatedStorage.Get(TokenKey, DataScope::Company, _Token);
end;
local procedure RefreshToken() _Token : Text
var
ExpirationDateTime: DateTime;
begin
//Try to get valid token
_Token := GetTokenValue('test@user.com');
if _Token <> '' then
exit;
//Start operation to get new access token, if it expired or none exists
APIScriptRWU.INIT(OperationID::TestProvider_RefreshToken);
APIScriptRWU.ENDPOINT(ProviderID::TestProvider, OperationID::TestProvider_RefreshToken);
APIScriptRWU.EXECUTE();
//Save the token with code and manually set the expiration date and time after 10 minutes
//(This step is optional if you are saving the token value during response body processing.)
_Token := APIScriptRWU.GET_RESPONSE_VALUE_BY_JPATH('$.token');
ExpirationDateTime := CreateDateTime(Today, Time) + (10 * 60000);
APIScriptRWU.CREATE_TOKEN(ProviderID::TestProvider, EntityID::TestProvider_Session, 'test@user.com',_Token, DT2Date(ExpirationDateTime), DT2Time(ExpirationDateTime));
//Clean batch entries afterwards
APIScriptRWU.CLEAN_BATCH_ENTRIES();
//Get the new access token from Isolated Storage and return it
_Token := GetTokenValue('test@user.com');
if _Token = '' then
Error('Failed to retrieve authentication token.');
end;
This is an optional, legacy way to save the token by settings in response body. It requires less code, but is not so flexible and requires you to map the tokens to existing Business Central table.
Open the operation response body with Response Body action. Then import the JSON example with Payload / Import Payload function and finish some details:
root
node set the Type to Table
and select the User
table in Object ID field. Select the Session
entity in Entity ID field, too.email
node set the Type to Field
and select the Authentication Email
field in Object ID column.token
node contains new access token. To save it in the connections, set Mapping Type to Token
and select the Session
entity in Entity ID column.External ID
node, which contains the unique identifier of a user in an external system. Plus in this example you will store the user's e-mail in connection Ext. Description, so mark the email
node with Mapping Type Description
.Now let's set up some node details. First select the token
node and open Home / Details:
In the Mapping tab set the token expiration time in minutes, so it will be 1440
for us (60 minutes * 24 hours).
And now select the root
node, open the node details and go to Connection Management tab:
Create or Update
, because you want to update the connection with each token refresh (the token is saved to the connection).WHERE(Authentication Email=FILTER({{!Field11}}))
. The {{!Field11}}
variable will be replaced with a value of email
node, since you mapped it to Business Central field 11
Authentication Email.