ReviewOS

craft-native/craft

No way to use a passkey from a native screen: there is no ASAuthorization credential capability

#353
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Craft has no way to use a passkey from a native screen, so an app built with renderer: native cannot offer passkey sign-in at all.

Why the web answer does not apply

Passkeys on the web are navigator.credentials.create() / .get(), which @stacksjs/auth's passkey module wraps (startRegistration, startAuthentication, browserSupportsWebAuthn). All of those need a browsing context. An all-native app has no web view, and a native screen's JSContext has no navigator.credentials — browserSupportsWebAuthn() is simply false there, whatever the device can actually do.

iOS's own answer is ASAuthorizationPlatformPublicKeyCredentialProvider, driven through ASAuthorizationController, which is a native API and has no JS equivalent.

What is there now

CraftNativeActions.swift handles these modules:

Auth Background Biometric Brightness Browser Clipboard Database DeepLink Device
Dialog Files Haptics Health HeartRate KeepAwake Lifecycle Linking Location Music
Network Notification Push SecureStorage Share Speech StatusBar Storage

Auth is only signInWithApple (CraftSignInWithApple), and biometric is Face ID for unlocking something the app already holds. Neither is a passkey. AuthenticationServices is already imported in CraftApp.swift, so the framework is linked — it is the credential half that is missing.

What it would take

A capability — say passkeys, gated like the rest — over ASAuthorizationPlatformPublicKeyCredentialProvider:

  • create(options) → registration, from the server's generateRegistrationOptions (challenge, rp id, user handle)
  • get(options) → authentication, from generateAuthenticationOptions
  • something for whether this device can do it at all, so a screen can decide whether to offer it

Both are person-facing (the system sheet asks for Face ID), so they belong on the untimed path — with the caveat in craft-native/craft#352, since a sheet whose result never comes back is exactly what broke Apple Health.

The relying-party id has to match the app's webcredentials: associated domain, so whatever shape the API takes should make it hard to pass an rp id the entitlement does not cover.

The fallback, for the record

A passkey flow opened in Craft's in-app browser does work, because Safari supports WebAuthn and the associated domain is the app's. So an app is not hard-blocked today — it just has to leave its own native screens to sign in, which is the thing renderer: native exists to avoid.

Context: hq.training is adding passkeys (stacksjs/hq.training#277) and is all-native on the phone, so it hits this immediately.

9 days ago

Resolved on main and shipped in v0.0.175. Native iOS screens now expose capability-gated, associated-domain-bound passkeys (craft.passkeys.isAvailable/create/get) over AuthenticationServices, with contract and generator coverage.

Sign in to comment on this issue.