Passkey from my macOS Credential Provider Extension is not being offered by AutoFill

I am developing a Mac app which provides a Credential Provider extension and I'm having trouble with passkey integration. I wrote here about the issue I'm having with the iOS app. On the Mac I'm experiencing a different issue. As opposed to the iOS app (where I'm not even able to use my extension to create a new passkey in the first place) on the Mac I'm able to use my extension to create a passkey. I save the credential identity into the system AutoFill suggestions store without error. The problem is that when I attempt to authenticate on the same site the system does not offer my app's credential as a suggestion. Standard passwords are working fine. Can anyone help me understand how I can troubleshoot this type of problem?

Thanks!

-Jeremy

Replies

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 🙏

Hi Jeremy, you have two good options for how to proceed:

  1. You can try to debug this issue yourself. A good starting point is to look at the passkey logs, through something like log stream --predicate 'category == "Authorization" AND subsystem contains "com.apple.AuthenticationServices"' in the terminal. If it's a simple fix on your side, this may be enough and will likely be the fastest option.

  2. You can file this through Feedback Assistant and we can take a look ourselves. If you can take a screen recording showing the issue, then trigger capture of the logs through Feedback Assistant shortly after, we'll get those same logs (albeit a more redacted version than what you saw, for privacy reasons), and can match the timestamps in the logs with those in the screen recording to figure out exactly what's going on.

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:

  1. 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).

  2. 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.

  3. 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.

after creating the passkey does the system remember things about the ASPasskeyAssertionCredential that I pass in

No, the system only uses what you tell ASCredentialIdentityStore.

error 15:12:15.084213+0100 AuthenticationServicesAgent 0 <Missing Description> No matched credentials are found in the platform attached authenticator.

This error (which happens to be coming from an open source part of the implementation) most likely means there's an issue with the request coming from the website, specifically related to the allowCredentials argument. Something about the contents of allowCredentials told the system not to allow local passkeys. You can see all the conditions in produceHashSet near the top of that file.

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.

Unfortunately your options are pretty limited for introspecting things once they get here. You'll likely have better luck either using Web Inspector to look at the JavaScript invocation that those sites are making, or looking at the system logs which should show most or all of the relevant parameters. You may also want to try a WebAuthn test site like webauthn.me or webauthn.io, which give you control over all the arguments and let you inspect the outputs.

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.

Error code 31 is NotAllowedError. If you search NotAllowedError in the WebAuthn spec, there are many different conditions that can trigger it, and the spec intentionally tells browsers not to provide much information for user privacy.

Off the top of my head I don't think an invalid signature will result in a NotAllowedError. It's likely that some other part of your response is invalid (maybe the relying party?).

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.

You'll also need at least the Backup Eligible and Backup State bits set to true as well. Your extension should have received an error if those weren't set, but I'm not immediately sure how that would manifest to the website.

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:

  1. default 14:02:18.222589+0100 CredentialProvider-macOS 0 connection [0x12a8c5b50] invalidated after getting a no-senders notification - client is gone

  2. default 14:02:18.222505+0100 AuthenticationServicesAgent 19593644 Authorization Received internal cancel. Dropping.

  3. 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)"

@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.