talkingmoose
Moderator
Moderator

Jamf announced in January that the Jamf Pro Classic API will no longer support basic authentication in a future release. Classic API scripts can still run—administrators just need to make a simple change in how they authenticate.

Let’s look at why this change is coming and how to convert Classic API scripts to use bearer tokens for authentication instead of usernames and passwords. The token obtained during this workflow will allow authentication to either the Classic API or the more modern Jamf Pro API. We’ll cover:

  • How bearer tokens improve security
  • Request the first token
  • Parse the token
  • Use the token to send an API command
  • Expire the token
  • Renew the token

How bearer tokens improve security

This change to move away from supplying usernames and passwords as credentials for authenticating to Jamf Pro is a good thing.

Bearer tokens improve security by reducing the number of times credentials are sent to the server. Instead of sending them with every request, a token is sent instead, which expires automatically after 30 minutes. And they’re ephemeral—not stored on disk like credentials in a database, but rather kept only in memory.

They also mean nothing to anyone other than the Jamf Pro server that generates them. Unlike usernames and passwords, which might be used to also access other network resources, bearer tokens are just meaningless strings of characters to anyone who might intercept them.

Keep in mind a script still needs to transmit a username and password to first acquire a bearer token, but all following requests are done using a short-lived token instead.

Request the first token

Consider using a scripting application like CodeRunner to see how the following snippets work.

If your Classic API script hasn’t yet been updated to use bearer tokens, it’s using “basic” authentication. The username and password it’s sending is still secure during transit to the server because its SSL certificate is providing encryption along the way.

Here’s what the start of the old script may look like. It may assign a username, password, and Jamf Pro server URL to some variables at the beginning to let us specify them once.

 

 

 

#!/bin/zsh

# server and credential information
jamfProURL=https://talkingmoose.jamfcloud.com
username="jamfadmin"
password="P@55w0rd" 

 

 

 

We’ll keep this.

What we need to do next is use our credentials to request a token from the Jamf Pro server. Paste this after the server and credential information above:

 

 

 

# request auth token
authToken=$( /usr/bin/curl \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/token" \
--user "$username:$password" )

echo "$authToken"

 

 

 

When we run this in CodeRunner with an echo at the end to print our results, it displays the token information returned from the server.

CalleyO_0-1663352701574.png

 

Parse the token

Notice the token information we received contains two pieces of information—“token” and “expires”. Only the token is important if we know the script will run very quickly (less than 30 minutes). Doing something like updating device asset tags from a spreadsheet could take longer than 30 minutes, so we’ll want to make sure we renew the token before it expires. We’ll talk about renewing the token a little later.

For now, we need to separate what we’ve received into useable pieces of information. Let’s put the token information into a “token” variable. Let’s put the “expires” information into a “tokenExpiration” variable. And let’s put a converted form of the expiration information into a “localTokenExpirationEpoch” variable. Paste this next:

 

 

 

# parse auth token
token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )

tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )

localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )

echo Token: "$token"
echo Expiration: "$tokenExpiration"
echo Expiration epoch: "$localTokenExpirationEpoch"

 

 

 

The last three lines echo the results of what we parsed.

CalleyO_1-1663352701581.png

 

First, we get “Token” by itself.

Second, we get the “Expiration” by itself (2022-09-09T19:17:36.34Z). This is an expiration date for when the token will no longer be valid and it’s always 30 minutes into the future.

Finally, we convert the “Expiration” information into something called an epoch date. A Unix epoch date is literally the number of seconds since January 1, 1970. The “Expiration epoch” date is “1662751056”. When we have a straightforward number like this, we can do math calculations very easily.

Note the “TZ=UTC” item in front of the date command for the localTokenExpirationEpoch variable. This is a time zone environment variable and it’s defining the date as Universal Coordinated Time (+0000). It’s the same as the GMT+0 prime meridian that defines the starting point for time zones in the world.

The server is responding with an expiration date in the UTC time zone, not our local time zone. But the expiration date doesn’t say that. By providing the environment variable, we’re telling the date command the originating time zone and it’ll automatically convert to our local time zone when giving us the epoch date.

We can confirm this in Terminal by running /bin/date -r 1662751056. It converts the epoch date back to a date we can read: Fri Sep  9 14:17:36 CDT 2022. This shows a corrected time that is five hours different from the UTC time the server sent. Without this correction, our calculations could be up to 12 hours off. And it works regardless of whether Daylight Saving Time is in effect.

Use the token to send an API command

With basic authentication, the username and password are sent each time to the server when we use the curl command to request or change information. Here’s an example of what might come next in an unconverted Classic API script where we want to get a list of computer groups. Go ahead and paste this next:

 

 

 

