[T]he three great virtues of a programmer [are]: laziness, impatience, and hubris."
~Larry Wall, “Programming Perl”
Then I must be the best programmer evah!
~Me
Last March I reported at our monthly meetup that changes were coming to the Jamf Classic API and that it was time to start converting all of our Jamf scripts that made API calls from Basic Authentication to Token Authentication. Jamf had just released 10.35.0 and in the release notes there was the following paragraph:
You can now use the Classic API to authenticate using Basic authentication or a Bearer Token retrieved through the /v1/auth/token Jamf Pro API endpoint for enhanced security. For information on Bearer Token authentication, see the Jamf developer resources: https://developer.jamf.com/jamf-pro/docs/classic-api-authentication-changes
And I noted that if you followed the link to the developer site, you saw the following warning:
Support for Basic authentication via the Classic API has been deprecated and will be removed at a future time (estimated removal date: August-December 2022).
We had no idea of exactly when Jamf was going to pull this trigger, but my money was on there being an announcement at JNUC with a final date before the end of the year. (Spoiler alert: I was wrong!) Just before JNUC, Jamf released 10.41, and now the developer docs read:
Support for Basic authentication will be removed in a future release.
So we don’t know exactly when that other shoe is going to drop, and we are probably safe in assuming that there were be a decent amount of time from the announcement to the deadline.
But we have a fair number of scripts we maintain and a fair number of clients on whose servers we need to update those scripts. And while my wife says that I put the pro in procrastination and some of my best work is done at the last minute, I really want to be ahead of this one. Last March, I presented some sample code on how to get and use a token, but at that point I still hadn’t converted enough of my older scripts to really have a handle on the best way to go about this. Now that I have been pouring over nearly 300 scripts across multiple servers, I have distilled the work involved down to just a few steps. So I am going to share with you everything you need to convert your scripts that use the Jamf API to bearer (token) authentication easily, including: • A function you can drop into your script that will get the token from Jamf Pro • A snippet of code to paste in just above your first API call that to use that function • The search and replace terms you can use to convert your basic authentication calls into token authentication • And a shell script you can run against your Jamf Pro server to find which scripts need updating I will also show you one of our scripts that I’ve already updated in a side-by-side comparison.
The Function
Copy the following function and paste it into your script towards the top. A shell function needs to be declared before it can be used. So you could paste this in just before the next bit, but the common practice is to put all your functions towards the top of a script.
## This function calls the Jamf (newer) "Pro" API to generate a token for subsequent calls to the "Pro" or "Classic" APIs.
function getAPIToken() {
jamfURL=$1
basicAuth=$2
authToken=$(curl -s \
--request POST \
--url "${jamfURL}/api/v1/auth/token" \
--header "Accept: application/json" \
--header "Authorization: Basic ${basicAuth}" \
2>/dev/null \
)
## Courtesy of Der Flounder
## Source: https://derflounder.wordpress.com/2021/12/10/obtaining-checking-and-renewing-bearer-tokens-for-the-jamf-pro-api/
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
api_token=$(/usr/bin/awk -F \" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
else
api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "$authToken")
fi
echo ${api_token}
}
Before the First API Call
Next, paste the following block into your code somewhere above your first API call. This could even be put just after the function declaration we just finished, but for ease-of-maintainance, I prefer to keep variable declaration close to the first usage.
## Get the token and verify connection
# basicAuth=$(echo -n "${JAMFUSER}:${JAMFPASS}" | base64)
token=$(getAPIToken "${JAMFURL}" "${basicAuth}")
if [[ "${token}" == "" ]]; then
echo "Error: Unable to authenticate"
exit 1
fi
If your script is already using a Bas64 encoded form of your API credentials, then just change the line:
token=$(getAPIToken "${JAMFURL}" "${basicAuth}")
to the variables where you have that information.
If, however, you are passing in the API username and password in cleartext, then you need to uncomment the line:
# basicAuth=$(echo -n "${JAMFUSER}:${JAMFPASS}" | base64)
and update the ${JAMFUSER} and ${JAMFPASS} variables accordingly
Search and Destroy Replace
Now it’s just a matter of searching for every instance of Authorization: Basic XXXXXX where XXXXXX is your (variable containing the) base64 encoded string and replacing it with Authorization: Bearer: YYYYYY where YYYYYY is your (variable containing the) token.
Example:
curl -s -H “Authorization: Basic ${basicAuth}” https://pretendo.jamfcloud.com/JSSResource/…
becomes
curl -s -H “Authorization: Bearer ${token}” https://pretendo.jamfcloud.com/JSSResource/…
WARNING: Make sure you limit your search and replace to only those instances that occur below the getAPIToken function definition. It is (and will be) the only place you still use basic authentication and you don’t want to change that one!
Before and After
Here is a screenshot of a GitHub diff of a script that has gone through the conversion.
Finding Files to Fix
I needed a way to search a client’s server for every script that needed updating and to track which scripts have been updated and which still need to be done. So I set up a private GitHub repo with the various snippets and functions I’ve described here. In it I have a folder for each of our clients to keep track of which scripts they have that make API calls. And in each of those folders I have a README.md file that contains a list of scripts that include the string JSSResource (a clear indication of an API call) and which of those do not contain the string Bearer since even those scripts that have been updated will contain a Basic authentication call to get the token in the first place. It’s been joked that if I had my way, I’d do all my work through the API and never use the web interface again. That might be exaggerating things a little, but not by much. The idea of logging into each client’s server (some using ?failover) and then clicking on each and every script and browsing the contents… does not appeal to me. And so I created the following script to go through every script on a Jamf server and identify which ones contain API calls and of those which need to be updated and which ones are fine. It’s output is in Markdown format so I can just put it into one of those README.md files for easy viewing.
#!/bin/zsh
:<<HEADER
Name: Find Files to Fix
Description: Locate scripts on a Jamf server that need to be updated from basic to token authentication
Created By: Chad Lawson
License: Copyright (c) 2023, Rocketman Management LLC. All rights reserved. Distributed under MIT License.
Usage: Update the JAMFURL, JAMFUSER, and JAMFPASS fields in the "Definitions" section below and run in the terminal.
Note: The generated report is formatted in Markdown. To change that, look at the printf line near the end.
HEADER
##
## Defintions
##
JAMFURL="https://pretendco.jamfcloud.com"
JAMFUSER=""
JAMFPASS=""
##
## Functions
##
function getAPIToken() {
jamfURL=$1
basicAuth=$2
authToken=$(curl -s \
--request POST \
--url "${jamfURL}/api/v1/auth/token" \
--header "Accept: application/json" \
--header "Authorization: Basic ${basicAuth}" \ 2>/dev/null
)
## Courtesy of Der Flounder
## Source: https://derflounder.wordpress.com/2021/12/10/obtaining-checking-and-renewing-bearer-tokens-for-the-jamf-pro-api/
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
api_token=$(/usr/bin/awk -F \" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
else
api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "$authToken")
fi
echo ${api_token}
}
function getField() {
needle=$1
haystack=$2
fieldText=$(echo "${haystack}" | xpath -e "/${needle}/text()" 2>/dev/null)
echo "${fieldText}"
}
##
## Main
##
## Let's start with an API token
basicAuth=$(echo -n "${JAMFUSER}:${JAMFPASS}" | base64)
token=$(getAPIToken "${JAMFURL}" "${basicAuth}")
if [[ "${token}" == "" ]]; then
echo "Error: Unable to authenticate"
exit 1
fi
## Get the computer IDs
SCRIPTLIST=( \
$(curl -s \
-H "Authorization: Bearer ${token}" \
"${JAMFURL}/JSSResource/scripts" \
| xmllint --format - \
| awk -F '[<>]' '/<id>/{printf "%s\n", $3}' \
) \
)
## Create the Markdown report
for script in ${SCRIPTLIST}; do
scriptData=$( \
curl -s \
-H "Authorization: Bearer ${token}" \
"${JAMFURL}/JSSResource/scripts/id/${script}" \
)
## From the database
name=$(getField "/script/name" "$scriptData")
contents=$(getField "/script/script_contents" "$scriptData")
if [[ ${contents} =~ "JSSResource" && \
! ${contents} =~ "Bearer" ]]; then
printf "[%s](https://%s/view/settings/computer-management/script/%s?tab=script)\n" ${name} ${JAMFURL} ${script}
fi
done
So each line has a checkbox that indicates if it’s done or not. And each name becomes a link directly to the script contents on the server.
This could easily be changed to CSV or any other format you like. All the data about the scripts are there.
A Light at the End of the Tunnel
(and it’s not an oncoming train)
Hopefully this takes some of the mystery and frustration out of the process. It’s been a heck of a journey so far and I’ve learned a lot. Last March I said that I would have preferred if Jamf had gone with JSON Web Tokens (JWTs) for their token format instead of their approach. Now I hope that they leave things as they are so I don’t have to go through all this again. It’s five o’clock somewhere, right?
Comments