docs: correct iOS device-authentication guide#2691
Conversation
- The iOS Simulator can run device auth via relaxed attestation and the OryClientSimulator product, rather than being unusable outright. - App Attest keys cannot be biometric-gated through access control; the client gates signing with a LocalAuthentication check the server trusts. - Keychain items survive app deletion; kSecAttrAccessibleWhenUnlockedThisDeviceOnly protects backups and device migration, and a fresh install must clear stale keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR corrects factual and security-relevant guidance in the Kratos passwordless “deviceauthn” iOS documentation, aligning the guide with how iOS App Attest / Keychain behavior actually works and how the client must implement biometric gating.
Changes:
- Clarifies Simulator support by documenting relaxed attestation for development/testing (software-backed attestations via
OryClientSimulator). - Fixes Keychain guidance to reflect that
kSecAttrAccessibleWhenUnlockedThisDeviceOnlydoes not imply data is purged on uninstall, and recommends reinstall detection + cleanup. - Corrects biometric (
platform) guidance: App Attest keys can’t be access-control/biometry gated, so the client must gate each assertion withLocalAuthenticationand handle biometric-set changes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/components/Shared/kratos/passwordless/deviceauthn/ios.mdx | Updates iOS-specific deviceauthn guidance: Simulator/relaxed attestation notes, Keychain persistence semantics, and biometric gating requirements. |
| src/components/Shared/kratos/passwordless/deviceauthn/index.mdx | Updates shared deviceauthn requirements to reflect Keychain persistence across uninstall and recommend reinstall detection/cleanup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pcaillaudm
left a comment
There was a problem hiding this comment.
LGTM, just one non-blocking question
| to set on the key; instead, gate signing with a `LocalAuthentication` check | ||
| (`LAContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics)`) that must succeed before every `generateAssertion` call, | ||
| at both enrollment and login. To approximate `.biometryCurrentSet` semantics — invalidating the key when the enrolled biometric | ||
| set changes — pin the context's `evaluatedPolicyDomainState` at enrollment and reject a login when it no longer matches. |
There was a problem hiding this comment.
I noticed evaluatedPolicyDomainState is marked deprecated in the Apple docs - still fine to recommend here?
| This means that the emulator cannot be used. | ||
| This means the iOS Simulator cannot produce real attestations. For development and testing you can still run on the Simulator by | ||
| enabling <SameDeploymentLink to="kratos/passwordless/deviceauthn#relaxed-attestation-for-testing">relaxed | ||
| attestation</SameDeploymentLink>, which accepts software-backed attestations. The Ory Swift SDK ships an `OryClientSimulator` |
There was a problem hiding this comment.
| attestation</SameDeploymentLink>, which accepts software-backed attestations. The Ory Swift SDK ships an `OryClientSimulator` | |
| attestation</SameDeploymentLink>, which accepts software-backed attestations. The Ory client SDK for Swift ships an `OryClientSimulator` |
| /// kSecAttrAccessibleWhenUnlockedThisDeviceOnly — never in UserDefaults. The | ||
| /// ThisDeviceOnly attribute keeps the blob off encrypted backups and device | ||
| /// migrations, so it cannot follow the sealed secret onto another device. | ||
| /// Keychain items survive app deletion, so detect a reinstall (for example with |
There was a problem hiding this comment.
It's a little hard to decipher whether 'so detect a reinstall' is a consequence or a direct action for the developer to do. Here is a rewrite of the complete comment. (I didn't do it inline as it's too hard to review the changes.)
/// Store this entire struct inside the Keychain using the attribute
/// 'kSecAttrAccessibleWhenUnlockedThisDeviceOnly'. Never store it in UserDefaults.
/// The 'ThisDeviceOnly' flag prevents this data from being backed up
/// or migrated. The artifacts will never follow a secret to another device.
/// Because the Keychain data survives deletion, you must write code to detect
/// if the current launch is a reinstall. As the system completely wipes
/// UserDefaults data when a user deletes the app (and creates a blank new one on reinstall),
/// you can create a dummy marker/flag in UserDefaults to detect the reinstall. On boot up if
/// a 'PinArtifacts' struct is in the Keychain, but your UserDefaults marker is missing,
/// the user has reinstalled the app. You must delete these stale Keychain artifacts
/// before allowing the app to run. (None of these struct fields are secret on their own).
Corrects three factual errors in the iOS device-authentication guide, found by comparing it against a reference iOS client implementation (ory-corp/cloud PR #12950).
OryClientSimulatorproduct. Relaxed attestation is refused once the project leaves the development environment..biometryCurrentSetin its access control. App Attest keys are system-managed and accept no access-control flags, so the client must run aLocalAuthenticationcheck before everygenerateAssertioncall. The server trusts theplatformdeclaration on iOS because it cannot verify biometric gating remotely, which makes the client-side gate security-critical.kSecAttrAccessibleWhenUnlockedThisDeviceOnlycauses uninstall to purge the stored blob. That attribute keeps the blob off encrypted backups and device migrations, not off a reinstall. A fresh install must detect the reinstall (for example via aUserDefaultsinstall marker, which the system does clear) and drop any stale artifacts before use.