# get list of static groups
computerGroupXML=$( /usr/bin/curl \
--header "Accept: text/xml" \
--request GET \
--silent \
--url "$jamfProURL/JSSResource/computergroups" \
--user "$username":"$password" )

echo "$computerGroupXML"

 

 

 

If we run the entire script so far, we should get a string of XML that denotes several computer groups.

CalleyO_2-1663352701586.png

 

Now, let’s convert the command to use the bearer token instead of basic authentication. It’s simple to do. We’ll replace:

     --user "$username":"$password"

with

     --header "Authorization: Bearer $token"

and run the script again to test. We should get the same thing.

CalleyO_3-1663352701590.png

 

That’s it! That’s the basic change we need to make. We’re keeping our username, password, and server information that we had originally. But we used it instead to ask for a token and then simply replaced our credentials with the token when sending the API command. All we need to do is update all our other scripts to do the same thing.

We’re not done yet, though.

Expire the token

Remember, the token is valid for 30 minutes. If all we’re going to do is make a one-time Classic API call that takes just a few seconds to run and finish, we don’t need the token anymore. A good practice is to expire the token at the end of the script. Paste this near the end of your script where no further API calls will be made:

 

 

 

# expire auth token
/usr/bin/curl \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/invalidate-token"

 

 

 

We don’t see anything change when we run the script again. But we also don’t see any errors.

CalleyO_4-1663352701595.png

 

Did it work?

Normally, we can rely on this working, but just to test this one time (or if you’d like to always test), we can add a command to verify whether the token is still valid. Paste this after the command expiring the token:

 

 

 

# verify auth token is valid
checkToken=$( /usr/bin/curl \
--header "Authorization: Bearer $token" \
--silent \
--url "$jamfProURL/api/v1/auth" \
--write-out "%{http_code}" )

tokenStatus=${checkToken: -3}
echo Token status: "$tokenStatus"

 

 

 

This runs one more API command, which attempts to authenticate with the bearer token. At the end, it includes a --write-out argument. Every command we’ve sent to the server has returned an HTTP status code. Here’s a list of some that we might receive:

 

 

 

200 Request successful
201 Request to create or update object successful
400 Bad request
401 Authentication failed
403 Invalid permissions
404 Object/resource not found
409 Conflict
500 Internal server error

 

 

 

The --write-out argument instructs the curl command to show us the code returned from our API call. When we run the script with the additional code to check our token status, we see the result on the last line.

CalleyO_5-1663352701599.png

 

A 401 code lets us know “Authentication failed” or that our bearer token is expired and no longer works.

Renew the token

What if our script is still running after 30 minutes? Won’t the token expire?

Yes, it will, and the script will stop working. This is where getting the expiration date of the token and the converted epoch date is useful. What we need to do is add something that’s tracking the time and will renew the token just before it expires. We can do that with an “if” statement and a function.

First, we need to include in our script everything above that we’ve added: requesting and parsing the token, using it to make a Classic API command, and then expiring it. Now, we’ll add some more code to check for the pending expiration and renew the token.

The reason a script may take longer than 30 minutes to complete is that it’s probably looping through something in Jamf Pro. Let’s assume it’s looping through devices without an asset tag and updating each device with an asset tag from a spreadsheet. That part of the script may look something like this:

 

 

 

while IFS= read aDevice
do
# run Classic API commands here
# to update a device
# and keep looping
done <<< "$listOfDevices"

 

 

 

The commands to update the device aren’t important here—just their location within the while loop.

What we need to do with each iteration of the loop is verify whether the token is close to expiring. Instead of waiting until the last minute, though, we need to renew it with a little time to spare. Let’s choose 5 minutes before it expires. Paste this above the while loop:

 

 

 

# subtract five minutes (300 seconds) from localTokenExpirationEpoch
renewalTime=$(( $localTokenExpirationEpoch - 300 ))

 

 

 

We’ll renew the token around 25 minutes instead of 30. Calculating the renewal time is as simple as subtracting 300 (5 minutes) from the epoch date.

How many times will we need to renew the token? Once? A few times? We don’t know. So, something like this is best handled as a function in our script that we can call any time we need it. Let’s name that function “renewToken” and add it inside the while loop with some additional code. Here’s what we’ll have so far.

 

 

 

# subtract five minutes (300 seconds) from localTokenExpirationEpoch
renewalTime=$(( $localTokenExpirationEpoch - 300 ))

while IFS= read aDevice
do
	now=$( /bin/date +"%s" )

	if [[ "$renewalTime" -lt "$now" ]]; then
	   renewToken
	fi

