FIDO2/Passkey Example Apps
These applications are provided for demonstration and learning purposes only. They are not hardened, audited, or supported for use in production environments. Do not deploy them as-is in a live system.
This guide shows you how to get the Nevis FIDO2 mobile example applications up and running against your Authentication Cloud instance.
Nevis provides two open-source example applications (one for Android and one for iOS) that demonstrate end-to-end FIDO2 passkey registration and authentication. Both are available as public repositories on GitHub and are intended as a practical starting point for development teams building native mobile passkey authentication into their own applications.
The example apps are designed to work with Nevis Authentication Cloud as the backend. The underlying FIDO2 concepts (passkey registration, assertion, and the WebAuthn challenge-response flow) apply equally to a Nevis ID deployment. However, the required server-side configuration in Nevis ID is not provided out of the box and requires additional setup.
What you need
Before you start, ensure you have the following ready:
- A running Nevis Authentication Cloud instance.
- An access key for your Authentication Cloud instance.
- The example application sources cloned from GitHub (see below).
Your development environment must meet the following requirements:
- Android
- iOS
| Requirement | Minimum version |
|---|---|
| Android | 9 (API level 28) |
| JDK | 17 |
| Android Studio | Meerkat Feature Drop 2024.3.2 |
| Requirement | Minimum version |
|---|---|
| iOS | 16.4 |
| Xcode | 16 (Swift 6.0) |
Getting the example application
Clone the repository for the platform you want to work with:
- Android
- iOS
git clone https://github.com/nevissecurity/nevis-mobile-authentication-fido2-example-android.git
The Android app uses Android Credential Manager to create and assert passkeys. The UI is built with Jetpack Compose and follows a Clean Architecture pattern.
In addition to the standard flows, the Android app supports passkey autofill: the Credential Manager autofill integration surfaces available passkeys in the keyboard bar or a bottom sheet as the user taps a username field.
Passkey APIs require Google Play Services. Use a physical device, or an emulator with Google Play Services and a Google account signed in.
git clone https://github.com/nevissecurity/nevis-mobile-authentication-fido2-example-ios.git
The iOS app uses the Apple Authentication Services framework to create and assert passkeys. By default, passkeys are stored in iCloud Keychain (via Apple's Passwords app). If the user has a third-party credential manager configured (for example, 1Password), the system may offer to save the passkey there instead. The UI is built with SwiftUI.
In addition to the standard flows, the iOS app supports passkey autofill: it integrates with the QuickType bar so the system surfaces available passkeys directly in the keyboard as the user taps a username field.
Passkey APIs are not available on the iOS Simulator. Run the app on a physical device with iCloud Keychain enabled and an Apple ID signed in.
Configure and run
Follow the setup steps in the README.md of the cloned repository. In summary:
- Android
- iOS
In the project root, create or edit
local.propertiesand set the hostname of your Authentication Cloud instance and your access key:HOST_NAME=<your-instance>.mauth.nevis.cloud
BACKEND_ACCESS_TOKEN=<your-access-key>Open the project in Android Studio and run it on a physical device.
- Open
Configuration.plistand set the hostname of your Authentication Cloud instance and your access key. - Open the Xcode project and run it on a physical device.
The example apps embed the access key directly to keep setup self-contained. This is intentional for demo purposes only. The Authentication Cloud REST API is designed exclusively for server-to-server calls: access keys carry unrestricted access to your instance and must never be stored on user devices or included in distributed app packages.
In a production native app integration, the mobile app does not call the Authentication Cloud API directly. The recommended architecture is:
- The mobile app sends a request to your own application backend.
- Your backend holds the access key, calls the Authentication Cloud API, and receives the challenge or credential options in response.
- Your backend forwards these options to the mobile app.
- The app uses the platform passkey APIs to create or assert the credential and sends the result back to your backend.
- Your backend forwards the credential to Authentication Cloud for verification and returns the outcome to the app.
This keeps the access key server-side and gives you full control over authentication policy, logging, and session management. The FIDO2 indirect communication flows document this pattern in detail:
- Register a FIDO2 authenticator (indirect communication)
- Authenticate with FIDO2 (indirect communication)
Authentication Cloud also supports direct communication flows where the client communicates with Authentication Cloud without an intermediate backend. This pattern is primarily designed for web and JavaScript integrations; for native mobile apps the indirect pattern is strongly preferred, as direct communication would require placing the access key on the device.
Demo scenarios
Both apps expose the following scenarios from a single main screen.
Registration
- Enter a username and tap Register.
- The platform passkey creation sheet appears. Authenticate with biometrics or your device PIN to confirm.
- The passkey is registered with your Authentication Cloud instance and stored in the platform credential store (Google account on Android, iCloud Keychain on iOS).
Attempting to register the same username a second time may result in an error. The exact behavior depends on the credential manager: Apple's Passwords app prevents creating a second passkey for the same relying party, while third-party managers such as 1Password may allow it. The server may also return an error if the username is already enrolled. The app demonstrates this error handling explicitly.
Authentication
Username-based authentication:
- Enter a previously registered username and tap Authenticate.
- The platform passkey selection sheet appears. Authenticate with biometrics or your device PIN.
- The app displays the resulting access token claims on success, or an error if no passkey is found for that username.
Usernameless authentication (discoverable credential flow):
- Leave the username field blank and tap Authenticate.
- The system presents a list of all available passkeys for the relying party. Select one and authenticate.
- No prior knowledge of the username is required on the user side.
iOS only: keyboard-assisted authentication:
When the username field is focused, iOS surfaces registered passkeys directly in the QuickType bar above the keyboard. The user can tap a passkey suggestion to authenticate without navigating to a dedicated flow.
Cross-device usage
The example apps also exercise cross-device scenarios that demonstrate the portable nature of FIDO2 passkeys:
Using a mobile passkey in a desktop browser: a passkey registered through the app can be used to authenticate in a desktop browser. The browser displays a QR code which the mobile device scans to complete the assertion. This works on both Android and iOS.
Storing a passkey on a different device: a registration can be initiated on one device and completed on a second device by scanning the QR code shown during registration. On Android, set the authenticator attachment to
cross-platformbefore starting the registration. The passkey is stored on the device that scanned the QR code.iOS/Android interoperability: a passkey registered via the iOS app can be used for authentication initiated from an Android device (and vice versa), as long as both connect to the same Authentication Cloud backend.
iOS iCloud sync: a passkey registered on one iPhone is automatically synchronized to other Apple devices signed in to the same Apple ID via iCloud Keychain, and can be used in Safari on macOS without any additional steps.
Web-based sign-in flow
In addition to native passkey flows, the apps demonstrate a browser-based sign-in flow. In this scenario, the app opens the Authentication Cloud Test & Debug page in a browser, the user authenticates using FIDO2, and the result is passed back to the app as a JWT via a deep link. The app then introspects the token to display the result. In a real integration, the app would open your own FIDO2-enabled login page instead.
The webpage serving the login must redirect back to the app using a custom URL scheme, for example:
<scheme>://success?token=<authorizationToken>
<scheme>://failure?error=<errorMsg>
The two platforms implement this differently:
iOS uses ASWebAuthenticationSession, which is straightforward and well-supported.
Android uses Chrome Custom Tabs. Standard WebView does not support WebAuthn and cannot be used for passkey flows.
Custom Tabs are required for the browser-based sign-in flow on Android. The following browsers are known to support Custom Tabs: Chrome, Brave, Ecosia, Edge, and Vivaldi. Firefox, Firefox Focus, DuckDuckGo, and Samsung Internet do not support the minimized Custom Tab mode. Opera and Mi Browser do not support Custom Tabs at all; when one of these is set as the default browser, a fallback to another Custom-Tab-capable browser is necessary.
The browser-based sign-in flow used by these example apps (Chrome Custom Tabs on Android, ASWebAuthenticationSession on iOS) does not require the .well-known association files described in Native app association for passkeys. In these flows, WebAuthn is handled entirely by the browser session — the native app only launches the URL and receives the result back via deep link.
This does not apply if you embed a web view directly in your app (Android WebView with Credential Manager, or iOS WKWebView). In that scenario, credential sharing between the web context and the native layer requires the same association files and entitlements described in Native app association for passkeys.
Platform behavior and limitations
Passkey sync and device-binding
Platform passkeys on both iOS and Android are synced credentials by default. It is not possible to create a device-bound passkey that cannot leave the device when using the standard platform authenticator:
- iOS: when using Apple's built-in credential store, passkeys are synced via iCloud Keychain. Apple does not expose a configuration option to prevent this. If the user has a third-party credential manager configured (for example, 1Password), that manager's sync settings apply instead.
- Android: passkeys created with the standard Credential Manager are backed up to the user Google account.
If device-binding is a hard requirement, two alternatives are available:
- Nevis UAF-based mobile authentication: the Nevis Mobile Authentication SDK and the brandable Nevis Access App use the FIDO UAF standard, which creates device-bound credentials that cannot be backed up or synced to another device. See Mobile Authentication (FIDO UAF) Example Apps to explore these capabilities through the SDK example applications.
- Hardware security key: a key such as a YubiKey used as a cross-platform authenticator is device-bound by design, as the private key never leaves the hardware token.
FIDO2 option support by platform
The apps allow you to configure FIDO2 options before each operation, but the actual behavior depends on the platform authenticator. The table below summarizes how each option is handled:
| FIDO2 option | iOS platform passkey | Android platform passkey | Hardware security key |
|---|---|---|---|
userVerification | Always enforced (Face ID / Touch ID / passcode); discouraged is ignored | Respected | Respected (depends on key capabilities) |
authenticatorAttachment | Ignored; platform authenticator is always used | Respected | Respected (platform vs. cross-platform) |
residentKey | Always resident and discoverable | Respected | Fully configurable |
attestation | Not supported; always returns none | Not supported | Supported (none, indirect, direct) |
| Syncs across devices | ✅, via iCloud Keychain | ✅, via Google account | ❌; device-bound by design |
| Can be made device-bound | ❌ | ❌ | ✅ |