Using a Self Service Policy to Grant End Users a Secure Token

Occasionally end users may end up without a secure token. This attribute is required to enable FileVault on any macOS device. Additionally, it is required for the end user to install updates on Apple silicon devices (this is a little complicated but for the purpose of this post I am ignoring volume ownership, as it is functionally equivalent to secure token in this respect).

Use the following script in a Self Service policy to grant the end user a secure token. This script will check if the currently logged in user has a secure token. If so, a notification informs them that no action is required.

If the currently logged in user does not have a secure token you will be guided through the process to grant one. In order to grant a secure token to a user without one, an account with a secure token must be used. The script will find all secure token users on the system and list them for you. Select an account that you already know the password for.

Next you will be prompted for the existing secure token user’s password. This is required to grant the token to other users

Then, you will be prompted for the end user’s password to complete the process.

Finally, the script will check if a bootstrap token is escrowed with MDM, and escrows the token if needed.

The script is available below:

#!/bin/sh
# Set the icons and branding
selfServiceBrandIcon="/Users/$3/Library/Application Support/com.jamfsoftware.selfservice.mac/Documents/Images/brandingimage.png"
fileVaultIcon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FileVaultIcon.icns"
if [[ -f $selfServiceBrandIcon ]]; then
brandIcon="$selfServiceBrandIcon"
else
brandIcon="$fileVaultIcon"
fi
# Start by setting result to UNDEFINED
result="UNDEFINED"
MissingSecureTokenCheck() {
# Get the currently logged-in user and go ahead if not root.
userName=$(/bin/ls -l /dev/console | /usr/bin/awk '{ print $3 }')
# This function checks if the logged-in user has Secure Token attribute associated
# with their account. If the token_status variable returns "0", then YES is set.
# If anything else is returned, NO is set.
if [[ -n "${userName}" && "${userName}" != "root" ]]; then
# Get the Secure Token status.
token_status=$(/usr/sbin/sysadminctl -secureTokenStatus "${userName}" 2>&1 | /usr/bin/grep -ic enabled)
# If there is no secure token associated with the logged-in account,
# the token_status variable should return "0".
if [[ "$token_status" -eq 0 ]]; then
result="NO"
fi
# If there is a secure token associated with the logged-in account,
# the token_status variable should return "1".
if [[ "$token_status" -eq 1 ]]; then
result="YES"
fi
fi
# If unable to determine the logged-in user
# or if the logged-in user is root, then UNDEFINED is returned
}
MissingSecureTokenCheck
if [[ $result = "NO" ]]; then
# Current user does not have a secure token. Need to generate one.
# Granting user needs to be an admin. Get all the admin users on the computer.
adminUsers=$(dscl . read /Groups/admin GroupMembership | cut -d " " -f 2-)
# For each user, check if they have a secure token
for EachUser in $adminUsers; do
TokenValue=$(sysadminctl -secureTokenStatus $EachUser 2>&1)
if [[ $TokenValue = *"ENABLED"* ]]; then
SecureTokenUsers+=($EachUser)
fi
done
# List out the users with a secure token
if [[ -z "${SecureTokenUsers[@]}" ]]; then
# If no secure token admin users, show dialog stating such
/usr/bin/osascript -e "display dialog \"\" & return & \"There are no secure token admin users on this device.\" with title \"Grant Secure Token\" buttons {\"OK\"} default button 1 with icon POSIX file \"$brandIcon\""
exit 0
else
# Have user select a secure token user they know the password for
adminUser=$( osascript -e "set ASlist to the paragraphs of \"$(printf '%s\n' "${SecureTokenUsers[@]}")\"" -e 'return choose from list ASlist with prompt "Select a user you know the password for:"' )
# Get a secure token users password
adminPassword=$( /usr/bin/osascript -e "display dialog \"To grant a secure token\" & return & \"Enter login password for '$adminUser'\" default answer \"\" with title \"Grant Secure Token\" buttons {\"Cancel\", \"Ok\"} default button 2 with icon POSIX file \"$brandIcon\" with text and hidden answer
set adminPassword to text returned of the result
return adminPassword")
# Exit if user cancels
if [ "$?" != "0" ] ; then
echo "User aborted. Exiting…"
exit 0
fi
fi
# Try the entered password
passCheck=`dscl /Local/Default -authonly "${adminUser}" "${adminPassword}"`
# If the credentials pass, continue, if not, tell user password is incorrect and exit.
if [ "$passCheck" == "" ]; then
echo "Password Verified"
else
echo "Password Verification Failed. Please try again."
/usr/bin/osascript -e "display dialog \"\" & return & \"Password Verification Failed. Please try again.\" with title \"Grant Secure Token\" buttons {\"OK\"} default button 1 with icon POSIX file \"$brandIcon\""
exit 1
fi
# Get the logged in user's password via a prompt
echo "Prompting ${userName} for their login password."
userPassword=$( /usr/bin/osascript -e "display dialog \"To grant a secure token\" & return & \"Enter login password for '$userName'\" default answer \"\" with title \"Grant Secure Token\" buttons {\"Cancel\", \"Ok\"} default button 2 with icon POSIX file \"$brandIcon\" with text and hidden answer
set userPassword to text returned of the result
return userPassword")
# Exit if user cancels
if [ "$?" != "0" ] ; then
echo "User aborted. Exiting…"
exit 0
fi
echo "Granting secure token."
# Grant the token
sysadminctl -secureTokenOn ${userName} -password ${userPassword} -adminUser ${adminUser} -adminPassword ${adminPassword}
# Check for bootstrap token escrowed with Jamf Pro
bootstrap=$(profiles status -type bootstraptoken)
if [[ $bootstrap == *"escrowed to server: YES"* ]]; then
echo "Bootstrap token already escrowed with Jamf Pro!"
else
# Escrow bootstrap token with Jamf Pro
echo "No Bootstrap token present. Escrowing with Jamf Pro now…"
sudo profiles install -type bootstraptoken -user "${adminUser}" -pass "${adminPassword}"
fi
elif [[ $result = "YES" ]]; then
echo "Current user already has a secure token. No action necessary."
/usr/bin/osascript -e "display dialog \"\" & return & \"$userName already has a secure token. No action necessary.\" with title \"Grant Secure Token\" buttons {\"OK\"} default button 1 with icon POSIX file \"$brandIcon\""
else
echo "Undefined secure token status"
/usr/bin/osascript -e "display dialog \"\" & return & \"Could not determine secure token status.\" with title \"Grant Secure Token\" buttons {\"OK\"} default button 1 with icon POSIX file \"$brandIcon\""
exit 1
fi

Re-enabling Jamf Connect Login after an in-place macOS Upgrade

When macOS is upgraded from one major version to the next the login window mechanisms are reset to their default values. This disables the custom Jamf Connect login window.

If Jamf Connect (or NoMAD Login AD) are used in your environment, this may be problematic, and we will need a way to re-enable the login screen automatically after an OS upgrade occurs. This post will go over my process for doing this with Jamf Pro and Jamf Connect v2, but it can be adapted for use with Jamf Connect v1, or NoMAD Login AD and other management solutions.

The main mechanism used here is a script run at startup, and a local plist file that stores the last known operating system build version of a given macOS device. The last known build version is compared to the current build version at startup, if they differ we know that the OS was updated. With this information, we can then run a policy to perform any post-update maintenance we want, in this case re-enabling Jamf Connect.

The first prerequisite is to ensure you have the Jamf startup script enabled. Navigate to Settings > Computer Management > Check-In and ensure that the boxes for Create startup script and Check for policies triggered by startup are checked. This ensures our script will run each time the computer boots. You could just as easily create your own LaunchDaemon here, but Jamf has already done that for us, so we might as well use the built in mechanism.

Additionally, we will need a smart computer group to identify devices with Jamf Connect installed. If you are using Jamf Connect v2.x this is as simple as Application Title is Jamf Connect.app

Next we need a policy to re-enable Jamf Connect. Here is an example:

  • General
  • Name: Enable Jamf Connect Login
  • Trigger: Custom (enable-jamfconnectlogin)
  • Frequency: Ongoing
  • Files and Processes
  • Execute Command: /usr/local/bin/authchanger -reset -jamfconnect
  • Note that the above command is for Jamf Connect v2.x. If you are using Jamf Connect v1.x, or NoMAD Login AD, use the appropriate authchanger command for that version.
  • Scope
  • Targets: Jamf Connect Installed (smart group from earlier)

Now we can upload our script to Jamf Pro. The script performs the following actions:

  1. Gets the current local operating system build.
  2. Checks if there is an existing local plist file for the macOS build version, and creates it if needed.
  3. If the current OS version matches the local plist we assume the OS was not updated, exit with status 0.
  4. If the current OS version does not match the the local plist, we assume the OS was updated, and perform some maintenance.
  5. Check to see if Jamf Connect Login is already enabled. If not, we call the custom trigger to enable it on devices in scope.
  6. Update inventory (the OS was updated, after all).
  7. Update the macOS build in the local plist with the new build version.

The script is available below:

#!/bin/sh
# Location of macOS Build plist for comparison
# Subsitute your org name for anyOrg, or place in another location
buildPlist="/usr/local/anyOrg/macOSBuild.plist"
# Get the local os build version
# Using build version accounts for supplimental updates as well as dot updates and os upgrades
localOS=$( /usr/bin/sw_vers | awk '/BuildVersion/{print $2}' )
# If the macOS Buld plist key does not exist, create it and write the local os into it
if ! /usr/libexec/PlistBuddy -c 'print "macOSBuild"' $buildPlist &> /dev/null; then
echo "macOS Build plist does not exist. Creating now…"
defaults write $buildPlist macOSBuild $localOS
else
echo "macOS Build plist already exists. Skipping creation…"
fi
# Get the os from the macOS build plist now that we have ensured it exists
plistOS=$( defaults read $buildPlist macOSBuild )
# If the local OS does not match the plist OS do some maintainance
if [[ $localOS != $plistOS ]]; then
# Check for Jamf Connect Login status and re-enable if needed
if [[ ! $( /usr/local/bin/authchanger -print | grep JamfConnectLogin ) ]]; then
# If JCL is disabled, re-enable it with a policy (scope carefully)
/usr/local/bin/jamf policy -event enable-jamfconnectlogin
else
echo "Jamf Connect Login is enabled on this device. Nothing to do here…"
fi
# Update inventory
/usr/local/bin/jamf recon
# Update the local plist file for next time
defaults write $buildPlist macOSBuild $localOS
else
echo "macOS was not updated. Nothing to do here."
fi

With this script uploaded to Jamf Pro, the last step is to create a policy that runs it.

  • General
  • Name: macOS Update Maintenance
  • Trigger: Startup
  • Frequency: Ongoing
  • Scripts
  • Select script uploaded previously.
  • Scope
  • Targets: All Managed Clients or All Computers

Now each time one of our Jamf Pro managed macs boots, it will run this script. The script will determine if the macOS build version has changed, then re-enable Jamf Connect if it is disabled.

And there is the added bonus of also performing a recon so that our inventory records are updated immediately after an OS update is done by the user!