# run Classic API commands here
# to update a device
# and keep looping
done <<< "$listOfDevices"

 

 

 

As the script loops through our list of devices to update their asset tags, the first thing it’ll do is verify whether the token’s renewal time is less than the time right now. That won’t happen for about 25 minutes, so the loop will carry on updating the device’s asset tag.

After 25 minutes of updating asset tags, the renewal time arrives! That’s when the script calls the “renewToken” function that we’ll add next. A function is like a mini script within the script. With just one word, we can call that mini script any time we’d like.

Here’s the function; paste it anywhere in the script so long as it’s somewhere before the while loop.

 

 

 

renewToken()	{

	# renew auth token
	authToken=$( /usr/bin/curl \
	--header "Accept: application/json" \
	--header "Authorization: Bearer $token" \
	--request POST \
	--silent \
	--url "$jamfProURL/api/v1/auth/keep-alive" )
	
	# parse auth token
	token=$( /usr/bin/plutil \
	-extract token raw - <<< "$authToken" )
	
	tokenExpiration=$( /usr/bin/plutil \
	-extract expires raw - <<< "$authToken" )
	
	localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
	-f "%Y-%m-%dT%T" "$tokenExpiration" \
	+"%s" 2> /dev/null )
	
	# update the renewal time for another 25 minutes
	renewalTime=$(( $localTokenExpirationEpoch - 300 ))

}

 

 

 

It looks like a lot of the same code we’ve already been using. And it is for the most part.

The biggest difference is the command at the top for renewing the auth token. It’s using the old token to request a new one! That means the script has still only ever sent the username and password credentials to the server just one time.

The rest of the function parses for the token and new expiration date, generates the expiration epoch date, and updates the renewal time for another 25 minutes.

Although renewing the Classic API token is really for scripts that could take longer than 30 minutes to complete, you can still add the code for it to your shorter existing scripts. It’ll make your scripts longer and more complex; however, sometimes having just one “template” script to maintain can be easier than trying to determine when you need just some or all the code. As you gain experience with the Classic API and scripting, you’ll know when to omit unnecessary code.

Some closing thoughts

Because of how some websites format their content, copying snippets and pasting them into scripts or script editors may not always work. Verify that single and double dashes like the ones in front -f or --header haven’t been automatically converted to other characters like long em dashes. Retype them if you need. Also, quotes should be straight " not curly “”. And wherever you see a backslash at the end of a line in the examples above, that just means the command has been split onto multiple lines to make it easier to write here.

Something like this:

/usr/bin/plutil \

-extract expires raw - <<< "$authToken"

is the same as:

/usr/bin/plutil -extract expires raw - <<< "$authToken"

You might say, “I’m not a scripter. I just use what I find online.” That’s OK, but what you find may be an outdated script that’s still using basic authentication. In the future, that script won’t work—only because it can’t authenticate correctly with your Jamf Pro server. Updating it to use bearer token authentication isn’t difficult and should be mostly copy/paste from what’s presented here.

If you don’t like showing username and password credentials in your scripts, you can use base64 to obfuscate them. This isn’t encryption. Anyone can still reverse the encoding and see the credentials. But it may give you a little extra piece of mind.

Once you’ve converted all your scripts to use bearer tokens for authentication, do one more thing to improve security. In Jamf Pro, go to the Settings cog > System Settings > Jamf Pro User Accounts & Groups > Password Policy. Disable the option to allow basic authentication for your Classic API scripts.image (4).png

In the future, Jamf will remove this option altogether. For more information about this change, see the Classic API Authentication Changes page in the Jamf Developer Portal.

36 Comments
curran
New Contributor III

Very thorough and helped me grasp the concepts right away. I enjoyed the format of this article and learned a few new tricks that were explained well enough that they were easily incorporated into my existing scripts.

donmontalvo
Esteemed Contributor III

THIS is the correct way to document stuff. :) #kudogiven

ChaseEndy
New Contributor III

Thank you for this!!

hansjoerg_watzl
Contributor II

Really GOOD post/documentation! Correct structure, easy to follow and works!
Thanks!!

rastogisagar123
Contributor II

Clean and Easy to understand Document. William Sir is always great!!

pbenware1
Release Candidate Programs Tester

I love this article.  Clear, concise, well written with good examples, and so happens to inform me on an issue I'm working on now with our Service Now integration.

Thank you!

burdett
Contributor II

@talkingmoose This is very helpful.  Do you have any suggestion on how one could test the credentials before making the token request and echo the reason, like Authentication failed?  Somehow in deploying my API script I managed to disable the account I was using to get the token, and it took me for ever of figure out I didn't break the script, but the account was disabled.

