how to prompt for and require ADMIN username & password

I'm developing a macOS app that will usually be running in a non-admin user environment. But I have a screen of the app that I would like to secure so as to make it only accessible to admin users (think: parents).

I can't figure out what API I'm supposed to use to prompt for specifically an ADMIN user. I've tried googling a ton, but I must be trying the wrong search terms, because I can't find anything.

The API for LAContext() is almost what I want, I can get it to prompt for a password, but it seems to ONLY work for the current logged in user. I can't find a policy type that allows me to specify something like .adminUserAuthentication. It seems like LAContext() was not meant for this use case. But then, what is the right API to call to do this?

Can someone point me in the right direction?

I don't want to limit myself to this only working for supervised users, or users with parental controls turned on, I would like a generic solution. I've seen apps that prompt for admin credentials on regular non-admin users, so it must be possible, right?

Accepted Reply

I can't figure out what API I'm supposed to use to prompt for
specifically an ADMIN user.

You should use Authorization Services for this. A good place to start (IMO, but I’m biased because I wrote it :-) is Technote 2095 Authorization for Everyone. Some of the details in there are out of date, but it’ll give you a high level understanding of how the API is meant to be used.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

I can't figure out what API I'm supposed to use to prompt for
specifically an ADMIN user.

You should use Authorization Services for this. A good place to start (IMO, but I’m biased because I wrote it :-) is Technote 2095 Authorization for Everyone. Some of the details in there are out of date, but it’ll give you a high level understanding of how the API is meant to be used.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Brilliant, thanks so much for the info, much appreciated.
@eskimo So I've taken a couple stabs at trying to implement this, and I'm really struggling. Tonight I spent a couple hours scouring the internet for more clues as to how to pull this off in Swift, and I can't find any tutorial or code fragments that seem to be up to date with Swift 5.2.

The tech note you linked to I've read and re-read, but I'm trying to implement this in Swift, not objc, and I'm really struggling. Do you know of any tutorial or sample code for a recent swift version that covers this API, AuthorizationCreate, or something like it?

I have a couple specific questions:
  1. I'm not trying to actually escalate permissions, or run a script as root, or anything like that, I just want to force the entry of an admin user/pass to protect an area of my app. Do I need to create my own custom policy right for that? Or is there some default right I can ask for, something like system.privelege.admin?

  2. If I'm testing while logged in as an admin, will the attempt to authorize immediately succeed? Do I have to build my app and install it on a non-admin user to see if it's working?

  3. I'd like to prompt for the admin user/pass every time even if the admin is logged in... Is this possible?

  4. Would you be willing to write up a snippet or two of Swift to cover this use case? I (and I guess many future googlers) would really appreciate it! 🙏

Do you know of any tutorial or sample code for a recent swift version
that covers this API, AuthorizationCreate, or something like it?

No. I’m certain that Apple hasn’t published anything like that — this is a very dark and dusty area of the system — and I’m no better at finding tutorials on the Internet than you are )-:

Having said that, I’m very good at calling C APIs from Swift, so let’s take a stab at that…



The first thing to note is that an AuthorizationRef is not reference counted, which makes it a challenge to use from Swift (if you follow Swift Evolution you’ll see that move-only types are a commonly-requested feature, but we’re not there yet). So, for the moment, it’s best to wrap the type in a class:

Code Block
final class QAuthorization {
var ref: AuthorizationRef
init(ref: AuthorizationRef) {
self.ref = ref
}
}


You can then add a convenience initialiser wrapped around AuthorizationCreate:

Code Block
extension QAuthorization {
convenience init() throws {
var refQ: AuthorizationRef? = nil
let err = AuthorizationCreate(nil, nil, [], &refQ)
guard err == errSecSuccess else {
… throw an error here …
}
self.init(ref: refQ!)
}
}


Now add a method to call AuthorizationCopyRights:

Code Block
extension QAuthorization {
func checkRight(_ name: String, flags: AuthorizationFlags = [.interactionAllowed, .extendRights]) throws {
let err = name.withCString { namePtr -> OSStatus in
var item = AuthorizationItem(name: namePtr, valueLength: 0, value: nil, flags: 0)
return withUnsafeMutablePointer(to: &item) { itemPtr -> OSStatus in
var rights = AuthorizationRights(count: 1, items: itemPtr)
return AuthorizationCopyRights(self.ref, &rights, nil, flags, nil)
}
}
guard err == errSecSuccess else {
… throw an error here …
}
}
}


Oh yeah, that’s scary! All this complexity is caused by Swift’s strict rules about pointer lifetimes. Specifically, lines 4 through 7 are there purely to build an AuthorizationRights structure containing the right name string.

Finally, wrapping AuthorizationRightSet is pretty straightforward:

Code Block
extension QAuthorization {
func setRight(
name: String,
definition: String,
descriptionKey: String? = nil,
bundle: Bundle? = nil,
localeTableName: String? = nil
) throws {
// `NSBundle` and `CFBundle` are not toll-free bridged, so we have to
// bounce through the URL.
//
// <https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFDesignConcepts/Articles/tollFreeBridgedTypes.html#//apple_ref/doc/uid/TP40010677-SW1>
let bundleCF = bundle.map {
CFBundleCreate(nil, $0.bundleURL as NSURL)!
}
let err = AuthorizationRightSet(
self.ref,
name,
definition as NSString,
descriptionKey.map { $0 as NSString },
bundleCF,
localeTableName.map { $0 as NSString }
)
guard err == errSecSuccess else {
… throw an error here …
}
}
}


