Secure Token Creation Script for macOS: A Guide for IT Pros

In today’s IT landscape, managing user accounts and ensuring secure access is critical for maintaining robust system security. One of the key aspects of this management on macOS is the use of secure tokens. Secure tokens are vital for various security functions, including enabling FileVault and performing certain administrative tasks.

This blog post will delve into a script that automates the process of granting secure token access to user accounts on macOS, explaining its importance, functionality, and use cases for IT professionals and Managed Service Providers (MSPs).

Background

Secure tokens are a security feature on macOS that provide additional authentication measures, particularly when it comes to FileVault encryption. For IT professionals and MSPs, managing these tokens is essential to maintaining secure environments across numerous devices.

The provided script simplifies the process of granting secure token access to a user account, even creating the account if it doesn’t already exist. This automation is especially beneficial in large-scale environments where manual configuration would be impractical and time-consuming.

The Script

#!/usr/bin/env bash
# Description: Grants secure token access to Service Account. Account will be created if it doesn't exist. Service Accounts will not show up at the desktop login.
# Release Notes: Initial Release
#
# Custom Fields:
#  New Account Password Custom Field: A secure custom field that stores the password for the new user account.
#  Optional Authentication Account Username Custom Field: A secure custom field that stores the username of the admin account that has secure token already on the device.
#
# Parameters:
#  username: Username to grant secure token access to
#  password: Password of user to grant secure token access to
#  adminuser: (Optional) Secure token Admin username - leave blank to prompt local user
#  adminpassword: (Optional) Secure token Admin password - leave blank to prompt local user
#
# Usage: ./Create-SecureTokenAccount.sh <-u|--username <arg>> <-p|--password <arg>> [-a|--adminuser <arg>] [-d|--adminpassword <arg>]
# <> are required
# [] are optional
# Example: ./Create-SecureTokenAccount.sh --username test --password Password1 --adminuser admin --adminpassword Password2
#
# Notes:
# By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
# Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
# Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
# Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
# Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
# Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
# Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
# EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#
#

die() {
    local _ret="${2:-1}"
    test "${_PRINT_HELP:-no}" = yes && print_help >&2
    echo "$1" >&2
    exit "${_ret}"
}

begins_with_short_option() {
    local first_option all_short_options='upadvh'
    first_option="${1:0:1}"
    test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0
}

GetCustomField() {
    customfieldName=$1
    dataPath=$(printenv | grep -i NINJA_DATA_PATH | awk -F = '{print $2}')
    value=""
    if [ -e "${dataPath}/ninjarmm-cli" ]; then
        value=$("${dataPath}"/ninjarmm-cli get "$customfieldName")
    else
        value=$(/Applications/NinjaRMMAgent/programdata/ninjarmm-cli get "$customfieldName")
    fi
    if [[ "${value}" == *"Unable to find the specified field"* ]]; then
        echo ""
        return 1
    else
        echo "$value"
    fi
}

# THE DEFAULTS INITIALIZATION - OPTIONALS
_arg_username=
_arg_password=
_arg_adminuser=
_arg_adminpassword=

print_help() {
    printf '%s\n' "Grants secure token access to an account. Account will be created if it doesn't exist."
    printf 'Usage: %s <-u|--username <arg>> <-p|--password <arg>> [-a|--adminuser <arg>] [-d|--adminpassword <arg>] [-h|--help]\n' "$0"
    printf '\t%s\n' "-u, --username: Username to grant secure token access to. (Required)"
    printf '\t%s\n' "-p, --password: Password of user to grant secure token access to. (Required)"
    printf '\t%s\n' "-a, --adminuser: (Optional) Secure token Admin username. (Leave blank to prompt local user)"
    printf '\t%s\n' "-d, --adminpassword: (Optional) Secure token Admin password. (Leave blank to prompt local user)"
    printf '\t%s\n' "-h, --help: Prints help"
}