talkingmoose
Moderator
Moderator

@burdett I have a script you can use as a reference to do this.

https://gist.github.com/ad57503c09fac1ba73768c32b187284b

Similar to this script, you can add the list of HTTP error cords at the top of the script. You'd also need to add the --write-out "%{http_code}" option to your curl command to additionally retrieve the result code. You can parse out that three digit code and echo it in your script output.

Malcolm
Contributor II

@talkingmoose 

I use a lot of x get command for API so it passes it all through one line into a variable.

how would I rewrite the following...

 

strexistingname=`/usr/bin/curl -s -u "apiassigneduser":"PASSWORD" -X GET "https://mdmurl:8443/JSSResource/computers/match/$strmacbookserial" -H "accept: application/xml" | awk -F'\\>\\<' '{$1=$1}1' OFS='\n<' | grep '<name>' | sed -e 's/name>//g' | sed -e 's.</name..g' | sed -e 's.<..g'`

 

 

sk25
Contributor

@talkingmoose Kindly advice on how to get the bearer if SSO enabled for the Jamf login? We have 2 tenant of Jamf. One we can login using SSO and another we can login using local Jamf account. I'm able to execute the script successfully for the local Jamf account (However, renewal part alone getting error message). Whereas unable to execute the script if SSO enabled Jamf account. 

sk25
Contributor

@all Kindly advice on the below as I'm getting error message

Token status: 401

<stdin>: Could not extract value, error: No value at that key path or invalid key path: expires'

Below is the code,

#! /bin/zsh

# server and credential information

jamfProURL="https://XX.jamfcloud.com"

username="XX"

password="XX"

 

# request auth token

 authToken=$( /usr/bin/curl \

 --request POST \

 --silent \

 --url "$jamfProURL/api/v1/auth/token" \

 --user "$username:$password" )

 

echo "$authToken"

 

renewToken()    {

 

    # renew auth token

     authToken=$( /usr/bin/curl \

     --header "Accept: application/json" \

     --header "Authorization: Bearer $token" \

     --request POST \

     --silent \

     --url "$jamfProURL/api/v1/auth/keep-alive" )

 

# parse auth token

token=$( /usr/bin/plutil -extract token raw - <<< "$authToken" )

tokenExpiration=$( /usr/bin/plutil -extract expires raw - <<< "$authToken" )

localTokenExpirationEpoch=$( TZ=GMT /bin/date -j -f "%Y-%m-%dT%T" "$tokenExpiration" "+%s" 2> /dev/null )

 }

echo Token: "$token"

echo Expiration: "$tokenExpiration"

echo Expiration epoch: "$localTokenExpirationEpoch"

# get list of static groups

computerGroupXML=$( /usr/bin/curl \

--header "Accept: text/xml" \

--request GET \

--silent \

--url "$jamfProURL/JSSResource/computergroups" \

--header "Authorization: Bearer $token" )

# --user "$username":"$password" )

echo "$computerGroupXML"

# expire auth token

/usr/bin/curl \

--header "Authorization: Bearer $token" \

--request POST \

--silent \

--url "$jamfProURL/api/v1/auth/invalidate-token"

 

# verify auth token is valid

checkToken=$( /usr/bin/curl \

--header "Authorization: Bearer $token" \

--silent \

--url "$jamfProURL/api/v1/auth" \

--write-out "%{http_code}" )

 

tokenStatus=${checkToken: -3}

echo Token status: "$tokenStatus"

 

# update the renewal time for another 25 minutes

   renewalTime=$(( $localTokenExpirationEpoch - 300 ))   

while IFS= read aDevice

do

    now=$( /bin/date "+%s" )

    if [[ "$renewalTime" -lt "$now" ]]; then

       renewToken

    fi

# run Classic API commands here

# to update a device

# and keep looping

done <<< "$listOfDevices"

/usr/bin/plutil \

-extract expires raw - <<< "$authToken"

talkingmoose
Moderator
Moderator

@Malcolm, what you have seems to work just fine. You might be interested in what xpath can do for you when parsing XML. Here’s what I put together. It’s still one line. I just like breaking it into multiple rows for readability.

strexistingname=$( /usr/bin/curl \
--header "accept: application/xml" \
--request GET \
--silent \
--user "apiassigneduser":"PASSWORD" \
--url "https://mdmurl:8443/JSSResource/computers/serialnumber/$strmacbookserial" \
| /usr/bin/xpath -e '/computer/general/name/text()' 2> /dev/null ) 

 

talkingmoose
Moderator
Moderator