This assumes that you want to map your right to an existing rule. If not, you’ll need a version that passes a dictionary to the rightDefinition parameter of AuthorizationRightSet.



And with that out of the way, let’s return to your specific questions:

I'm not trying to actually escalate permissions, or run a script as
root, or anything like that, I just want to force the entry of an
admin user/pass to protect an area of my app. Do I need to create my
own custom policy right for that?

That’s what I recommend. If you piggyback on top of an existing right then there’s no way for a sys admin to separate the two operations. For example, the parent might want to allow an older child to do your operation but block a younger child, and if you piggyback on system.privelege.admin they can’t do that without given the older child full admin access.

If I'm testing while logged in as an admin, will the attempt to
authorize immediately succeed? Do I have to build my app and install
it on a non-admin user to see if it's working?

That very much depends on your right specification.

I'd like to prompt for the admin user/pass every time even if the
admin is logged in... Is this possible?

Yes. The trick here is to not include shared in your right definition, so that your attempt to get the right won’t rely on credentials acquire via other means (such as the admin user being logged in).

Would you be willing to write up a snippet or two of Swift to cover
this use case?

I think the above qualifies (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
@eskimo -- thank you SO much, that is fantastic information, very much appreciated. I think your code snippets will be really useful to a lot of future googlers.
Ok, so in addition to what eskimo posted above, the below seems to be working using the higher-level SFAuthorization class, although it seems to be less configurable (for instance I don't think you can change the message "<Your App> wants to make changes"), so YMMV:


Code Block swift
func authenticateAsAdmin() -> Bool {
guard
let authorization = SFAuthorization.authorization() as? SFAuthorization,
let right = NSString(string: kAuthorizationRuleAuthenticateAsAdmin).utf8String
else {
/* or maybe throw an error */
return false
}
do {
try authorization.obtain(withRight: right, flags: [.extendRights, .interactionAllowed, .destroyRights])
} catch {
return false
}
return true
}


Furthermore, I figured out that one of my problems was I forgot to check if my app was in sandbox mode, in which case these API's don't work and you only get vague permission denied errors.

the below seems to be working using the higher-level SFAuthorization
class

Oh, yeah, SFAuthorization is awesome and I always forget to use it. SFAuthorizationView is also super helpful.

although it seems to be less configurable (for instance I don't think
you can change the message "<Your App> wants to make changes")

This is driven by the right specification. You’re getting generic text because you’re using kAuthorizationRuleAuthenticateAsAdmin, which is what I’m trying to steer you away from. If you use a custom right you can customise this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

This is driven by the right specification. You’re getting generic text because you’re using kAuthorizationRuleAuthenticateAsAdmin, which is what I’m trying to steer you away from. If you use a custom right you can customise this.

Aha, yes, that makes sense. Really appreciate it, thanks!


Thanks for all this information!
The documentation pages of both Authorization Services and SFAuthorization show warnings that these cannot work in sandboxed apps since they allow privilege escalation.
Is there a way to do the same thing ("I just want to force the entry of an admin user/pass to protect an area of my app") in a sandboxed app (for example to be allowed into the mac app store)?

Is there a way to do the same thing … in a sandboxed app

Are you sandboxing your app because you’re distributing it via the Mac App Store? Or are you distributing your app independently, using Developer ID signing, and sandboxing it because it’s the right thing to do?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
The quick answer is "Sandboxing because we want to (eventually) distribute via the Mac App Store".
(For testing, QA, and customer demos we are also handing out Developer ID signed versions, and these are sandboxed to be as close as possible to the Mac App Store version.)

The quick answer is "Sandboxing because we want to (eventually)
distribute via the Mac App Store".

OK.

Alas, I don’t think there’s a good option for you here. Authorization Services doesn’t work from within the sandbox, even if you’re only use it to guard an operation that your app already has the privileges to do. I think it’d be reasonable to file a bug about this but I wouldn’t get my hopes up.

You could ask the user for a user name and password and then do the authentication yourself but that has a bunch of drawbacks:
  • It requires you to handle sensitive data, namely the user’s password.

  • It will fail in non-standard environments, for example, where the user has logged in with a smart card.

  • It may set off alarm bells in App Review.

IMPORTANT I don’t work for App Review and thus can’t give you definitive answers about their policies.

Another option would be to implement your own locked-with-a-PIN feature, but I’m not a huge fan of that either.

If you do the PIN thing and there’s any chance that your app might be deployed in a managed environment, make sure you support managed app configuration for it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Alright, thanks for your thoughts on the matter! If we end up filing a bug report I will link it here :)

Earlier I wrote:

You’re getting generic text because you’re using kAuthorizationRuleAuthenticateAsAdmin, which is what I’m trying to steer you away from.

One of my colleagues pointed out that the generic text isn’t the only problem here. kAuthorizationRuleAuthenticateAsAdmin is a rule name, not a right name. If you pass it to something like AuthorizationCopyRights, Authorization Services is unable to find your right and falls back to its default. This ends up doing what you want, but only by accident; it’s not something you want to rely on [1].

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] We’d actually like to change the implementation so that it fails rather than falls back to the default, but we have to keep it this way to maintain compatibility.