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
new Credentials() → (Credentials) • Creates a new Credentials instance for the currently loading plug-in. Throws an error if called outside of plug-in loading.
Instance Functions
read(service: String) → (Object or null) • Looks up the current credentials for a given service identifier. If credentials have previously been stored, an object will be returned containing user and password properties.
write(service: String, username: String, password: String) → (Credential) • Creates or updates an existing credential, storing the username and password for this service securely in the Keychain.
remove(service: String) → ( ) • Deletes any currently stored credentials for the specified service
- (See URL.Bookmark section:)
readBookmark(service: String) → (URL.Bookmark or null) • Reads the entry for the given service identifier and attempts to return it as a URL.Bookmark, or null if no such entry exists.
writeBookmark(service: String, bookmark: URL.Bookmark) → ( ) • Stores the URL.Bookmark persistently for later access.
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.
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.
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.
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:
- User: otto-automator
- Password: omni-automation
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
}
const 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 COLOR:
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
}
const action = new PlugIn.Action(async 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;
})();