@sk25The Jamf Pro API should bypass Single Sign-On. If you’re having issues connecting with your account, consider creating a new account to use as an API-only service account. You can find a list of minimum privileges for each endpoint at https://developer.jamf.com/jamf-pro/docs/privileges-and-deprecations.

talkingmoose
Moderator
Moderator

@sk25 When assembling the full script from the code snippets, it appears you had some items out of order. Here’s the fully assembled script:

#!/bin/zsh
 
# server and credential information
username="jamfadmin"
password=“P@55w0rd"
 
renewToken() {
 
# renew auth token
authToken=$( /usr/bin/curl \
--header "Accept: application/json" \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/keep-alive" )
 
# parse auth token
token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )
 
tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )
 
localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )
 
# update the renewal time for another 25 minutes
renewalTime=$(( $localTokenExpirationEpoch - 300 ))
 
}
 
# request auth token
authToken=$( /usr/bin/curl \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/token" \
--user "$username:$password" )
 
echo "$authToken"
 
# parse auth token
token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )
 
tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )
 
localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )
 
echo Token: "$token"
echo Expiration: "$tokenExpiration"
echo Expiration epoch: "$localTokenExpirationEpoch"
 
# subtract five minutes (300 seconds) from localTokenExpirationEpoch
renewalTime=$(( $localTokenExpirationEpoch - 300 ))
 
while IFS= read aDevice
do
now=$( /bin/date +"%s" )
 
if [[ "$renewalTime" -lt "$now" ]]; then
  renewToken
fi
 
# get list of static groups
computerGroupXML=$( /usr/bin/curl \
--header "Accept: text/xml" \
--request GET \
--silent \
--url "$jamfProURL/JSSResource/computergroups" \
--header "Authorization: Bearer $token" )
 
done <<< "$listOfDevices"
 
echo "$computerGroupXML"
 
# expire auth token
/usr/bin/curl \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/invalidate-token"
 
# verify auth token is valid
checkToken=$( /usr/bin/curl \
--header "Authorization: Bearer $token" \
--silent \
--url "$jamfProURL/api/v1/auth" \
--write-out "%{http_code}" )
 
tokenStatus=${checkToken: -3}
echo Token status: "$tokenStatus"
sk25
Contributor

@talkingmoose As per the below, Jamf Pro API and Classic API will Authenticate local Jamf username and password. It won't support SSO. Thanks.

sk25_0-1681982885313.png

 

sk25
Contributor

@talkingmoose Thanks for the reassemble script. However, I'm getting the static group list but token status still showing as 401. 

As per privilege, I've read access to computer smart groups, computer static group and all access(create, read, update and delete) for API integrations and API roles. Apart from this any-other privilege required not to get token status as 401? Thanks.

talkingmoose
Moderator
Moderator

@sk25 The end of the script expires the token and validates it's no longer active. The 401 is confirming the token doesn't work, which is expected. You can delete the validation at the end to avoid that message or modify the script to present that information differently.

Malcolm
Contributor II

@talkingmoose 

Thanks for taking time to reply regards to:

strexistingname=$( /usr/bin/curl \
--header "accept: application/xml" \
--request GET \
--silent \
--user "apiassigneduser":"PASSWORD" \
--url "https://mdmurl:8443/JSSResource/computers/serialnumber/$strmacbookserial" \
| /usr/bin/xpath -e '/computer/general/name/text()' 2> /dev/null ) 

 

How I understood it though, was that the use of the --user was to be done away with and to only use a token via the {--header "Authorization: Bearer $token"} after the initial token is received? or is it more the case that the token should be received before continuing with using the --user process for api?

talkingmoose
Moderator
Moderator

@Malcolm

Your script will still need to use username and password to get the initial token. That’s a one-time authentication. From there you can keep the token active until you’re ready to expire it and never have to send the credentials again.

sk25
Contributor

@talkingmoose Seems that your script worked well and also after modification of token validation everything works well. Thanks for your guidance.

Malcolm
Contributor II
Thanks, I was mostly concerned that I couldn’t use the user credential API commands when the API no longer supports user auth, but if your saying, as long as I user auth and get the token, I can run subsequent API query with the API user credentials prior to ending and killing the token… then this should be a fairly easy change to my scripts, by simply adding the token processing to the top of bottom of my scripts.

It was initially assumed I would have to rework the API commands, to use:

--header "Authorization: Bearer $token"

Is that correct?
IMPORTANT - This email and any attachments may be confidential. If received in error, please contact us and delete all copies. Before opening or using attachments check them for viruses and defects. Regardless of any loss, damage or consequence, whether caused by the negligence of the sender or not, resulting directly or indirectly from the use of any attached files our liability is limited to resupplying any affected attachments. Any representations or opinions expressed are those of the individual sender, and not necessarily those of the Department of Education.
talkingmoose
Moderator
Moderator

