I was struggling with this exact thing since yesterday, and was dismayed to see that no one had answered your question in 4 months. I just figured out what my issue was, which was that the minimum deployment version of the extension target was set to 15.4 and my phone is running something slightly lower than that. The minimum deployment version of the app itself was low enough, which resulted in the app installing and running fine on my phone, but the extension silently failing to show up in the list. I set the minimum deployment of the extension to 15.0 an re-ran the app on my iPhone and the extension immediately showed up in Settings -> Passwords -> Autofill passwords
Post
Replies
Boosts
Views
Activity
You are exactly right, thank you! Evidently I was not careful enough when comparing the working Mac extension to the not-working iOS extension - in the iOS Info.plist I was using NSExtension > ASCredentialProviderExtensionCapabilities > ProvidesPasskeys = YES. Now the system offers my extension when creating passkeys.
Now my iOS extension is suffering the same problem as my Mac extension, so if you have a chance to lend a helping hand on that one I would be hugely appreciative.
Is there any information that I could add here to make this request more actionable? Is there anything I could clarify either about the information at hand or about what it is I’m asking? Thanks very much 🙏
Thanks so much for getting back to me @garrett-davidson . I've filed a request through the Feedback Assistant, and I hope you don't mind that I also ask you a little bit more in this thread.
Firstly, the passkey logs via the terminal didn't yield anything very informative (I've pasted the logs all the way at the bottom).
Secondly, I realized that in my original post I didn't mention that there is a potentially relevant error that appears in the system console when I attempt to authenticate using the newly created passkey:
error 15:12:15.084213+0100 AuthenticationServicesAgent 0 <Missing Description> No matched credentials are found in the platform attached authenticator.
Here is the error in the context of the surrounding console errors (just in case any of these other errors reveal something that I'm not picking up on):
error 15:12:15.082058+0100 nfcd 0 Logging -[_NFHardwareManager listener:shouldAcceptNewConnection:]:84 PID 1573 () missing entitlement: com.apple.nfcd.hwmanager
error 15:12:15.082315+0100 AuthenticationServicesAgent 0 Logging -[NFHardwareManager updateHWSupportWithXPC:waitForInit:]:361 Failed to get HW support : Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.nfcd.hwmanager" UserInfo={NSDebugDescription=connection to service named com.apple.nfcd.hwmanager}
error 15:12:15.082503+0100 nfcd 0 Logging -[_NFHardwareManager listener:shouldAcceptNewConnection:]:84 PID 1573 () missing entitlement: com.apple.nfcd.hwmanager
error 15:12:15.082819+0100 nfcd 0 Logging -[_NFHardwareManager listener:shouldAcceptNewConnection:]:84 PID 1573 () missing entitlement: com.apple.nfcd.hwmanager
error 15:12:15.082673+0100 AuthenticationServicesAgent 0 Logging -[NFHardwareManager updateHWSupportWithXPC:waitForInit:]:361 Failed to get HW support : Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.nfcd.hwmanager" UserInfo={NSDebugDescription=connection to service named com.apple.nfcd.hwmanager}
error 15:12:15.082986+0100 AuthenticationServicesAgent 0 Logging -[NFHardwareManager controllerInfoWithError:]:558 Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.nfcd.hwmanager" UserInfo={NSDebugDescription=connection to service named com.apple.nfcd.hwmanager}
error 15:12:15.084213+0100 AuthenticationServicesAgent 0 <Missing Description> No matched credentials are found in the platform attached authenticator.
error 15:12:15.101763+0100 pkd 2232554 ls could not create extension point record for <private>: Error Domain=NSOSStatusErrorDomain Code=-10814 UserInfo={_LSLine=85, _LSFunction=<private>}
error 15:12:15.309050+0100 com.apple.WebKit.WebContent 0 ProcessSuspension 0x10a040100 - [sessionID=9223372036854775944] WebProcess::markAllLayersVolatile: Failed to mark layers as volatile for webPageID=37958
error 15:12:15.724853+0100 CredentialProviderExtensionHelper 2232557 NSExtension errors encountered while discovering extensions: Error Domain=PlugInKit Code=13 "query cancelled" UserInfo={NSLocalizedDescription=query cancelled}
I'll finish by briefly breaking down my understanding of the situation, in the hopes that it can narrow down the conversation:
My extension's Info.plist contains NSExtension -> NSExtensionAttributes -> ASCredentialProviderExtensionCapabilities -> ProvidesPasskeys: YES, which is demonstrably working as evidenced by the fact that I can use my extension to create a passkey via a webpage (google.com, for example).
When creating a passkey, the system invokes prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) on my extension, passing in a value which I am successfully able to cast as an ASPasskeyCredentialRequest.
My job is then to create the passkey, save it according to the business rules of my app, insert the corresponding credential identity into the ASCredentialIdentityStore, and finally call extensionContext.completeAssertionRequest(using:) passing in an ASPasskeyAssertionCredential that contains a correctly formatted binary blob of authenticatorData (among other things).
Based on my current knowledge and understanding, it seems to me that the only possible point of failure that could cause what I'm seeing is that, despite the ASCredentialIdentityStore accepting the credential identity without error, there is some issue with that identity which is causing it to not be recognized when I subsequently attempt to authenticate with the corresponding passkey. The main point I'm making here is that I think that if the authenticatorData binary blob is accepted by google.com and leads to a new passkey being added to my Google account, then that's not where the problem could lie. Is that a correct assumption? When I call extensionContext.completeAssertionRequest(using:) after creating the passkey does the system remember things about the ASPasskeyAssertionCredential that I pass in that could lead to my extension not being offered in the system modal when I try to authenticate later?
Thank you so much for any advice/feedback you can offer me - I've been stuck with this issue for quite a long time now and I'll feel so relieved when I have resolved it.
Very best,
Jeremy
Passkey Logs from terminal command
AuthenticationServicesAgent: (AuthenticationServicesCore)
[com.apple.AuthenticationServicesCore:Authorization] Initializing ASCAgent 0x1826a1e30.
1573
AuthenticationServicesAgent:
AuthenticationServicestore
[com.apple.AuthenticationServicesCore:Authorization] Received connection from com.apple.Safari
1573
AuthenticationServicesAgent:
(AuthenticationServicesCore) [com.apple.AuthenticationServicesCore:Authorization] Allowing request from web browser.
AuthenticationServicesAgent:
(AuthenticationServices) [com.apple.AuthenticationServices:Authorization] Beginning operation 6363A0C7-039-4FDC-A650-31912795FEE5 for com.apple.Safari.
1573
AuthenticationServicesAgent:
(AuthenticationServices)
[com.apple.AuthenticationServices:Authorization] Creating panel 0x103d2f5b0 for 6363A0C7-039-4FDC-A650-31912795FEE5.
Thank you! This is very encouraging. Being able to see the code that is deciding to throw this error makes the task of resolving the issue feel much more in my control. I'll add that I'm experiencing this error on google.com and kayak.com, which are the main two sites that I'm testing with so far.
Is there any way to step through the WebKit code as it executes or in any way gain access to the values of some the variables in play in that section of the code? If not it still seems like it may be enough of a lead to allow me to research my way to a solution, but some live inspection of values would presumably be extremely enlightening.
Thanks for everything.
Thank you @garrett-davidson, your various pieces of insight and advice both emboldened and empowered me to figure out the mistake I was making, and that problem is now resolved... 🎉
However, I have run into a new error which has left me with a new question that I would love your feedback on if you're willing.
Now, the system recognizes that my extension can provide the passkey, and therefore the system modal displays my app icon and allows me to proceed. It shows my extension UI which prompts the user to authenticate. After my extension has called extensionContext.completeAssertionRequest(using:) the webpage tells me that an error has occurred.
In the system console I see only this error:
Assertion failed: Error Domain=WKErrorDomain Code=31 "(null)"
I tried searching for the string "Assertion failed" in WebKit, and found three results but none of them seemed like the source of the error.
This error does not appear when I authenticate on webauthn.me, so I am limited in terms of the feedback I receive about what exactly is going wrong.
At the moment the only lead I have in mind is that I realized that the ASPasskeyCredentialRequest value that my extension receives does not have any property named challenge or anything similar. It has clientDataHash which comes with a documentation comment Hash of client data for credential provider to sign as part of the assertion/registration operation., so that's what I am currently signing with the passkey. The ASPasskeyAssertionCredential that my extension passes to extensionContext.completeAssertionRequest(using:) contains the signature, and the documentation for that property describes it as the signature for the assertion challenge.. I'm hoping that I'm signing the wrong thing and that that is why the assertion is failing.
Is there some other place that I'm supposed to be accessing the challenge data?
Thanks so much for all of your help so far and any more help that you provide.
Thank you, this is very helpful. So far it hasn't led me any obvious answers, but I'll keep investigating.
At first glance:
If I understand correctly, the error would be coming from the authenticatorGetAssertion operation. In the specification, there seem to be only two possible triggers:
Step 6: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation.
or step 7, which ends in: If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation.
I think that the fact that the system modal offers the passkey via my extension means that step 6 should not be triggering an error.
Regarding step 7, the flags I'm returning have both bit 0 ("User Present") and bit 2 ("User Verified") set to true. Perhaps those two bits are not necessarily sufficient to prevent the error from being thrown in step 7?
I just thought I'd write this here in case it reveals some obvious flaw in my reasoning that's easy for you to spot and point out. Thanks for everything.
These are the flags I'm passing:
generateAuthDataFlags(
userIsPresent: true,
userHasBeenVerified: true,
passkeyIsEligibleForBackup: true,
passkeyIsBackedUp: true,
includesAttestedCredentialData: false,
includesExtensionData: false
)
where the function is defined as:
func generateAuthDataFlags(
userIsPresent: Bool,
userHasBeenVerified: Bool,
passkeyIsEligibleForBackup: Bool,
passkeyIsBackedUp: Bool,
includesAttestedCredentialData : Bool,
includesExtensionData: Bool
) -> UInt8 {
///
UInt8(0b00000000)
.plus(userIsPresent ? 0b00000001 : 0)
.plus(false ? 0b00000010 : 0)
.plus(userHasBeenVerified ? 0b00000100 : 0)
.plus(passkeyIsEligibleForBackup ? 0b00001000 : 0)
.plus(passkeyIsBackedUp ? 0b00010000 : 0)
.plus(false ? 0b00100000 : 0)
.plus(includesAttestedCredentialData ? 0b01000000 : 0)
.plus(includesExtensionData ? 0b10000000 : 0)
}
Also, you mentioned that the relying party identifier could be wrong, but I'm simply taking the value directly from the incoming ASPasskeyCredentialRequest.credentialIdentity.relyingPartyIdentifier.
Again, I'm providing additional info just in case the answer to my problem occurs to you easily, since you've been generously helping me debug this, not because I'm expecting you to wrack your brain trying to find the needle in my haystack from afar.
There is actually one more error message that shows up in the console that I hope might quickly lead to the answer of how to solve this (hopefully final) roadblock:
Request cancelled due to AuthenticatorManager::cancelRequest being called.
This message appears directly after the other message I mentioned: Assertion failed: Error Domain=WKErrorDomain Code=31 "(null)". They are separated by about half a second.
I believe the second error message is coming from this function that I found in WebKit:
void AuthenticatorManager::cancelRequest()
{
invokePendingCompletionHandler(ExceptionData { ExceptionCode::NotAllowedError, "This request has been cancelled by the user."_s });
RELEASE_LOG_ERROR(WebAuthn, "Request cancelled due to AuthenticatorManager::cancelRequest being called.");
clearState();
m_requestTimeOutTimer.stop();
}
@garrett-davidson, if you're willing, please let me know if you know of anything that I should check for that would cause this function to be called.
My extension initially invokes extensionContext.cancel(withErrorCode: .userInteractionRequired) to trigger the UI to appear, but that is not when the error appears. It is after my extension calls extensionContext.completeAssertionRequest(using:) that the message appears. I am not waiting a long time to trigger this call, so I don't think that any timeouts are being reached. After completing the assertion request, my extension does not invoke the cancel function itself. I'm experiencing this on google.com, and I doubt that they're accidentally invoking the cancel function in a place where they shouldn't. Note that the same error appears in the console if I click "Cancel" on the initial system modal instead of clicking "Continue" in order to summon my extension.
One final possible-relevant clue that I'll mention is that when I initiate the authentication operation I am actually still seeing the error:
No matched credentials are found in the platform attached authenticator.
despite the fact that the system modal successfully identifies my extension as having the requested passkey and allows me to proceed.
Thank you for reading 🙏
P.S. You said in your last reply: "Your extension should have received an error if those weren't set" but I'm not aware of how errors are propagated back to my extension. extensionContext.completeAssertionRequest(using:) is not a throwing function, and the completion handler variant does not pass any error value into the completion handler. If there's some mechanism for receiving errors in my extension that I am unaware of I would love to know about it!
Here I've attached the system logs that lead up to the failure, in the hopes that the golden nugget of insight that I so desperately need is in there...
The very last line is the one I mentioned above:
error 14:02:18.222529+0100 AuthenticationServicesAgent 19621528 <Missing Description> Request cancelled due to AuthenticatorManager::cancelRequest being called.
Scrolling upward (backwards in time) from that last line there are a number of logs that seem to me potentially relevant.
For example:
default 14:02:18.222589+0100 CredentialProvider-macOS 0 connection [0x12a8c5b50] invalidated after getting a no-senders notification - client is gone
default 14:02:18.222505+0100 AuthenticationServicesAgent 19593644 Authorization Received internal cancel. Dropping.
default 14:02:18.222248+0100 AuthenticationServicesAgent 19593645 Authorization Asked to cancel operation 0420A06B-1595-4B97-9FB9-6915EA2EDDD8, override error: Error Domain=com.apple.AuthenticationServicesCore.AuthorizationError Code=12 "(null)"
logs.txt
@garrett-davidson I tried a huge number of variations, and one factor that proved to be key was that of the aaguid. In an earlier iteration I was using 00000000-0000-0000-0000-000000000000, but then at some point I thought it seemed weird that that would work better than a real UUID that actually uniquely identified the device, so I made the switch but was still blocked by other issues. After resolving all of those other issues, everything finally worked when I eventually set the aaguid back to 00000000-0000-0000-0000-000000000000 as I had had it many iterations prior.
I had watched these videos when they were released and just re-watched both of them now - the only mention I see of anything related to my specific question is in the Data Essentials video when he talks briefly about the importance of avoiding expensive operations during body evaluation, because it can lead to dropped frames, etc. This still doesn’t help me understand why It results in “undefined behavior” to modify the state during body evaluation. Why can’t modifying the state just invalidate the current evaluation and trigger a new one? It is then my job as the programmer to make sure that that recursion settles down promptly, and to keep an eye on performance.
To be clear, I'm not struggling to use standard SwiftUI tools. I'm striving for a highly elegant architecture that reduces the friction of developing UI to an even lower level than the already awesome level that one gets with SwiftUI out of the box, while simultaneously meeting certain additional requirements, for example aiming at portability of the maximum amount of interface-related code.
To that end I have come up with some very nice solutions, and one issue I'm facing is that I believe I have valid reasons for wanting, in some situations, modify the state during body evaluation, but even though my logic seems sound to me and the interface behaves exactly as it should, Xcode hits me with the standard warning:
Publishing changes from within view updates is not allowed, this will cause undefined behavior.
I've boiled down a dramatically simplified example of the type of situation in which I'm doing the disallowed thing of modifying the state during body evaluation:
@MainActor
final class StoredColors: ObservableObject {
@Published
var colors: [UUID: StandardHexadecimalColorCode] = [:]
func createNewColorEntry() {
colors[.generateRandom()] = .white
}
}
@MainActor
final class EditableText: ObservableObject {
@Published
var values: [UUID: String] = [:]
}
struct ColorList: View {
@ObservedObject var storedColors: StoredColors
@StateObject var editableTextByColorID: EditableText = .init()
var body: some View {
VStack(alignment: .leading, spacing: 7) {
ForEach(storedColors.colors.keys.asArray(), id: \.self) { colorID in
textField(
forColorWithID: colorID
)
.transition(.move(edge: .leading))
}
addItemButton(
onPressed: {
withAnimation {
storedColors.createNewColorEntry()
}
}
)
}
}
func textField(
forColorWithID colorID: UUID
) -> some View {
ensureThatThereIsEditableText(
forColorWithID: colorID
)
return
colorTextField(
withBinding: textBinding(forColorWithID: colorID)
)
}
@ViewBuilder
func colorTextField(
withBinding textBinding: Binding<String>?
) -> some View {
if let textBinding {
TextField(
"#AA12FF",
text: textBinding
)
}
}
func textBinding(
forColorWithID colorID: UUID
) -> Binding<String>? {
guard let currentValue = editableTextByColorID.values[colorID] else { return nil }
return
Binding(
get: { editableTextByColorID.values[colorID] ?? currentValue },
set: { newValue in
if editableTextByColorID.values[colorID] != nil {
editableTextByColorID.values[colorID] = newValue
}
}
)
}
func ensureThatThereIsEditableText(
forColorWithID colorID: UUID
) {
if editableTextByColorID.values[colorID] == nil {
editableTextByColorID.values[colorID] = ""
}
}
}
Can someone tell me why exactly my approach here causes "undefined behavior"?
@DTS Engineer My highest hope is that SwiftUI could be evolved in a minor way such that I am allowed to opt in to having recursive view updates. As I mentioned, my interface behaves exactly as I want it to, the only apparent problem is that I get the runtime warning from SwiftUI in the console.
If this is impossible then I'm at least hoping that you could help me understand the details of why this is not an option.