arminBriegel
New Contributor III
New Contributor III

Many organizations recommend or prescribe a specific web browser for their users. The user can change the setting in the user interface, even though it is not really obvious where to look. Many MacAdmins would like to pre-set this browser and the default during enrollment. However, there are several challenges associated with this in macOS.

It would be really nice if Apple provided some means to manage this with a configuration profile. If you agree, please file feedback with your AppleSeed for IT account.

While we wait for that, what can we do?

 

Use the browsers

Most third party browsers have a built-in command option to set themselves as the default browser. Most Chromium based browsers (Google Chrome, MS Edge, etc.) can be launched with the --make-default-browser option:

open -a "Google Chrome" --new --args --make-default-browser
open -a "Microsoft Edge" --new --args --make-default-browser

For Firefox, the options are different:

open -a "Firefox" --new --args -silent -nosplash -setDefaultBrowser

 

The user prompt

When you run any of these commands, it will take a second or so and then you will see a prompt to switch the default browser.

FirefoxPrompt.png

Apple is requiring user confirmation to change in the default browser. I assume there has been too much abuse by malware setting itself as the default for all web traffic.

This only applies to changing the default app for http, https, or the html file extension. You can still change the default for other URL schemes without a user prompt. (We will get to that later.)

There is another way that macOS treats the default browser settings specially. You cannot set separate default apps for http and https. When you try to change the default app for https, you will get an error.

 

Bypassing the user prompt

This script, which manipulates the launch services property list directly, is often recommended to change the default browser (and email app) without the prompt.

However, in my testing, this script requires the current user to log out after it runs, so that LaunchServices reloads the new settings before it overwrites them. It also depends on the locations and format of the LaunchServices file not changing, or getting locked away in future macOS releases.

If you can schedule this script as part of an enrollment workflow which already contains a reboot (for FileVault enablement or some other reason), then it should work fine (for now). But for other situations, I would not rely on hacking the property list, but prefer to use the supported means.

 

Using the API

As far as I know, Safari does not provide a command line option. You may also want to switch the default app for other url schemes, such as mailto, ssh, sftp or vnc.

You could hack the LaunchServices file, but should avoid that approach as it requires a logout or reboot and may not be stable or reliable. Apple provides a supported way to change this setting in the NSWorkspace AppKit API. MacAdmins usually don’t prefer to build Swift tools for our automations. (Though we could, and I did that in my 2022 MacSysAdmin presentation.)

In previous versions of macOS, Python 2 was pre-installed with the PyObjC bridge that allowed access to these APIs from a Python script, which was a useful solution for MacAdmins. I believe this is a big reason for Python’s popularity among MacAdmins. However, Python and the PyObjcC module are no longer pre-installed on macOS as of macOS 12.3. You can install your own Python 3, together with the PyObjC module, to keep using Python for these tasks. The MacAdmin’s python project provides such an installer. You will have to update older python scripts that were using Python 2 to Python 3 syntax and modules.

However, AppleScript also has an Objective-C bridge, which is pre-installed with macOS. We can call AppleScript/Objective-C functions from a script using osascript. I also prefer using the JavaScript version of AppleScript (JXA), since the Objective-C Framework syntax translates more easily to JavaScript than to “traditional” AppleScript.

This all seems a bit convoluted but serves us really well for the purpose of calling a single AppKit API function without requiring to install a Python runtime or compiling and installing a Swift or Objective-C binary.

To make things even more complicated, Apple introduced new APIs to change the default app in macOS Monterey 12. This is a good thing as the old API has been long deprecated with no replacement. Apple removed the old API in macOS Ventura, so our code has to handle both cases if we want to support versions of macOS older than Monterey.

Jamf Pro policy scripts and extension attributes run as root. Since the settings for default apps are stored on the user-level, we have to ensure the command runs as the current user. This is generally a good practice for osascript, anyway.

You can find the code to set the default app for an arbitrary URL scheme in this gist.