@Malcolm There’s a difference between using your credentials to authenticate each command you send to the API and using your credentials for requesting authorization to acquire a bearer token.

Basic authentication (username and password), which Jamf has deprecated and will remove in the future, is sent with each command to the API.

Bearer authentication is used only at the beginning of the script to acquire a token. The token is used throughout the rest of the script.

And you’ve got the right idea about changing your scripts. It’s pretty simple to add the token processing at the beginning, which will continue to use:

     --user "$username":"$password"

But throughout the rest of your script, you’ll use this instead:

     --header "Authorization: Bearer $token"

We can’t completely eliminate sending the username and password, but using a token with each command is far more secure than sending the credentials every time

Malcolm
Contributor II

@talkingmoose 

so to revisit my initial inquiry with the below example:

strexistingname=`/usr/bin/curl -s -u "apiassigneduser":"PASSWORD" -X GET "https://mdmurl:8443/JSSResource/computers/match/$strmacbookserial" -H "accept: application/xml" | awk -F'\\>\\<' '{$1=$1}1' OFS='\n<' | grep '<name>' | sed -e 's/name>//g' | sed -e 's.</name..g' | sed -e 's.<..g'`
strexistingname=$( /usr/bin/curl \
--header "accept: application/xml" \
--request GET \
--silent \
--user "apiassigneduser":"PASSWORD" \
--url "https://mdmurl:8443/JSSResource/computers/serialnumber/$strmacbookserial" \
| /usr/bin/xpath -e '/computer/general/name/text()' 2> /dev/null ) 

your example is using the --user field

but the object is to use the --header "Authorization: Bearer $token"

I have successfully been able to follow your instructions for using the : --header "Authorization: Bearer $token"

but the code I am attempting to convert, didn't seem to accept using: --header "Authorization: Bearer $token"

And your suggestion and confirmation was indicating it was still using the --user option

which is where my confusion is.

talkingmoose
Moderator
Moderator

@Malcolm I interpreted your original question to be looking for ways to improve your code. My suggestion was to look at xpath. I wasn't commenting on authentication.

Yes, you should definitely change your snippet to use the bearer token instead of basic aurhentication.

Malcolm
Contributor II
Thank you very much, I also like the way you code the retrieval of information better.
IMPORTANT - This email and any attachments may be confidential. If received in error, please contact us and delete all copies. Before opening or using attachments check them for viruses and defects. Regardless of any loss, damage or consequence, whether caused by the negligence of the sender or not, resulting directly or indirectly from the use of any attached files our liability is limited to resupplying any affected attachments. Any representations or opinions expressed are those of the individual sender, and not necessarily those of the Department of Education.
aghali
New Contributor III

I can't thank you enough! Excellent article, very detailed and extremely helpful.

MacJunior
Contributor III

How do I use encoded credentials with bearer authentication?

howie_isaacks
Valued Contributor II

This was very helpful. Reading this and trying out all of the steps helped me learn how to use the API in scripts. Today, I created a new script that uses the API to assign Macs to sites. This has been a big issue for us since we have several Jamf admins who can only access their assigned site. We had a lot of Macs assigned to the wrong site or not assigned to any. I was able to automate the assignments using the script.

Malcolm
Contributor II

@talkingmoose 

I'm looking at trying to do the same thing but for a powershellscript, have you had any success doing that?

 

the intent is to populate our DHCP with approved access policies for mdm bound Mac addresses, so that we can curb user wifi enrolment of non bound devices, which are also generating multiple IP reservations in our DHCP, due to the changing Mac addresses, where the device causing it is unmanaged.

HenryOzsoy
New Contributor III

Hi all,

I have a shell script that will take all app IDs I have listed and add them to a category ID I have listed. I just can't seem to incorporate this into @talkingmoose method of using bearer token.

Can anyone point me in the right direction?

 

 

#!/bin/sh
TARGET=(29 58 64 65 87 88 133 157 158 169 190 205 236 267 271 278 279 284 290 315 318 337 343 371 379 381 389 395 406 411 415 434 444 449 479 489 512 513 535 563 567 572 574 576 579 590 591 595 597 598 599 605 630 642 654 657 660 680 688 694 738 740 751 752)
CATEGORY=24

cat $TARGET | while read APP; do
curl -sk -u @username:@password -H "Content-Type: text/xml" -d "<?xml version="1.0" encoding="ISO-8859-1"?><mobile_device_application><general><category><id>$CATEGORY</id></category></general></mobile_device_application>" https://$jamfProURL/JSSResource/mobiledeviceapplications/id/$APP -X PUT

