×

Credentials

Utilizing the URL.FetchRequest and URL.FetchResponse sub-classes of the URL class, plug-ins can securely access network resources through the HTTPS protocol. Full documentation and examples are available. (LINK)

For those plug-ins targeting network resources, it may be useful to be able to store and retrieve credentials automatically when using these type of Omni Automation plug-ins. macOS, iOS, and iPadOS offer secure storage and access to login elements through the operating sytem KeyChain.

The Credentials class enables the storage of private username and password pairs in the system Keychain so that once stored, it will no longer be necessary for the host plug-in to prompt the user for credentials. Credential instances are tied to a single plug-in and single application, and may only be created in plug-ins when they are being loaded.

For example, when a PlugIn.Action is being created, you could use the following pattern to instantiate an instance of the Credentials class fo ruse in the plug-in:

Create an Instance


(() => { let credentials = new Credentials(); var action = new PlugIn.Action(function(selection) { // ... use the captured credentials ... }); return action; })();

Attempts to create Credential instances elsewhere will throw an error. Care should be taken to store instances in anonymous closures as above, and not pass them to or store them on other objects.

Credentials are keyed off a service identifier, which your plug-in can define however it likes.

Constructors

Instance Functions

Example Plug-In

The following example plug-in uses the URL.FetchRequest and URL.FetchResponse class with the Credentials class and the Form.Field.Password form element to present the interface for associating a set of credentials with the specified target server URL.

credentials-example-interface

The result of a successful execution of the example plug-in will be the logging of JSON content from a secure file hosted by this website, to the local Omni application console.

Once the credentials have been added to the Keychain application (below), further executions of the plug-in will not require user-interaction.

(⬇ see below ) The plug-in credentials entry in the Keychain application (macOS):

credentials-keychain

NOTE: To reset the credentials, you may either delete the Keychain element, or hold down the Control modifier key when selecting the plug-in from the Automation menu.

reset-credentials-confirmation

Selecting “Reset” will trigger the following code in the plug-in:

Remove Stored Service Credentials


credentials.remove(serviceTitle)

Running the Example Plug-In

This plug-in is designed to work with any Omni application.

Here are the account user and password for testing the plug-in:

Enter those when trying out the plug-in.

Credentials Example Plug-In
 

/*{ "type": "action", "targets": ["omnigraffle","omnifocus","omniplan","omnioutliner"], "author": "Otto Automator", "identifier": "com.omni-automation.all.credentials-example", "version": "1.5", "description": "This plug-in uses the Credentials class to create and retrieve log-in pairs. NOTE: to clear stored credentials, hold down Control modifier key when selecting plug-in from Automation menu.", "label": "Credentials Example", "shortLabel": "Credentials Example", "paletteLabel": "Credentials Example", "image": "key.fill" }*/ (() => { var serviceTitle = "conference-data"; var serviceURLString = "https://omni-automation.com/secure/conference-sessions.json" var credentials = new Credentials() const requestCredentials = async () => { try { OSname = app.platformName // CREATE FORM FOR GATHERING USER INPUT inputForm = new Form() // CREATE TEXT AND PASSWORD FIELDS nameField = new Form.Field.String( "accountName", "Name", null ) if(OSname !== "macOS"){ nameField.autocapitalizationType = TextAutocapitalizationType.None } passwordFieldA = new Form.Field.Password( "accountPassword", "Password", null ) if(OSname !== "macOS"){ passwordFieldA.autocapitalizationType = TextAutocapitalizationType.None } passwordFieldB = new Form.Field.Password( "accountPasswordCheck", "Re-Enter", null ) if(OSname !== "macOS"){ passwordFieldB.autocapitalizationType = TextAutocapitalizationType.None } // ADD THE FIELDS TO THE FORM inputForm.addField(nameField) inputForm.addField(passwordFieldA) inputForm.addField(passwordFieldB) // VALIDATE THE USER INPUT inputForm.validate = formObject => { accountName = formObject.values["accountName"] nameStatus = (accountName && accountName.length > 0) ? true:false accountPassword = formObject.values["accountPassword"] passwordStatus = (accountPassword && accountPassword.length > 0) ? true:false accountPasswordCheck = formObject.values["accountPasswordCheck"] passwordCheckStatus = (accountPasswordCheck && accountPasswordCheck.length > 0) ? true:false if(passwordStatus && passwordCheckStatus && accountPassword != accountPasswordCheck){ throw "Passwords do not match" } validation = (nameStatus && passwordStatus && passwordCheckStatus) ? true:false return validation } // PRESENT THE FORM TO THE USER formPrompt = "Enter account name (otto-automator) and password (omni-automation):" formObject = await inputForm.show(formPrompt, "Continue") // RETRIEVE FORM VALUES accountName = formObject.values["accountName"] accountPassword = formObject.values["accountPassword"] // STORE THE VALUES credentials.write(serviceTitle, accountName, accountPassword) // RETRIEVE CREDENTIALS OBJECT credentialsObj = credentials.read(serviceTitle) //--> {"user":"otto-automator","password":"omni-automation"} // PASS CREDENTIALS TO PROCESSING FUNCTION fetchData(credentialsObj) } catch(err){ if(!err.causedByUserCancelling){ new Alert(err.name, err.message).show() } } } const fetchData = async credentialsObj => { try { // CONSTRUCT ENCODED CREDENTIALS STRING accountName = credentialsObj["user"] accountPassword = credentialsObj["password"] data = Data.fromString(accountName + ":" + accountPassword) encodedCredentials = data.toBase64() // CONSTRUCT REQUEST request = URL.FetchRequest.fromString(serviceURLString) request.method = 'GET' request.cache = "no-cache" request.headers = {"Authorization": "Basic" + " " + encodedCredentials} // EXECUTE REQUEST response = await request.fetch() // PROCESS REQUEST RESULT responseCode = response.statusCode console.log("# RESPONSE CODE:", responseCode) if (responseCode >= 200 && responseCode < 300){ // PROCESS RETRIEVED DATA console.log(response.bodyString) new Alert("Data Retrieved", "The data has been logged in the console.").show() } else if (responseCode === 401){ throw { name: "Error 401", message: "Problem authenticating account with server." } } else { errorObj = new Error() errorObj.name = `Error ${responseCode}` errorObj.message = "There was a problem retrieving the data." throw errorObj } } catch(err){ new Alert(err.name, err.message).show() } } const action = new PlugIn.Action(function(selection, sender){ try { console.clear() // TO REMOVE CREDENTIALS HOLD DOWN CONTROL KEY WHEN SELECTING PLUG-IN if (app.controlKeyDown){ credentialsObj = credentials.read(serviceTitle) if(!credentialsObj){ throw { name: "Missing Credentials", message: "There are no stored credentials to remove." } } else { alertMessage = "Remove the stored credentials?" alert = new Alert("Confirmation Required", alertMessage) alert.addOption("Reset") alert.addOption("Cancel") alert.show(buttonIndex => { if (buttonIndex === 0){ console.log(`Removing Service “${serviceTitle}”`) credentials.remove(serviceTitle) console.log(`Service “${serviceTitle}” Removed`) } }) } } else { credentialsObj = credentials.read(serviceTitle) console.log("# CREDENTIALS READ") if (credentialsObj){ console.log("# FETCHING DATA") fetchData(credentialsObj) } else { console.log("# REQUESTING CREDENTIALS") requestCredentials() } } } catch(err){console.error(err.name, err.message)} }); action.validate = function(selection, sender){ // validation code return true }; return action; })();

 

Credentials Library

To better enable the use of credentials in multiple plug-ins, you may wish to create an Omni Automation library containing important credential-related functions that can be called from other plug-ins.

Omni Automation libraries are specialized plug-ins containing functions called from other plug-ins that contain statements to load the library and call its functions. Documentation regarding libraries can be found here.

The following example library contains the functions necessary to query the user to create a new set of credentials for a plug-in. It’s main function requestCredentials(serviceTitle, credentials) is called from other plug-ins to instantiate new credentials for the calling plug-in.

Credentials Library
 

/*{ "type": "library", "targets": ["omnigraffle","omnioutliner","omniplan","omnifocus"], "identifier": "com.omni-automation.all.credentials-library", "version": "1.0" }*/ (() => { const credentialsLib = new PlugIn.Library(new Version("1.0")); credentialsLib.requestCredentials = (serviceTitle, credentials) => { try { // CREATE FORM FOR GATHERING USER INPUT let inputForm = new Form() // CREATE TEXT FIELDS let nameField = new Form.Field.String( "accountName", "Name", null ) let passwordFieldA = new Form.Field.Password( "accountPassword", "Password", null ) let passwordFieldB = new Form.Field.Password( "accountPasswordCheck", "Re-Enter", null ) // ADD THE FIELDS TO THE FORM inputForm.addField(nameField) inputForm.addField(passwordFieldA) inputForm.addField(passwordFieldB) // VALIDATE THE USER INPUT inputForm.validate = formObject => { let accountName = formObject.values["accountName"] let nameStatus = (accountName && accountName.length > 0) ? true:false let accountPassword = formObject.values["accountPassword"] let passwordStatus = (accountPassword && accountPassword.length > 0) ? true:false let accountPasswordCheck = formObject.values["accountPasswordCheck"] let passwordCheckStatus = (accountPasswordCheck && accountPasswordCheck.length > 0) ? true:false if(passwordStatus && passwordCheckStatus && accountPassword != accountPasswordCheck){ throw "Passwords do not match" } let validation = (nameStatus && passwordStatus && passwordCheckStatus) ? true:false return validation } // PRESENT THE FORM TO THE USER let formPrompt = `Name and password for ${serviceTitle}:` let formPromise = inputForm.show(formPrompt, "Continue") formPromise.then(function(formObject){ // RETRIEVE FORM VALUES let accountName = formObject.values["accountName"] let accountPassword = formObject.values["accountPassword"] // UPDATE THE PASSED CREDENTIALS credentials.write(serviceTitle, accountName, accountPassword) }) } catch(err){ console.log(err.message) } } return credentialsLib; })();

The following example plug-in demonstrates how to incorporate the library in the plug-in design:

Call Credentials Library
 

/*{ "type": "action", "targets": ["omnigraffle","omnioutliner","omniplan","omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation-all.call-credentials-library", "version": "1.1", "description": "Calls the Credentials Library.", "label": "Call Credentials Library", "shortLabel": "Call Credentials Library", "paletteLabel": "Call Credentials Library", "image": "key.fill" }*/ (() => { let credentials = new Credentials() let serviceTitle = "TEST-SERVICE" function processWithCreds(credentialsObj){ console.log("PROCESSING USING CREDENTIALS") // PROCESS USING CREDENTIALS } var action = new PlugIn.Action(function(selection, sender){ // TO REMOVE CREDENTIALS HOLD DOWN CONTROL KEY WHEN SELECTING PLUG-IN if (app.controlKeyDown){ let credentialsObj = credentials.read(serviceTitle) if(!credentialsObj){ let alertMessage = "There are no stored credentials to remove." new Alert("Missing Resource", alertMessage).show() } else { let alertMessage = "Remove the stored credentials?" let alert = new Alert("Confirmation Required", alertMessage) alert.addOption("Remove") alert.addOption("Cancel") alert.show(buttonIndex => { if (buttonIndex === 0){credentials.remove(serviceTitle)} }) } } else { let credentialsObj = credentials.read(serviceTitle) if (credentialsObj){ console.log("PROCESSING USING CREDENTIALS") processWithCreds(credentialsObj) } else { console.log("REQUESTING CREDENTIALS") let aPlugin = PlugIn.find("com.omni-automation.all.credentials-library") let credLib = aPlugin.library("all-credentials-library") credLib.requestCredentials(serviceTitle, credentials) credentialsObj = credentials.read(serviceTitle) processWithCreds(credentialsObj) } } }); action.validate = function(selection, sender){ return true }; return action; })();

And version of the previous script without the comments. To use, edit the items in RED:

Call Credentials Library Template


/*{ "type": "action", "targets": ["omnigraffle","omnioutliner","omniplan","omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation-all.call-credentials-library", "version": "1.0", "description": "Calls the credentials library.", "label": "Call Credentials Library", "shortLabel": "Call Credentials Library", "paletteLabel": "Call Credentials Library", "image": "key.fill" }*/ (() => { let credentials = new Credentials() let serviceTitle = "TEST-SERVICE" function processWithCreds(credentialsObj){ // PROCESS USING CREDENTIALS } let action = new PlugIn.Action(function(selection, sender){ // TO REMOVE CREDENTIALS HOLD DOWN CONTROL KEY WHEN SELECTING PLUG-IN if (app.controlKeyDown){ let alertMessage = "Remove the stored credentials?" let alert = new Alert("Confirmation Required", alertMessage) alert.addOption("Remove") alert.addOption("Cancel") alert.show(buttonIndex => { if (buttonIndex === 0){credentials.remove(serviceTitle)} }) } else { let credentialsObj = credentials.read(serviceTitle) if (credentialsObj){ processWithCreds(credentialsObj) } else { let credLibID = "com.omni-automation.all.credentials-library" let aPlugin = PlugIn.find(credLibID) let credLib = aPlugin.library("all-credentials-library") credLib.requestCredentials(serviceTitle, credentials) credentialsObj = credentials.read(serviceTitle) processWithCreds(credentialsObj) } } }); action.validate = function(selection, sender){ return true }; return action; })();