While working with Local Authentication framework, specifically LAContext class I found myself with few contradictions to documentation, and although I believe that those differences are rather positive than negative, either documentation is lacking behind or those APIs are not working as intended - which I believe is combination of both.
1. Local Authentication
1.1 Biometry type, and associated with it hash
With introduction of LADomainState one can extract underlying biometry type along it's (current) state hash this way:
@available(iOS 18, macOS 15, *)
func postIOS18() {
let context = LAContext()
let biometryType = context.domainState.biometry.biometryType // (1)
let biometryStateHash = context.domainState.biometry.stateHash // (2)
}
prior to receiving above APIs, we would retrieve such information something along those lines:
func preIOS18() {
let context = LAContext()
let policy: LAPolicy // ...
var error: NSError?
_ = context.canEvaluatePolicy(policy, error: error) // (3)
// ... (Handle error - if present)
let biometryType = context.biometryType // (4)
let biometryStateHash = context.evaluatedPolicyDomainState // (5)
}
However, moving let biometryType = context.biometryType (4) before call to canEvaluatePolicy (3) still yields correct biometry type. This is in contradiction to article from Local Authentication documentation page Optionally, Adjust Your User Interface to Accommodate Face ID. Furthermore, biometryType documentation does not mentions such requirement.
Another difference is that call to canEvaluatePolicy (3) might return an error, eg. LAError(.biometryLockout) (if implemented correctly) preventing as from returning biometryStateHash (5) with nil value. This is not the case for new API, where the same call (2) will yield nil as a result - LADomainStateBiometry documentation does not mention it.
In summary, here are some questions:
Which API should be used to retrieve biometry type?, and why the "older way" has not been deprecated?
Is is safe to assume that calls to biometryType and stateHash from LADomainStateBiometry will produce meaningful results without prior call to canEvaluatePolicy?
Should I assume that call to biometryType found on LAContext instance will always return biometryType without prior call to canEvaluatePolicy?, or perhaps those are only side effects of changes made to accommodate LADomainState, and prior to them (pre-iOS 18) we must call canEvaluatePolicy to get meaningful value.
Are the stateHash properties found on LADomainState, LADomainStateBiometry and LADomainStateCompanion will return nil upon encountering any error under the hood? (which would be equivalent of below code, prior to iOS 18)
func biometryStateHash() -> Data? {
let context = LAContext()
if #available(iOS 18, macOS 15, *) {
_ = context.canEvaluatePolicy(policy, error: nil)
return context.evaluatedPolicyDomainState
} else {
return context.domainState.biometry.stateHash
}
}
1.2 Deprecation of evaluatedDomainState
There is a forum post LAContext.evaluatedPolicyDomainState change between major OS versions mentioning missing documentation (fixed), however there's still information missing of how they correlate to each other. From my findings, the deprecated evaluatedDomainState property value matches those of LADomainState stateHash (when no companion device is present), and LADomainStateBiometry stateHash (all the time). Are those assumptions correct?
1.3 LAContext (authenticated) session lifespan
Theres is little information about it state when authenticated by the user. Documentation on LAContext does not mention this behavior, while there are hints that once authenticated, and context is reused, any farther calls will not prompt user with UI.
The problem is that this behavior is little, to no documented. Here are few examples I have found:
Logging a User into Your App with Face ID or Touch ID (code sample) contains comment
// Get a fresh context for each login. If you use the same context on multiple attempts
// (by commenting out the next line), then a previously successful authentication
// causes the next policy evaluation to succeed without testing biometry again.
// That's usually not what you want.
Recent forum post, where such approach is mentioned by Quinn 'The Eskimo!'
"At the API level, one option you have is to create an LAContext and pass it in to each SecItemCopyMatching call via kSecUseAuthenticationContext."
WWDC22 Streamline local authorization flows session
"By binding the LAContext to our private key reference, we ensure that executing the signature operation will not trigger another authentication, while allowing the operation to continue without unnecessary prompts. These binding also means that no additional user interactions will be required for future signatures until the LAContext is invalidated."
Furthermore this is complicated by the touchIDAuthenticationAllowableReuseDuration property from LAContext instance which states that "The default value is 0, meaning that no previous biometric unlock can be reused." which is in direct contradiction to what I have experienced while working with LAContext and sources mentioned above.
While digging on this, whether this behavior is intended or not, I came across a post (I would love to share it, but the domain is not permitted) that shared the same findings (and concerns) regarding LAContext behavior as me. The author also provided a FB9984036 feedback number - although no further update was made on that topic.
So my questions are:
Is it safe to reuse LAContext (authenticated) instance?
How long such instance is considered authenticated?, is it a time duration or perhaps it stays in authenticated state until explicitly invalidated using invalidate method. (its invalidated for sure when app is terminated, but this was to be expected :))
How does touchIDAuthenticationAllowableReuseDuration work, and how does it correlate to "reusability" of the authenticated LAContext instance?
In what scenarios touchIDAuthenticationAllowableReuseDuration should be used and what is its expected behavior? (I have tried it both on iOS and macOS; from my perspective API this does not "work")
0
0
27