done

 

 

HenryOzsoy
New Contributor III

so far I got 

#!/bin/zsh

# server and credential information
jamfProURL=https://www.myjamfurl.com:8443/
username="username"
password="password"
  
renewToken() {
  
# renew auth token
authToken=$( /usr/bin/curl \
--header "Accept: application/json" \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/keep-alive" )
  
# parse auth token
token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )
  
tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )
  
localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )
  
# update the renewal time for another 25 minutes
renewalTime=$(( $localTokenExpirationEpoch - 300 ))
  
}
  
# request auth token
authToken=$( /usr/bin/curl \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/token" \
--user "$username:$password" )
  
echo "$authToken"
  
# parse auth token
token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )
  
tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )
  
localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )
  
echo Token: "$token"
echo Expiration: "$tokenExpiration"
echo Expiration epoch: "$localTokenExpirationEpoch"
  
# subtract five minutes (300 seconds) from localTokenExpirationEpoch
renewalTime=$(( $localTokenExpirationEpoch - 300 ))
  
while IFS= read aDevice
do
now=$( /bin/date +"%s" )
  
if [[ "$renewalTime" -lt "$now" ]]; then
  renewToken
fi
  
# change app categorys
TARGET=(29 58 64 65 87 88 133 157 158 169 190 205 236 267 271 278 279 284 290 315 318 337 343 371 379 381 389 395 406 411 415 434 444 449 479 489 512 513 535 563 567 572 574 576 579 590 591 595 597 598 599 605 630 642 654 657 660 680 688 694 738 740 751 752)
CATEGORY=24
APP="<mobile_device_application><general><category><id>$CATEGORY</id></category></general></mobile_device_application>"
$appcategory=$( /usr/bin/curl \
--header "Accept: text/xml" \
--request PUT \
--silent \
--url "$jamfProURL/JSSResource/mobiledeviceapplications/id/$APP" \
--header "Authorization: Bearer $token" )
  
done <<< "$appcategory"

  
# expire auth token
/usr/bin/curl \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/invalidate-token"
  
# verify auth token is valid
checkToken=$( /usr/bin/curl \
--header "Authorization: Bearer $token" \
--silent \
--url "$jamfProURL/api/v1/auth" \
--write-out "%{http_code}" )
  
tokenStatus=${checkToken: -3}
echo Token status: "$tokenStatus"


yet I'm getting a 

Token: <stdin>: Could not extract value, error: No value at that key path or invalid key path: token

Expiration: <stdin>: Could not extract value, error: No value at that key path or invalid key path: expires

Expiration epoch:

Untitled.sh: line 78: =: command not found

Token status: 000
error.

Malcolm
Contributor II

@HenryOzsoy 

I am going to be inclined to say its the put command.

How I am reading it is:

$appcategory=$( /usr/bin/curl \

--header "Accept: text/xml" \

--request PUT \

--silent \

--url "$jamfProURL/JSSResource/mobiledeviceapplications/id/<mobile_device_application><general><category><id>$CATEGORY</id></category></general></mobile_device_application>"

 

I think firstly you want:

--url "$jamfProURL/JSSResource/mobiledeviceapplications/id/$TARGET”

not

--url "$jamfProURL/JSSResource/mobiledeviceapplications/id/$APP”


This is some example code that uses PUT that will insert a variable into the Purchasing_Contact field of a device.

so I think your xml section needs to be similar...

I will try and re-write your section there in the way I think it should be written in a follow up post... But I would advise to test it against a single Target first.

            echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
			<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">
  			<xsl:output method=\"text\"/>
  			<xsl:template match=\"/\">
   			<xsl:value-of select=\"id\"/>
  			</xsl:template>
			</xsl:stylesheet>" > /private/var/tmp/stylesheet.xslt

  			curl -s -k \
    		-H "Authorization: Bearer $jamfProApiToken" \
    		-H "Content-type: application/xml" \
    		-X PUT \
    		-d "<computer><purchasing><purchasing_contact>$updatedmdmdeviceName</purchasing_contact></purchasing></computer>" \
    		"${jamfProURL}/JSSResource/computers/id/$computerID"
			echo "name change applied $updatedmdmdeviceName"

 

HenryOzsoy
New Contributor III

Thanks, @Malcolm . To follow up on your comment, I modified the script. I'm not getting any errors now, yet no change has occurred in the app category post-script.

#!/bin/sh

# server and credential information
jamfProURL=https://www.myjamfurl.com:8443
username="username"
password="password"

renewToken() {
  
  # renew auth token
  authToken=$( /usr/bin/curl \
--header "Accept: application/json" \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/keep-alive" )
  
  # parse auth token
  token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )
  
  tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )
  
  localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )
  
  # update the renewal time for another 25 minutes
  renewalTime=$(( $localTokenExpirationEpoch - 300 ))
  
}

