Jamf Connect Password Sync

Password Sync

🔐 Jamf Pro & Jamf Connect: Password Sync Remediation

Company: Digital Convegence

Version: 2.2 (Dec 2025)   |   Lead Engineer: Trevor Leadley
Platforms: macOS 10.13+   |   Jamf Pro   |   Jamf Protect   |   Okta/IDP

Password desyncs between your macOS local accounts and Okta shouldn’t turn into help-desk fires.

This automated Jamf Pro + Jamf Connect + Jamf Protect solution catches changes instantly, remediates them silently, and keeps everything in perfect sync — no reboots, no manual resets, no frustrated users.

1. Executive Summary & Purpose

This solution automates the remediation of “Password Out-of-Sync” states between the local macOS account password (FileVault / Computer User) and the Identity Provider (Okta) network password.

It specifically targets scenarios where the password has changed or been updated outside the regular Jamf Connect process, the SecureToken has expired/corrupted, or the system never completed a proper shutdown/hibernation cycle.

Triggers:

  • Okta/IDP Password Change – detected via Jamf Connect or Okta Workflows (real-time EA update).
  • Unauthorized Local Change – detected by Jamf Protect custom analytic monitoring the local directory service.

2. Process Overview

The solution uses four coordinated components:

  • Jamf Connect – checks network status every 30 minutes (or on state change) and updates PasswordCurrent status.
  • Jamf Pro Check-in – runs local status checks and updates Extension Attributes every 15 minutes.
  • Jamf Protect Analytics – detects changes to the user profile plist that stores password hash, SecureToken hash, and passwordLastSetTime.
  • Okta Workflows – updates the “Password Update” EA in real time when a user changes their password through Okta (not via Jamf Connect).

Users have until the next check-in to sync manually unless the analytic triggers immediate remediation.

3. Jamf Pro Configuration

Script Parameter Mapping

Parameter
Label
Purpose
P4
Jamf Server URL
FQDN (e.g., company.jamfcloud.com)
P5
API Username
Service account with Update permissions
P6
Encrypted Password
AES-256 Encrypted string of the API password
P8
Encryption Salt
Salt used for OpenSSL decryption
P9
Encryption Key
Passphrase/Key used for OpenSSL decryption

API Roles & Permissions

The service account in P5 requires:

  • Computers: Read, Update
  • Computer Extension Attributes: Read

4. Jamf Protect Custom Analytic

Analytic Name: password_update

Predicate (Plaintext):

($event.path BEGINSWITH[cd] "/var/db/dslocal/nodes/Default/users" 
  AND $event.path CONTAINS[cd] ".plist" 
  AND $event.isModified == 1 
  AND $event.file.contentsAsDict.accountPolicyData.asPlistDict.passwordLastSetTime != $event.file.snapshotData.asPlistDict.accountPolicyData.asPlistDict.passwordLastSetTime)

5. Extension Attribute Scripts

Jamf Connect Sync (EA – checks sync status at check-in)

#!/bin/bash

loggedInUser=$(scutil << "show State:/Users/ConsoleUser" | awk '/Name :/ && !/loginwindow/ { print $3 }')
isCurrent=$(defaults read /Users/"$loggedInUser"/Library/Preferences/com.jamf.connect.state PasswordCurrent 2>/dev/null)

if [ "$isCurrent" != "1" ]; then
  if [ "$isCurrent" == 1 ]; then
    echo "<result>Password Synced</result>"
  elif [ "$isCurrent" == 0 ]; then
    echo "<result>Password Not Synced</result>"
  fi
else
  alias=$(dscl . -read /Users/"$loggedInUser" | grep "RecordName" | awk '{print $2}')
  isCurrent=$(defaults read /Users/"$alias"/Library/Preferences/com.jamf.connect.state PasswordCurrent 2>/dev/null)
  if [ "$isCurrent" == 1 ]; then
    echo "<result>Password Synced</result>"
  elif [ "$isCurrent" == 0 ]; then
    echo "<result>Password Not Synced</result>"
  fi
fi
exit 0;

Jamf Connect Password – Expiration in days

#!/bin/sh
# Jamf EA: Jamf Connect Password - Expiration in days (integer)

currentUser=$(ls -1 /dev/console | awk '{print $3}')
defaults read /var/db/dslocal/nodes/Default/users/$currentUser.plist > /dev/null 2>&1

if [ -z "$currentUser" ]; then
  echo "Error: CURRENT_USER is not set." >&2
  exit 1
fi