parse_commandline() {
    while test $# -gt 0; do
        _key="$1"
        case "$_key" in
        -u | --username)
            test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
            _arg_username="$2"
            shift
            ;;
        --username=*)
            _arg_username="${_key##--username=}"
            ;;
        -u*)
            _arg_username="${_key##-u}"
            ;;
        -p | --password)
            test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
            _arg_password="$2"
            shift
            ;;
        --password=*)
            _arg_password="${_key##--password=}"
            ;;
        -p*)
            _arg_password="${_key##-p}"
            ;;
        -a | --adminuser)
            test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
            _arg_adminuser="$2"
            shift
            ;;
        --adminuser=*)
            _arg_adminuser="${_key##--adminuser=}"
            ;;
        -a*)
            _arg_adminuser="${_key##-a}"
            ;;
        -d | --adminpassword)
            test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
            _arg_adminpassword="$2"
            shift
            ;;
        --adminpassword=*)
            _arg_adminpassword="${_key##--adminpassword=}"
            ;;
        -d*)
            _arg_adminpassword="${_key##-d}"
            ;;
        -h | --help)
            print_help
            exit 0
            ;;
        -h*)
            print_help
            exit 0
            ;;
        *)
            _PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1
            ;;
        esac
        shift
    done
}

parse_commandline "$@"

# Get Script Variables and override parameters
if [[ -n $(printenv | grep -i newAccountUsername | awk -F = '{print $2}') ]]; then
    _arg_username=$(printenv | grep -i newAccountUsername | awk -F = '{print $2}')
fi
if [[ -n $(printenv | grep -i newAccountPasswordCustomField | awk -F = '{print $2}') ]]; then
    # Get the password from the custom field
    if ! _arg_password=$(GetCustomField "$(printenv | grep -i newAccountPasswordCustomField | awk -F = '{print $2}')"); then
        # Exit if the custom field is empty
        if [[ -z "${_arg_password}" ]]; then
            echo "[Error] Custom Field ($(printenv | grep -i newAccountPasswordCustomField | awk -F = '{print $2}')) was not found. Please check that the custom field contains a password."
            exit 1
        fi
        # Exit if the custom field is not found
        echo "[Error] Custom Field ($(printenv | grep -i newAccountPasswordCustomField | awk -F = '{print $2}')) was not found. Please check the custom field name."
        exit 1
    fi
fi
if [[ -n $(printenv | grep -i optionalAuthenticationAccountUsername | awk -F = '{print $2}') ]]; then
    _arg_adminuser=$(printenv | grep -i optionalAuthenticationAccountUsername | awk -F = '{print $2}')
fi
if [[ -n $(printenv | grep -i optionalAuthenticationAccountPasswordCustomField | awk -F = '{print $2}') ]]; then
    # Get the password from the custom field
    if ! _arg_adminpassword=$(GetCustomField "$(printenv | grep -i optionalAuthenticationAccountPasswordCustomField | awk -F = '{print $2}')"); then
        # Exit if the custom field is empty
        if [[ -z "${_arg_adminpassword}" ]]; then
            echo "[Error] Custom Field ($(printenv | grep -i optionalAuthenticationAccountPasswordCustomField | awk -F = '{print $2}')) was not found. Please check that the custom field contains a password."
            exit 1
        fi
        # Exit if the custom field is not found
        echo "[Error] Custom Field ($(printenv | grep -i optionalAuthenticationAccountPasswordCustomField | awk -F = '{print $2}')) was not found. Please check the custom field name."
        exit 1
    fi
fi

# If both username and password are empty
if [[ -z "${_arg_username}" ]]; then
    echo "[Error] User Name is required."
    if [[ -z "${_arg_password}" ]]; then
        echo "[Error] Password is required, please set the password in the secure custom field."
    fi
    exit 1
fi

# If username is not empty and password is empty
if [[ -n "${_arg_username}" ]] && [[ -z "${_arg_password}" ]]; then
    echo "[Error] Password is required, please set the password in the secure custom field."
    exit 1
fi

# If username is not empty and password is empty
if [[ -n "${_arg_adminuser}" ]] && [[ -z "${_arg_adminpassword}" ]]; then
    echo "[Error] Password is required, please set the password in the secure custom field."
    exit 1
fi

UserAccount=$_arg_username
UserPass=$_arg_password
UserFullName="ServiceAccount"
secureTokenAdmin=$_arg_adminuser
secureTokenAdminPass=$_arg_adminpassword
macOSVersionMajor=$(sw_vers -productVersion | awk -F . '{print $1}')
macOSVersionMinor=$(sw_vers -productVersion | awk -F . '{print $2}')
macOSVersionBuild=$(sw_vers -productVersion | awk -F . '{print $3}')

# Check script prerequisites.

# Exits if macOS version predates the use of SecureToken functionality.
# Exit if macOS < 10.
if [ "$macOSVersionMajor" -lt 10 ]; then
    echo "[Warn] macOS version ${macOSVersionMajor} predates the use of SecureToken functionality, no action required."
    exit 0
