I suggested many times in the past to every partners I met and to all the attendees to my AL courses for Microsoft Italy that the way to store sensitive data in Dynamics 365 Business Central must be the Isolated Storage usage and not custom tricks on custom tables (see here) or using the old Service Password table (deprecated in Wave 2 release).
In this days, during an extension’s code checking for a partner where I had previosuly suggested those things, I see a license control checking like the following:
- They have a function for downloading a license key (call to an external service)
- This license key is saved into the Isolated Storage with Datascope = Module (visible to all the extension
- They have a codeunit with a subscriber to the OnAfterLogInEnd event with the following code:
[EventSubscriber(ObjectType::Codeunit, Codeunit::LogInManagement, 'OnAfterLogInEnd', '', false, false)] local procedure CheckLicense() var LicenseKeyValue: Text; validLicense: Boolean; begin validLicense := false; if IsolatedStorage.Get('LicenseKey',DataScope::Module,LicenseKeyValue) then if not LicenseCheck(LicenseKeyValue) then error('Your extension license is not valid') else //Valid license found validLicense := true else if CheckLicensingLimit() then error('You don''t have a license for this extension.'); end; local procedure LicenseCheck(LicenseKey: Text): Boolean begin //custom license check here end; local procedure CheckLicensingLimit(): Boolean begin //if extension's installation date + N days are overdue, the extension cannot be used again exit false; end;
In this event subscriber, they search for a license key variable in the Isolated Storage. If the variable is found, a license check is performed (by calling the LicenseCheck procedure) and an error is thrown if the license is not valid.
At the same manner, an error is thrown also if the license key is not found in the Isolated Storage (the extension was never licensed by the user). For this task, they save in the Isolated Storage via an Install codeunit the date of the extension’s installation, then if this date + N limit days are overdue, an error is thrown (the customer has N days to acquire a license for this extension).
All is good as logic, but perform this license check in the OnAfterLogInEnd event and throwing an error here is absolutely to avoid. If your extension’s license check is failed (missing license or not valid license) you cannot login to that tenant anymore!
My suggestion: throw an error from features of your extension or disable your extension’s business logic, but don’t throw errors from standard events in the application. N extensions can be installed on a tenant (you’re not alone) and you can’t block others if your extension’s license is not valid.
Another important thing to remember is the changements in secret management from Dynamics 365 Business Central Wave 2 release (explained here) related to Isolated Storage:
- In Dynamics 365 Business Central SaaS, sensitive data stored in the Isolated Storage are always encrypted.
- In Dynamics 365 Business Central on-premise, encryption is controlled by the end user (via the Data Encryption Management page), and here:
- If encryption is turned on, a secret stored in the Isolated Storage is automatically encrypted.
- A secret that was inserted while encryption was turned off will remain unencrypted if encryption is turned on.
- If you turn off encryption, the secret will be decrypted.
Accordingly to these changes, if you have an extension that works for Dynamics 365 Business Central SaaS and on-premise (same code) and you’re using the Isolated Storage to store secrets, you need to check if the encryption is enabled (always true for SaaS) and then save the secret accordingly.
So, a function that saves a license key to the Isolated Storage and that works for Dynamics 365 Business Central SaaS and on-premise will be as follows:
local procedure SaveLicense() var licenseKeyValue: Text; begin if not EncryptionEnabled() then IsolatedStorage.Set('LicenseKey',licenseKeyValue,DataScope::Module) else IsolatedStorage.SetEncrypted('LicenseKey',licenseKeyValue,DataScope::Module) end;