The process of creating and publishing a new open-source macOS authorization plugin has taught me a few things about handling the list of login mechanisms in the authorization database. It also helped me realize that existing methods of maintaining the database often rely upon Python — a dependency I preferred to avoid.
In this post, I’ll go over the basics of managing the login mechanism list using shell scripts, which may be of interest to organizations who deploy Escrow Buddy, Crypt, or XCreds. I’ll also share a script that includes flexible shell functions for those who manage authorization plugins but don’t want or need to deploy a Python runtime.
Authorization plugins
macOS provides numerous ways for developers and IT administrators to run processes in the background. The most common ones — including LaunchDaemons and LaunchAgents — are widely understood, used, and abused in the Mac admin and security community. But there are situations where a daemon or agent isn’t the ideal tool for the job, and one of these situations is automation and decision-making around user login.
Enter macOS authorization plugins. Apple’s documentation describes their use:
A typical use for authorization plug-ins is to implement policies that are not included in the standard authorization configuration.
In the Mac admin world, such policies could include federating local accounts to cloud identity providers, providing additional account setup automation during the provisioning process, or handling situations that require user credentials like keychain syncing and FileVault key generation.
Authorization database
Authorization plugins can have multiple “mechanisms” that can serve specific purposes or run with specific privileges. In order to register these mechanisms with the macOS authorization services API, they need to be listed in the system.login.console
section of the authorization database, within the mechanisms
array.
The command security authorizationdb read system.login.console
will display the current mechanisms in a plist. (macOS stores the state of these mechanisms in multiple places, but using the security
command and other supported APIs is the best way to ensure any automation you create is stable and long-lived.)
As of macOS Sequoia, the command above produces the following output:
% security authorizationdb read system.login.console
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>class</key>
<string>evaluate-mechanisms</string>
<key>comment</key>
<string>Login mechanism based rule. Not for general use, yet.</string>
<key>created</key>
<real>470991139.20000899</real>
<key>mechanisms</key>
<array>
<string>builtin:prelogin</string>
<string>builtin:policy-banner</string>
<string>loginwindow:login</string>
<string>builtin:login-begin</string>
<string>builtin:reset-password,privileged</string>
<string>loginwindow:FDESupport,privileged</string>
<string>builtin:forward-login,privileged</string>
<string>builtin:auto-login,privileged</string>
<string>builtin:authenticate,privileged</string>
<string>PKINITMechanism:auth,privileged</string>
<string>builtin:login-success</string>
<string>loginwindow:success</string>
<string>HomeDirMechanism:login,privileged</string>
<string>HomeDirMechanism:status</string>
<string>MCXMechanism:login</string>
<string>CryptoTokenKit:login</string>
<string>loginwindow:done</string>
</array>
<key>modified</key>
<real>717879783.86331403</real>
<key>shared</key>
<true/>
<key>tries</key>
<integer>10000</integer>
<key>version</key>
<integer>11</integer>
</dict>
</plist>
YES (0)
The output has two parts: a plist representing the current state of the authorization database (sent to the stdout pipe), and a status message indicating whether the query was successful (to stderr). Here’s an example of an unsuccessful query generating no stdout and an error code in stderr:
% security authorizationdb read bogus-right-name
NO (-60005)
Resets can happen
During some macOS upgrades and updates, the list of login mechanisms in the authorization database gets reset to its default state. This is understandable, as the introduction of new macOS features sometimes requires new entries in the database, and Apple probably aims to avoid the risk of incompatible third-party mechanisms breaking the login process.
Default authorization database
The fresh authorization database is synthesized from /System/Library/Security/authorization.plist. Referring to this file on a given version of macOS can help you understand which mechanisms are standard and in which order they’re found.
If you deploy an authorization plugin to your managed Macs, the potential for periodic reset puts the onus on you and your team to be aware of when the plugins you deploy might have been removed from the authorization database and — after testing and ensuring compatibility with new macOS versions — re-adding your plugin mechanism to the authorization database to restore functionality.
Auditing desired state
In order to check whether your custom mechanism is present in the authorization database, a simple grep
can provide the necessary logic:
% security authorizationdb read system.login.console | grep "<string>YourPlugin:Mechanism</string>"
<string>YourPlugin:Mechanism</string>
YES (0)
If you’re planning on including this logic in a script, you may not want any output. To suppress output, we can add -q
to the grep
command, and redirect stderr to /dev/null.
% security authorizationdb read system.login.console 2>/dev/null | grep -q "<string>YourPlugin:Mechanism</string>"
This will produce a predictable exit code: zero if the desired mechanism is present, non-zero otherwise. An example of using this in a script-based collection mechanism (like a Jamf extension attribute) would be:
#!/bin/sh | |
# This Jamf extension attribute returns "Configured" if Escrow Buddy is present | |
# in the authorization database login mechanisms, or "Not Configured" otherwise. | |
MECHANISM="Escrow Buddy:Invoke,privileged" | |
if security authorizationdb read system.login.console 2>/dev/null | grep -q "<string>$MECHANISM</string>"; then | |
echo "<result>Configured</result>" | |
else | |
echo "<result>Not Configured</result>" | |
fi |
However, sometimes knowing whether the mechanism is in the database isn’t sufficient. Because the order of the mechanisms in the array represent the order they’re executed during login, you may want to capture where in that array your mechanism is.
This example extension attribute returns the index (beginning at zero) of the specified mechanism, or -1
if the mechanism isn’t listed:
#!/bin/sh | |
# This Jamf extension attribute returns the mechanism entry index number | |
# (starting with zero) if Escrow Buddy is present in the authorization database | |
# login mechanisms, or "-1" otherwise. | |
MECHANISM="Escrow Buddy:Invoke,privileged" | |
security authorizationdb read system.login.console 2>/dev/null > /tmp/auth.db | |
INDEX=$(/usr/libexec/PlistBuddy -c "Print :mechanisms:" /tmp/auth.db 2>/dev/null | grep -n "<string>$MECHANISM</string>" | awk -F ":" '{print $1}') | |
if [ -z $INDEX ]; then | |
# Return -1 if mechanism is not in array | |
INDEX="-1" | |
else | |
# Adjust for extra lines of output at beginning of array | |
INDEX=$((INDEX-2)) | |
fi | |
echo "<result>$INDEX</result>" |
Modifying the mechanism list
If your desired entry is missing from the list of login mechanisms in the authorization database, and you’ve tested to ensure compatibility with the current macOS version, you can provide an updated plist as input to the security
command to add your entry.
Making a backup
Before making any changes, it’s not a bad idea to save a backup of the plist in case restoration is required. We can do that by saving the plist to disk and making a copy:
security authorizationdb read system.login.console 2>/dev/null > /tmp/system.login.console.plist
cp /tmp/system.login.console.plist /tmp/system.login.console.plist.backup
Then we can edit the system.login.console.plist file as needed.
Validating input
Because errors in the authorization database can potentially prevent login from succeeding, extra diligence in catching errors is warranted. Login mechanism identifiers follow a predictable format that we can validate:
We can use grep
to ensure any provided mechanism names match a regular expression:
MECHANISM="YourPlugin:Mechanism"
if ! grep -qE '^[^,:]+:[^,:]+(,privileged)?$' <<< "$MECHANISM"; then
echo "ERROR: Specified right is not formatted correctly: $MECHANISM"
exit 1
fi
Relative positioning
When adding mechanisms to the array, we’ll need to specify an index; the new mechanism will be inserted into the array at that index, incrementing subsequent items’ indices by 1. Therefore, it may be useful to reference an existing item in the array that we can insert relative to. In the example below, we’re calculating the index of the built-in loginwindow:done
mechanism.
INDEX=$(/usr/libexec/PlistBuddy -c "Print :mechanisms:" /tmp/system.login.console.plist 2>/dev/null | grep -n "loginwindow:done" | awk -F ":" '{print $1}')
INDEX=$((INDEX-2))
Writing changes
Finally, we can use PlistBuddy’s Add
verb to modify the plist on disk, which we can feed as input to the security authorizationdb write system.login.console
command to apply the changes to the authorization database:
/usr/libexec/PlistBuddy -c "Add :mechanisms:$INDEX string 'MyPlugin:Action'" /tmp/system.login.console.plist
security authorizationdb write system.login.console < /tmp/system.login.console.plist
Removing mechanisms
If we need to remove a mechanism from the list, we can use a similar process to do so. First we’ll find its index. Then we’ll remove it using PlistBuddy’s Delete
verb. Then we’ll write the plist back to the database.
INDEX=$(/usr/libexec/PlistBuddy -c "Print :mechanisms:" /tmp/system.login.console.plist 2>/dev/null | grep -n "MyPlugin:Action" | awk -F ":" '{print $1}')
INDEX=$((INDEX-2))
/usr/libexec/PlistBuddy -c "Delete :mechanisms:$INDEX" /tmp/system.login.console.plist
security authorizationdb write system.login.console 2>/dev/null < /tmp/system.login.console.plist
Tying it all together
In order to simplify the process of managing system.login.console
mechanism entries for IT administrators, I’ve produced a shell script with various functions for dealing with the authorization database. This script incorporates the concepts covered above, and adds informative output, error handling, and sensible use of variables to make customization and implementation easier.
You can find this script on GitHub and embedded below. To use it, uncomment and customize the examples at the bottom of the script to suit your needs.
#!/bin/bash | |
### | |
# | |
# Name: ModifyAuthDBLoginMechs.sh | |
# Description: This script provides functions that can help Mac IT | |
# administrators modify and maintain the list of | |
# login mechanisms in the macOS authorization database. | |
# For details see: | |
# https://www.elliotjordan.com/posts/macos-authdb-mechs | |
# Author: Elliot Jordan <elliot@elliotjordan.com> | |
# Created: 2023-06-17 | |
# Last Modified: 2023-06-17 | |
# Version: 1.0.0 | |
# | |
### | |
# Temporary directory for storage of authorization database files | |
AUTH_DB="/tmp/system.login.console.plist" | |
# Check execution context (root required) | |
if [[ $EUID -ne 0 ]]; then | |
echo "ERROR: Please run this script as root." | |
exit 1 | |
fi | |
############################ FUNCTION: CheckAuthDB ############################ | |
# Checks current loginwindow auth database for entry specified by parameter 1. | |
CheckAuthDB() { | |
/usr/bin/security authorizationdb read system.login.console 2>/dev/null > "$AUTH_DB" | |
if grep -q "<string>$1</string>" "$AUTH_DB"; then | |
echo "$1 is configured in the authorization database." | |
return | |
fi | |
echo "$1 is not configured in the authorization database." | |
return 1 | |
} | |
############################ FUNCTION: BackupAuthDB ########################### | |
# Create a backup copy of the auth database at the path specified by $AUTH_DB | |
BackupAuthDB() { | |
if [[ -f "$AUTH_DB" ]]; then | |
/usr/bin/security authorizationdb read system.login.console 2>/dev/null > "$AUTH_DB" | |
fi | |
cp "$AUTH_DB" "$AUTH_DB.backup" | |
return | |
} | |
######################## FUNCTION: ValidateAuthDBEntry ######################## | |
# Validates the format of entry prior to modifying the authorization database | |
ValidateAuthDBEntry() { | |
if ! grep -qE '^[^,:]+:[^,:]+(,privileged)?$' <<< "$1"; then | |
echo "ERROR: Specified right is not formatted correctly: $1" | |
exit 1 | |
fi | |
} | |
############################ FUNCTION: AddToAuthDB ############################ | |
# Add an entry before/after another entry | |
# The entry to add is specified by parameter 1 | |
# "before" or "after" is specified by parameter 2 | |
# The existing entry to use for placement is specified by parameter 3 | |
AddToAuthDB() { | |
ValidateAuthDBEntry "$1" | |
INDEX=$(/usr/libexec/PlistBuddy -c "Print :mechanisms:" "$AUTH_DB" 2>/dev/null | grep -n "$3" | awk -F ":" '{print $1}') | |
if [[ -z $INDEX ]]; then | |
echo "ERROR: Unable to find $3 in authorization database." | |
exit 1 | |
fi | |
# Adjust index to account for PlistBuddy output format | |
if [[ "$2" == "before" ]]; then | |
INDEX=$((INDEX-2)) | |
elif [[ "$2" == "after" ]]; then | |
INDEX=$((INDEX-1)) | |
else | |
echo "ERROR: AddToAuthDB requires parameter 2 to be 'before' or 'after'." | |
exit 1 | |
fi | |
# Insert mechanism relative to parameter 3 | |
echo "Adding $1 to authorization database $2 $3..." | |
/usr/libexec/PlistBuddy -c "Add :mechanisms:$INDEX string '$1'" "$AUTH_DB" | |
# Save authorization database changes | |
if ! security authorizationdb write system.login.console 2>/dev/null < "$AUTH_DB"; then | |
echo "ERROR: Unable to save changes to authorization database." | |
exit 1 | |
fi | |
echo "$1 successfully configured in macOS authorization database." | |
} | |
########################## FUNCTION: RemoveFromAuthDB ######################### | |
# Remove mechanism specified by parameter 1 from authorization database | |
RemoveFromAuthDB() { | |
INDEX=$(/usr/libexec/PlistBuddy -c "Print :mechanisms:" "$AUTH_DB" 2>/dev/null | grep -n "$1" | awk -F ":" '{print $1}') | |
if [[ -z $INDEX ]]; then | |
echo "$1 is not configured in the authorization database." | |
return | |
fi | |
# Adjust index to account for PlistBuddy output format | |
INDEX=$((INDEX-2)) | |
# Remove mechanism | |
/usr/libexec/PlistBuddy -c "Delete :mechanisms:$INDEX" "$AUTH_DB" | |
# Save authorization database changes | |
echo "Removing $1 from authorization database..." | |
if ! security authorizationdb write system.login.console 2>/dev/null < "$AUTH_DB"; then | |
echo "ERROR: Unable to save changes to authorization database." | |
exit 1 | |
fi | |
} | |
################################ USAGE EXAMPLES ############################### | |
# The commented examples below demonstrate how to use the functions above. | |
# Example 1: Check authorization database for Escrow Buddy entry. | |
# If present, remove it. | |
# | |
# MECHANISM="Escrow Buddy:Invoke,privileged" | |
# if CheckAuthDB "$MECHANISM"; then | |
# BackupAuthDB | |
# RemoveFromAuthDB "$MECHANISM" | |
# fi | |
# Example 2: Check authorization database for Escrow Buddy entry. | |
# If absent, add it before loginwindow:done. | |
# | |
# MECHANISM="Escrow Buddy:Invoke,privileged" | |
# if ! CheckAuthDB "$MECHANISM"; then | |
# BackupAuthDB | |
# AddToAuthDB "$MECHANISM" "before" "loginwindow:done" | |
# fi | |
# Example 3: Check authorization database for Crypt entries. | |
# If any are absent, re-add them before loginwindow:done. | |
# | |
# MECHANISM1="Crypt:Check,privileged" | |
# MECHANISM2="Crypt:CryptGUI" | |
# MECHANISM3="Crypt:Enablement,privileged" | |
# if ! CheckAuthDB "$MECHANISM1" || ! CheckAuthDB "$MECHANISM2" || ! CheckAuthDB "$MECHANISM3"; then | |
# BackupAuthDB | |
# CheckAuthDB "$MECHANISM1" && RemoveFromAuthDB "$MECHANISM1" | |
# CheckAuthDB "$MECHANISM2" && RemoveFromAuthDB "$MECHANISM2" | |
# CheckAuthDB "$MECHANISM3" && RemoveFromAuthDB "$MECHANISM3" | |
# AddToAuthDB "$MECHANISM1" "before" "loginwindow:done" | |
# AddToAuthDB "$MECHANISM2" "before" "loginwindow:done" | |
# AddToAuthDB "$MECHANISM3" "before" "loginwindow:done" | |
# fi | |
# Example 4: Remove all Crypt entries from authorization database. | |
# | |
# MECHANISM1="Crypt:Check,privileged" | |
# MECHANISM2="Crypt:CryptGUI" | |
# MECHANISM3="Crypt:Enablement,privileged" | |
# BackupAuthDB | |
# CheckAuthDB "$MECHANISM1" && RemoveFromAuthDB "$MECHANISM1" | |
# CheckAuthDB "$MECHANISM2" && RemoveFromAuthDB "$MECHANISM2" | |
# CheckAuthDB "$MECHANISM3" && RemoveFromAuthDB "$MECHANISM3" |
Further reading
If you’re interested in learning more about managing the system.login.console
right of the macOS authorization database, check out the articles and code examples below:
- Csaba Fitzl: Beyond the good ol’ LaunchAgents - 28 - Authorization Plugins
- DssW: system.login.console - Overview of the macOS Authorization Right (includes default mechanisms for macOS 10.6 through 10.14)
- Charles Edge: authorizationdb Defaults in macOS 10.14
- Erik Berglund: Scripts/random/modifyAuthorizationDB/modifyAuthorizationDB
- Jamf: Modifying the loginwindow Application
- JumpCloud: support/scripts/macos/remove_mac_agent.sh (clever one-line removal method using
sed
) - Airbnb: macadmins/puppet-authpluginmech: A method of managing mechanisms for authorization plugins
- Airbnb: puppet-crypt/manifests/config.pp
And if you’re interested in the other rights managed by the authorization database, see:
- Rich Trouton: Managing the Authorization Database in OS X Mavericks
- Armin Briegel: Demystifying
root
on macOS, Part 4 —The Authorization Database - Nathaniel Strauss: Authorization Rights Management for Standard User Access
- Apple: Introduction to Authorization Services Programming Guide (archived, but helpful context)