# Exit if macOS 10 < 10.13.4.
elif [ "$macOSVersionMajor" -eq 10 ]; then
    if [ "$macOSVersionMinor" -lt 13 ]; then
        echo "[Warn] macOS version ${macOSVersionMajor}.${macOSVersionMinor} predates the use of SecureToken functionality, no action required."
        exit 0
    elif [ "$macOSVersionMinor" -eq 13 ] && [ "$macOSVersionBuild" -lt 4 ]; then
        echo "[Warn] macOS version ${macOSVersionMajor}.${macOSVersionMinor}.${macOSVersionBuild} predates the use of SecureToken functionality, no action required."
        exit 0
    fi
fi

# Exits if $UserAccount already has SecureToken.
if sysadminctl -secureTokenStatus "$UserAccount" 2>&1 | grep -q "ENABLED"; then
    echo "${UserAccount} already has a SecureToken. No action required."
    exit 0
fi

# Exits with error if $secureTokenAdmin does not have SecureToken
# (unless running macOS 10.15 or later, in which case exit with explanation).

if [ -n "$secureTokenAdmin" ]; then
    if sysadminctl -secureTokenStatus "$secureTokenAdmin" 2>&1 | grep -q "DISABLED"; then
        if [ "$macOSVersionMajor" -gt 10 ] || [ "$macOSVersionMajor" -eq 10 ] && [ "$macOSVersionMinor" -gt 14 ]; then
            echo "[Warn] Neither ${secureTokenAdmin} nor ${UserAccount} has a SecureToken, but in macOS 10.15 or later, a SecureToken is automatically granted to the first user to enable FileVault (if no other users have SecureToken), so this may not be necessary. Try enabling FileVault for ${UserAccount}. If that fails, see what other user on the system has SecureToken, and use its credentials to grant SecureToken to ${UserAccount}."
            exit 0
        else
            echo "[Error] ${secureTokenAdmin} does not have a valid SecureToken, unable to proceed. Please update to another admin user with SecureToken."
            exit 1
        fi
    else
        echo "[Info] Verified ${secureTokenAdmin} has SecureToken."
    fi
fi

# Creates a new user account.
create_user() {
    # Check if the user account exists
    if id "$1" >/dev/null 2>&1; then
        echo "[Info] Found existing user account $1."
    else
        echo "[Warn] Account $1 doesn't exist. Attempting to create..."
        # Create a new user
        dscl . -create /Users/"$1"
        # Add the display name of the User
        dscl . -create /Users/"$1" RealName "$3"
        # Replace password_here with your desired password to set the password for this user
        dscl . -passwd /Users/"$1" "$2"
        # Set the Unique ID for the New user. Replace with a number that is not already taken.
        LastID=$(dscl . -list /Users UniqueID | sort -nr -k 2 | head -1 | grep -oE '[0-9]+$')
        NextID=$((LastID + 1))
        dscl . -create /Users/"$1" UniqueID $NextID
        # Set the group ID for the user
        dscl . -create /Users/"$1" PrimaryGroupID 20
        # Append the User with admin privilege. If this line is not included the user will be set as standard user.
        # sudo dscl . -append /Groups/admin GroupMembership "$1"
        echo "[Info] Account $1 created."
    fi
}
# Adds SecureToken to target user.
securetoken_add() {
    if [ -n "$3" ]; then
        # Admin user name was given. Do not prompt the user.
        sysadminctl \
            -secureTokenOn "$1" \
            -password "$2" \
            -adminUser "$3" \
            -adminPassword "$4"
    else
        # Admin user name was not given. Prompt the local user.
        currentUser=$(stat -f%Su /dev/console)
        currentUserUID=$(id -u "$currentUser")
        launchctl asuser "$currentUserUID" sudo -iu "$currentUser" \
            sysadminctl \
            -secureTokenOn "$1" \
            -password "$2" \
            interactive
    fi
    # Verify successful SecureToken add.
    secureTokenCheck=$(sysadminctl -secureTokenStatus "${1}" 2>&1)
    if echo "$secureTokenCheck" | grep -q "DISABLED"; then
        echo "[Error] Failed to add SecureToken to ${1}. Please rerun policy; if issue persists, a manual SecureToken add will be required to continue."
        exit 126
    elif echo "$secureTokenCheck" | grep -q "ENABLED"; then
        echo "[Info] Successfully added SecureToken to ${1}."
    else
        echo "[Error] Unexpected result, unable to proceed. Please rerun policy; if issue persists, a manual SecureToken add will be required to continue."
        exit 1
    fi
}