# (full expiry logic continues here – see original PDF for complete implementation)

6. Computer Smart Groups

  • Password Requires Sync – immediate attention & remediation (executes update prompt if expired).
  • Password Update Check – passive sync verification.
  • Password Expiring – users whose passwords expire within 5 days.

7. The Remediation Script (Core)

#!/bin/bash

# --- PRE-FLIGHT ---
loggedInUser=$( ls -l /dev/console | awk '{print $3}' )
jamfserver="$4"
jamfProUser="$5"
jamfProPassEnc="$6"

# Decrypt password
jamfProPass=$( echo "$jamfProPassEnc" | /usr/bin/openssl enc -aes256 -d -a -A -S "$8" -k "$9" )
APIauth="$jamfProUser:$jamfProPass"

# --- CORE FUNCTIONS ---
get_token() {
    echo "Fetching fresh API token..."
    authToken=$( /usr/bin/curl --request POST --silent --url "https://$jamfserver/api/v1/auth/token" --user "$APIauth" )
    token=$( /usr/bin/plutil -extract token raw - <<< "$authToken" )
    tokenExpiration=$( /usr/bin/plutil -extract expires raw - <<< "$authToken" )
    
    # ISO 8601 fix for macOS date compatibility
    cleanExpiration=$(echo "$tokenExpiration" | sed 's/\.[0-9]*//g; s/Z//g')
    localTokenExpirationEpoch=$( /bin/date -j -f "%Y-%m-%dT%T" "$cleanExpiration" +"%s" 2>/dev/null )
}

update_ea(){	
    currentEpoch=$(date +%s)
    if [[ -n "$token" ]] && [[ $localTokenExpirationEpoch -gt $currentEpoch ]]; then
        getudid=$(ioreg -d2 -c IOPlatformExpertDevice | awk -F\" '/IOPlatformUUID/{print $(NF-1)}')
        eaID="33"
        eaName="Password Update" 
        value="" # clears the Extension Attribute

        curl -k -s --header "Authorization: Bearer $token" -X "PUT" \
             "https://$jamfserver/JSSResource/computers/udid/$getudid/subset/extension_attributes" \
              -H "Content-Type: application/xml" \
              -H "Accept: application/xml" \
              -d "<computer><extension_attributes><extension_attribute><id>$eaID</id><name>$eaName</name><value>$value</value></extension_attribute></extension_attributes></computer>"
    else
        echo "API Token invalid or date parsing failed. EA update skipped."
    fi
}

get_uptime() {
    uptime_string=$(uptime | awk -F'up ' '{print $2}' | cut -d',' -f1)
    days=$(echo $uptime_string | awk '{print $1}')
    if [[ $uptime_string == *"day"* ]]; then
       echo "$days"
    else
       echo "0"
    fi
}

# [Remediation Logic & Secure Token Checks continue here – full script includes password hash validation, 
#  SecureToken status check, and Okta sync prompt if needed]

Note: The script forces a refresh of accountPolicyData with defaults read /var/db/dslocal/nodes/Default/users/<user>.plist > /dev/null 2>&1 to prevent corrupted passwordLastSetTime values.

8. Help Desk Triage & The “Danger Path”

🚩 Danger Path: If the script alerts “Security Authorization Issue”, the user lacks a Secure Token.

THE GOLDEN RULE: DO NOT RESTART THE COMPUTER.

Danger Path Remediation (Tier 2/3)

  1. Grant Token (if another admin is present):
    sysadminctl -adminUser [Admin] -adminPassword - -secureTokenOn [User] -password -
  2. Escrow Bootstrap Token (if missing from Jamf):
    sudo profiles install -type bootstraptoken
  3. Validate: sysadminctl -secureTokenStatus [User] should return ENABLED.

9. Testing & Validation Loop

  1. Create dummy user: sudo sysadminctl -addUser testuser -password "Pass123"
  2. Trigger analytic: sudo dscl . -passwd /Users/testuser "Pass123" "NewPass456"
  3. Check /Library/Application Support/JamfProtect/groups/ for trigger file.
  4. Verify in Jamf Pro: Extension Attribute ID (Number) “Password Update” is now blank.

10. Troubleshooting API Status Codes

Code
Meaning
Fix
401
Unauthorized
Check P5/P6/P8/P9 decryption logic.
403
Forbidden
Grant “Update” permissions to the API account.
404
Not Found
Ensure EA ID (Number) “Password Update” exists in Jamf settings.

Related Documents

  Remediation Files - Script and EA