The script takes two arguments: the first is the url scheme (http, mailto, ssh, etc.) and the second is the bundle identifier for the app that should be the new default (com.apple.safari, or com.microsoft.Outlook). When running as a Jamf Pro policy, you can set these as Parameter 4 and Parameter 5.

If you do not know the bundle identifier for an app, you can get it with:

mdls /Applications/Firefox.app -name kMDItemCFBundleIdentifier -raw 

I have also built another script that can be used as an extension attribute in Jamf Pro that reports the current setting for the current user’s default browser. Since extension attributes do not have arguments, you will have to create a duplicate and change the urlScheme variable in line 14 if you want to use this with different url schemes.

 

Working with the prompt

Since the prompt to confirm the change is unavoidable, you want to make this part of a workflow when you already have the user’s attention. If the user chose to install a third party browser from Self Service, it is likely they want to set it as the default as well. In this case the prompt serves our purposes well, as it gives the user a choice to keep the existing browser.

If you want to use a recurring policy to regularly re-set the default browser, you have take into consideration, that the mandatory dialog might pop up at random times, interrupt what the user is doing and will probably be ignored. If this is something you really want to control, then you may need to build more user interface with a tool like jamfHelper or SwiftDialog that locks the screen and provides more explanation and context.

You also really should provide feedback to Apple that they need to provide a means to manage and lock this kind of setting with the MDM. (I may have mentioned that before.)

 

Conclusion

We have have explored the challenges of managing default browsers and apps for other URL schemes. We have also looked at solutions, like hacking the LaunchServices configuration directly, calling the browsers with additional options or building scripts that use the official APIs. You will have to develop strategies to deal with the mandatory prompt to change the default browser.

You can share your strategies and solutions in the comments!

 

PS: A note on variable substitution

Whenever I post code with osascript someone will inevitably comment that substituting variables with arbitrary values directly into executable code is a terrible, horrible, dangerous thing to do and I need to re-think all of my life choices…

Here is my defense to pre-empt these comments. The variables in this script do not have arbitrary values.

These scripts are designed to be used by MacAdmins using Jamf Pro. (They can be adapted to run in other management systems. Feel free to do so.) If they feed the script poor, malformed arguments which makes the script throw up and crash, then that is on them. They are the ones who have to directly deal with the consequences. I trust them to be able to figure out how to do it right.

If an admin feeds a variable value that is specifically designed to run arbitrary code within the context of the script then… well done! That is a valuable exercise and I am sure you learned something along the way. Now you can abuse a script in Jamf Pro to… run arbitrary code that you gave it… which is a feature of Jamf Pro and any other management system, anyway.

I believe that justifies me handling the variables in a simpler way that also improves code readability. Since I am using programming languages and APIs (JXA with the Objective-C bridge) that are less familiar to most MacAdmins readability is something I strive for.

That said, when you are building a script that runs in a context where the variables or arguments may have any arbitrary value, especially if they are user provided, then you absolutely must be careful in handling the input in way that won’t crash the script on strange characters or allow arbitrary code execution. This is complicated to get right, and if you end up in this situation, you probably will re-think all of your life choices.

2 Comments
cingalls
New Contributor II

Great explanation, thank you!
I particularly like the script to set the default app for an arbitrary URL scheme: https://gist.github.com/scriptingosx/5f5ea7fcb1d6839a83fd02ab2729d0e9#file-setdefaultappforurl-sh 
Is it also possible to set it for a specific file content type instead of a URL scheme? e.g. setting "com.apple.ical.ics" or "public.vcard" to default to "com.microsoft.Outlook"?

arminBriegel
New Contributor III
New Contributor III

@cingalls Great question, one that I was keeping for a future post. Unfortunately, the APIs for changing default handlers for file types are not bridged to AppleScript. So to manage those you will have to use an additional tool. However, most of the tools that MacAdmins have been using for this, such as duti, cdef, or SwiftDefaultApps  have not been updated in years...