Skip to content
macOSDeep Dive Published Updated 8 min readViews unavailable

Passkeys on macOS: AuthenticationServices Requests and Server Verification

Integrate macOS passkey registration and sign-in with AuthenticationServices, associated domains, server challenges, and replay-resistant verification.

Passkeys let a person authenticate to a service with a public-key credential instead of a shared password. In an app on macOS, AuthenticationServices presents the system authorization experience and returns a credential response. The private key remains under platform or credential-provider control; the app sends the public-key credential data to its relying-party server for verification. A passkey is not a locally verified password and a successful sheet is not, by itself, proof that the server should establish a session.

The design spans three parties: the Mac app, the Apple credential provider, and the relying-party backend. The server creates unpredictable challenges and validates the returned assertion against the registered credential, expected origin or client data, challenge, relying-party identifier, signature, and user-verification policy. The client starts a request and transports data; it must not invent server challenges or declare authentication complete solely because AuthenticationServices returned successfully.

Establish the relying-party relationship

The relying-party identifier is usually the service’s domain, and the app needs the appropriate associated-domain configuration for Web Credentials. The domain relationship file and app entitlement bind the app to the site it claims to serve. Test this binding in the exact signed build and distribution channel; a debug app with a different bundle identifier or associated-domain entitlement may behave differently from the shipping app.

Choose the server’s account identifier carefully. Passkey user IDs should be opaque, stable byte strings rather than email addresses or mutable display names. The server must map the credential to an account using its own authoritative database and must not accept a client-provided account name as proof of ownership. During account recovery, require the service’s separately designed recovery controls before enrolling another credential.

Ask the server for a one-time registration challenge before creating a credential. The challenge should be unpredictable, bound to the registration session and account, short-lived, and consumed exactly once. Do not reuse one challenge across users or allow it to be replayed after a failed attempt. For authentication, the server similarly issues a fresh challenge and records what credential, account, and policy the request is intended to verify.

Create a platform credential request

For a native macOS app, ASAuthorizationPlatformPublicKeyCredentialProvider creates a registration or assertion request for the configured relying-party identifier. ASAuthorizationController manages the requests and returns control through its delegate. Provide a presentation context tied to the visible app window, retain the controller and delegate until completion, and model cancellation as a normal result.

The following is the central registration setup; challenge and userID must come from the authenticated server session, not from a hard-coded client value:

import AuthenticationServices

func beginPasskeyRegistration(
    relyingParty: String,
    challenge: Data,
    userID: Data,
    accountName: String,
    delegate: ASAuthorizationControllerDelegate,
    presentation: ASAuthorizationControllerPresentationContextProviding
) -> ASAuthorizationController {
    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
        relyingPartyIdentifier: relyingParty
    )
    let request = provider.createCredentialRegistrationRequest(
        challenge: challenge,
        name: accountName,
        userID: userID
    )
    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = delegate
    controller.presentationContextProvider = presentation
    controller.performRequests()
    return controller
}

Keep the returned controller strongly referenced for the request lifetime. On completion, check that the credential type is the expected platform public-key registration response, serialize only the documented credential fields, and send them to the server over an authenticated TLS connection. Do not log the attestation object, client data, challenge, credential ID, or user handle. These are authentication protocol values and should be treated as sensitive.

Verify registration on the server

The server validates the registration response using a maintained WebAuthn implementation. It checks the challenge and origin, relying-party ID hash, credential public key, flags, credential ID uniqueness, and registration policy. Do not write signature or CBOR parsing from scratch based on a simplified example. Apple provides a credential API; the backend still implements the relying-party verification rules.

Store the public key and credential metadata with the correct account, including the credential ID and counter semantics expected by the authenticator ecosystem. Design for credential providers whose backup and synchronization behavior can differ. A counter is one signal that may detect cloned or unexpected authenticator behavior; do not implement a rigid assumption that all synced passkeys increment counters in one universal pattern. Follow the current WebAuthn specification and your chosen server library’s guidance.

Return a clear result to the app only after the server commits the credential. If the network fails after the user completes the system sheet, the client may not know whether registration succeeded. Provide an idempotent retry keyed to the server-side ceremony rather than starting multiple independent enrollments. On retry, query the server for ceremony state and avoid registering duplicate credentials.

Authenticate with an assertion

For sign-in, fetch the current challenge and allowed credential list from the server, then create an assertion request. The authorization controller can present system UI for available credentials and the user selects one. If no credential is available or the person cancels, return to the sign-in surface with a helpful next step; do not silently fall back to an unrelated account or weaken server-side verification.

The app should preserve ceremony state only for the current request. Associate the challenge with a random request ID and account context, apply a short expiration, and clear it on success, cancellation, or timeout. Do not hold an assertion indefinitely in a background queue. The server must reject expired and already-consumed challenges even if the client retries the same network request.

After AuthenticationServices returns an assertion, send the credential response to the relying party and wait for the server’s verified result. A local result means the credential provider produced an assertion; it does not mean the relying party accepted the signature or granted the requested account privileges. Separate successful passkey verification from later authorization checks, such as whether the account is active or permitted to access a resource.

Handle multiple credential types without ambiguity

AuthenticationServices can coordinate more than one credential request, for example passkeys and passwords, but the application should understand which credential type was returned and route it to the matching server verifier. Never parse a passkey assertion as an application password or assume the first returned credential is the one the UI intended. Typed result handling should reject unexpected credential classes instead of coercing them.

Third-party credential providers may participate in the system experience. Do not promise that every passkey is stored in iCloud Keychain or available on every Mac. The user may use a managed provider, a security key, or a synced credential. Handle “no credential available,” provider denial, and unsupported platform state as distinct outcomes. Product copy should say “passkey” rather than naming a particular storage provider unless the app actually requires it.

If your app is itself a browser, there is a separate browser-oriented credential manager API for access to a person’s passkeys. Follow Apple’s browser-specific flow rather than assuming a normal relying-party app API and browser API have identical permission semantics. WKWebView can handle Web Authentication challenges for content it displays; avoid adding a second competing authorization controller unless the browser design requires it.

Secure account recovery and user experience

Offer users a way to add and remove passkeys from an already authenticated account, and communicate which device or credential they are managing without exposing secrets. Use step-up authentication or a separate recovery mechanism for high-risk operations such as removing the last credential. Provide multiple recovery options and make sure support staff cannot bypass cryptographic verification by merely changing an email address.

Passkeys protect against many password phishing patterns, but they do not make the rest of the account system invulnerable. Secure session cookies, CSRF protections, rate limits, account recovery, device management, and authorization checks remain necessary. Avoid telling a user that the account is “unhackable.” Explain the specific property: authentication uses a public-key credential bound to the relying party and the server verifies the assertion.

Test both sides of the ceremony

Test registration, assertion, cancellation, credential absence, expired challenge, repeated challenge, incorrect relying-party ID, app entitlement mismatch, missing associated domain, server timeout, and account revocation. Verify the production-signed app on a clean user account. Include a credential from an alternate provider when your support policy allows it. Confirm that the UI does not report sign-in success before the server response.

On the backend, use official WebAuthn test vectors or the library’s verifier suite for malformed client data, invalid signatures, altered challenge, wrong origin, and credential mismatch. Audit challenge creation and consumption for race conditions. Redact credential payloads from analytics and crash logs, and rotate server signing keys only according to the service’s own key management plan.

The safe passkey flow has one source of truth for every step: the server creates ceremonies and grants sessions; AuthenticationServices presents credential UI; and the app securely transports results. Keeping those responsibilities separate turns the platform API into a usable login experience without treating a successful dialog as an authorization shortcut.

Related:

Sources:

Comments