# request auth token
authToken=$( /usr/bin/curl \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/token" \
--user "$username:$password" )

echo "$authToken"

# parse auth token
token=$( /usr/bin/plutil \
-extract token raw - <<< "$authToken" )

tokenExpiration=$( /usr/bin/plutil \
-extract expires raw - <<< "$authToken" )

localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \
-f "%Y-%m-%dT%T" "$tokenExpiration" \
+"%s" 2> /dev/null )

echo Token: "$token"
echo Expiration: "$tokenExpiration"
echo Expiration epoch: "$localTokenExpirationEpoch"

# subtract five minutes (300 seconds) from localTokenExpirationEpoch
renewalTime=$(( $localTokenExpirationEpoch - 300 ))

while IFS= read aDevice
do
  now=$( /bin/date +"%s" )
  
  if [[ "$renewalTime" -lt "$now" ]]; then
    renewToken
  fi
  
  # change app categorys
  TARGET=29
  CATEGORY=24

  appcategory=$( /usr/bin/curl \
--header "Accept: text/xml" \
--request PUT \
--silent \
--url "$jamfProURL/JSSResource/mobiledeviceapplications/id/$TARGET/<mobile_device_application><general><category><id>$CATEGORY</id><name>Applications</name></category></general><mobile_device_application>" \
--header "Authorization: Bearer $token" )
  
done <<< "$changeappcategorys"

echo "$appcategory"

# expire auth token
/usr/bin/curl \
--header "Authorization: Bearer $token" \
--request POST \
--silent \
--url "$jamfProURL/api/v1/auth/invalidate-token"

# verify auth token is valid
checkToken=$( /usr/bin/curl \
--header "Authorization: Bearer $token" \
--silent \
--url "$jamfProURL/api/v1/auth" \
--write-out "%{http_code}" )

tokenStatus=${checkToken: -3}
echo Token status: "$tokenStatus"
Malcolm
Contributor II

@HenryOzsoy 

try this:

 

            echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
			<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">
  			<xsl:output method=\"text\"/>
  			<xsl:template match=\"/\">
   			<xsl:value-of select=\"id\"/>
  			</xsl:template>
			</xsl:stylesheet>" > /private/var/tmp/stylesheet.xslt

  			curl -s -k \
    		-H "Authorization: Bearer $token" \
    		-H "Content-type: application/xml" \
    		-X PUT \
    		-d "<mobile_device_application><general><category><id>$CATEGORY</id></category></general></mobile_device_application>" \
    		"${jamfProURL}/JSSResource/mobiledeviceapplications/id/$TARGET"
			echo "Category change Category $CATEGORY to application $TARGET"

 

 

But I would suggest trying this first against just one of your targets:

 

            echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
			<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">
  			<xsl:output method=\"text\"/>
  			<xsl:template match=\"/\">
   			<xsl:value-of select=\"id\"/>
  			</xsl:template>
			</xsl:stylesheet>" > /private/var/tmp/stylesheet.xslt

  			curl -s -k \
    		-H "Authorization: Bearer $token" \
    		-H "Content-type: application/xml" \
    		-X PUT \
    		-d "<mobile_device_application><general><category><id>24</id></category></general></mobile_device_application>" \
    		"${jamfProURL}/JSSResource/mobiledeviceapplications/id/29"
			echo "Category change category 24 on app 29"

 

 

HenryOzsoy
New Contributor III

Thanks, @Malcolm works great.

Malcolm
Contributor II

@HenryOzsoy 

Happy to have helped, it took me a long time to work out the put command the right way, I had an old script for forcing old assets to unmanaged a Jamf employee helped me with because I wanted to retain the information of the previously managed devices, such as the activation lock and FileVault key.

I later ended up needing to do it for a different purpose, and had to dissect their code to work it out.

I use it to auto name devices, using assigned users and prefix information I append to the device, set via a prestage enrolment group, the trouble I had was I had a lot of devices, in presages where the data wasn;t accurate as moving a device from one prestage to another would leave the old prestage information on the device record, so I had to query the presages for the device id, then collect the correct prestage prefix info I would store in the prestage purchasing section of the prestage info, then I would update that data back to the device record, and then use that data with the assigned user to name the device.

I then later on used it to also bind devices that were shared devices, by using a purchasing field for the string AD, which then in the same naming script, would also domain bind the device.

Contributors
About the Author
Jamf, Mac administrator, former Microsoft MVP.