# Create new user if it doesn't already exist.
create_user "$UserAccount" "$UserPass" "$UserFullName"
# Add SecureToken using provided credentials.
securetoken_add "$UserAccount" "$UserPass" "$secureTokenAdmin" "$secureTokenAdminPass"

 

Access over 300+ scripts in the NinjaOne Dojo

Get Access

Detailed Breakdown

Overview of the Script

The script in question is designed to grant secure token access to a user account on macOS, with the capability to create the account if it does not already exist. Here’s a detailed step-by-step breakdown of how the script works:

  1. Parameter Parsing: The script starts by defining a die function to handle errors and a print_help function to display usage information. It then parses command-line arguments to extract the username, password, and optionally, the admin username and password.
  2. Environment Variables: It checks for environment variables that may override the command-line parameters. If specific environment variables are set, the script retrieves their values to use them as parameters.
  3. macOS Version Check: The script checks the macOS version to ensure it supports secure token functionality. It exits if the macOS version is too old to utilize secure tokens.
  4. Secure Token Status Check: It checks if the specified user account already has a secure token. If the user account already has a secure token, the script exits, as no further action is needed.
  5. Admin User Token Check: If an admin username is provided, the script verifies that this admin user has a secure token. If not, it exits with an error unless the macOS version is 10.15 or later, where a different process is recommended.
  6. User Account Creation: The script includes a function to create a new user account if it doesn’t already exist. It assigns a unique ID, sets a password, and configures other necessary attributes.
  7. Granting Secure Token: The script attempts to grant a secure token to the specified user account using the provided credentials. If the admin username is provided, it uses those credentials; otherwise, it prompts the local user for authentication.

Potential Use Cases

Imagine an IT professional named Alex who manages a fleet of macOS devices for a large corporation. Alex needs to ensure that all user accounts on these devices have secure tokens for FileVault encryption. Manually checking and granting secure tokens on each device would be incredibly time-consuming.

By deploying this script through a centralized management tool, Alex can automate the process, ensuring all user accounts across the organization have the necessary secure tokens, thereby maintaining compliance with the company’s security policies.

Comparisons

Other methods to grant secure tokens typically involve manual intervention through the macOS System Preferences or using sysadminctl commands individually for each user. While these methods work, they are not scalable for managing large numbers of devices. The script automates these steps, making it more efficient and reducing the likelihood of human error.

FAQs

  • What happens if the user account already exists?

    The script checks for the existence of the user account and skips the creation step if it already exists.

  • Can I use this script on older versions of macOS?

    The script includes checks to ensure it only runs on macOS versions that support secure tokens, specifically macOS 10.13.4 and later.

  • What if the admin user doesn’t have a secure token?

    The script will exit with an error if the admin user does not have a secure token, except on macOS 10.15 or later, where an alternative process is suggested.

Implications

Granting secure tokens to user accounts is crucial for enabling FileVault and performing administrative tasks securely. Automating this process helps maintain high security standards, ensures compliance with organizational policies, and reduces the risk of unauthorized access.

Recommendations

  • Regularly Update the Script: Ensure the script is kept up-to-date with the latest macOS changes and security practices.
  • Secure Custom Fields: Use secure custom fields to store sensitive information like passwords.
  • Centralized Management: Deploy the script through a centralized management tool to ensure consistency across all devices.

Final Thoughts

Automating the process of granting secure tokens using this script significantly enhances the efficiency and security of managing macOS devices. For IT professionals and MSPs, this script is a valuable tool in maintaining robust security practices.

NinjaOne offers comprehensive solutions that integrate seamlessly with such scripts, providing a holistic approach to IT management and security. By leveraging NinjaOne, you can streamline your workflows and ensure all your devices are secure and compliant with your organization’s policies.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service deliver tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like

×

See NinjaOne in action!

By submitting this form, I accept NinjaOne's privacy policy.

NinjaOne Terms & Conditions

By clicking the “I Accept” button below, you indicate your acceptance of the following legal terms as well as our Terms of Use:

  • Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms.
  • Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party.
  • Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library belonging to or under the control of any other software provider.
  • Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations.
  • Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks.
  • Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script.
  • EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).