flutter passkeys packagePasskeys Implementation

Integrate Passkeys into Flutter Apps (Open-Source Passkeys Package)

This article shows how to use the open-source Flutter passkeys package to add passkeys to Flutter apps, either with your own or a hosted passkey backend.

Blog-Post-Author

Vincent

Created: July 26, 2023

Updated: October 1, 2024


We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.

Hello Flutter developers,

With this tutorial, we want to introduce you to our new open-source Flutter passkeys package. The Flutter passkeys package offers passkeys / WebAuthn implementations for Android, iOS and web (more platforms to come; this post focuses on Android and iOS) with emphasis on maximum flexibility and ease of use for developers. We want to help make the internet a safer place and are happy if you help by starting to ditch passwords and join the passkey- era.

Regarding passkeys, currently available Flutter packages are either complicated to set up or require an external passkey backend, that you have to set up and maintain on your own. This can be quite challenging and tedious (especially when you want to quickly prototype with passkeys).

Therefore, the main goals of the Flutter passkeys package are:

  • Simplicity : Build an easy-to-use Flutter passkeys package that enables every Flutter developer to implement passkeys.
  • Flexibility : Provide developers the possibility to integrate their own passkey backend.
  • Support : There are a few pitfalls when setting up passkeys. We try to provide guidance and help for common mistakes.

Note: For realizing these main goals, no Corbado services are needed. You can simply use the passkeys package together with your own passkey backend.

This tutorial is structured as follows:

The code of this tutorial is taken from the Flutter passkeys package example, which you can find on GitHub and on pub.dev.

Heres a preview of the running example app in Android:

Flutter Passkey Flow Example GIF

1. Flutter passkeys

To better understand the architecture of the Flutter passkeys package, we first look at passkeys and how they work.

In a traditional authentication system (e.g., a password-based one), a developer sets up a backend including a database where user credentials (e.g., email + passwords) will be stored after the sign up. The password must only be known by the user and developers must take precautions to protect these passwords in the backend (e.g. hashing, salting).

1.1. General passkeys flow

Now, lets come to passkeys. For a passkey authentication system, there is also a sign up and a login flow. Moreover, there is also a backend involved which is called relying party server. During the sign up flow, the user creates a public / private key pair (by using e.g. Face ID or Touch ID). The public key is shared with the relying party server and is permanently stored there. The private key is kept securely on the users device.

During every login, the relying party server is asked for a cryptographic challenge by the client (the native iOS / Android app or the web browser). The challenge can only be solved with the private key. Access to that private key is protected with Face ID or Touch ID. As soon as the user proves his identity using his biometrics, the device can sign the challenge and sends it back to the relying party server.

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.

Join Passkeys Community

1.2. Flutter passkeys package architecture

Next, well explain some key concepts of the Flutter passkeys package (non-exhaustive architecture). In a nutshell, the package supports by abstracting the OS-specific authenticator implementations. The authenticator provides OS functionalities that can create a passkey and sign passkey challenges by asking the user for their biometrics. It's written in OS-specific languages (e.g. Swift, Kotlin, JavaScript).

The major benefit of this architecture is that the Flutter passkeys package abstracts away the complexities of handling OS -specific implementations of the authenticator. This simplification greatly eases the development process, offering developers straightforward usability while retaining the flexibility to select their own relying party server and the corresponding adapter in Flutter.

The relying party server operates on a remote server, with the Flutter application acting as a client. Integration is facilitated through a Flutter adapter tailored to the specific relying party server, allowing for the use of a customized backend. For added convenience, Corbado provides both an adapter (see the Flutter corbado_auth package) and a hosted relying party server that are optimized for the Flutter passkeys package, enabling rapid testing. Nevertheless, you have the option to employ your own relying party server if preferred.

Flutter Passkeys Package Architecture

A more detailed view at the architecture:

1. Native plugins

1.1. passkeys_android: The native implementation for Android that is responsible for receiving the challenge, verifying the user and sending a response to the challenge back afterwards. All user interaction on Android is handled by the passkeys_android plugin.

1.2. passkeys_ios: The native implementation for iOS that is responsible for receiving the challenge, verifying the user and sending a response to the challenge back afterwards. All user interaction on iOS is handled by the passkeys_ios plugin.
1.3. passkeys_web: The implementation for web apps that is responsible for receiving the challenge, verifying the user and sending a response to the challenge back afterwards. All user interaction on the web is handled by the passkeys_web plugin.

2. Flutter (Dart)

2.1. Passkey authenticator: The authenticator is the core of the passkeys package , handles the platform dependent parts and coordinates the native plugins (1.1, 1.2 & 1.3). On the other side, it communicates with the Relying party server adapter (2.2).

2.2. Relying party server adapter: The Relying party server adapter provides a specific implementation that is needed to relay the communication between the Passkey authenticator (2.1) and the Relying party server (3). It is written inside your Flutter app.

3. Relying party server: The Relying party server is responsible for managing passkeys. It generates the challenge, verifies the validity of the frontends response, and manages the users and passkeys.

As depicted in the chart, its possible to add more native plugins. Moreover, you can use any specific implementation and relying party server. For reference, in the example folder, we provide a specific implementation for Corbado and use Corbado as relying party server.

The great benefit of this modular architecture is that it allows to directly use passkeys out-of-the-box (e.g. with Corbado as passkey backend provider), while also giving the flexibility to integrate your own passkey backend instead.

Slack Icon

Become part of our Passkeys Community for updates and support.

Join

2. Create a Flutter application

For this tutorial, we just make use of the example project in /examples , that you can clone to your machine with the following command.

git clone https://github.com/corbado/flutter-passkeys

Go to the example directory:

cd packages/passkeys/passkeys/example

In the following, we explain selected important parts of the code.

2.1 Setup UI

For simplicity, a single stateful widget is created which represents a login, signup and logged-in page dependent on the state of the application.

Upon initialization a shared relying party server by Corbado is started and used in the following sections. In step 5.2, we will show how to replace the current implementation with your own passkey infrastructure.

main.dart
// flutter-passkeys/packages/passkeys/passkeys/example/lib/main.dart import 'package:corbado_auth/corbado_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:passkeys_example/pages/loading_page.dart'; import 'package:passkeys_example/providers.dart'; import 'package:passkeys_example/router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // This is a nice pattern if you need to initialize some of your services // before the app starts. // As we are using riverpod this initialization happens inside providers. // First we show a loading page. runApp(const LoadingPage()); // Now we do the initialization. final relyingPartyServer = CustomCorbadoAuth(); const corbadoProjectId = String.fromEnvironment('CORBADO_PROJECT_ID'); await relyingPartyServer.init(corbadoProjectId); runApp( ProviderScope( overrides: [ relyingPartyServerProvider.overrideWithValue(relyingPartyServer), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: router, theme: ThemeData( useMaterial3: false, colorScheme: const ColorScheme( brightness: Brightness.light, primary: Color(0xFF1953ff), onPrimary: Colors.white, secondary: Colors.white, onSecondary: Colors.black, error: Colors.redAccent, onError: Colors.white, background: Color(0xFF1953ff), onBackground: Colors.white, surface: Color(0xFF1953ff), onSurface: Color(0xFF1953ff), ), ), ); } }

That's it concerning the UI of the Flutter application.

2.2 Perform passkey authentication

When the user clicks on the login button, the login is performed, and the widget is updated once the result is available:

sign_in_page.dart
// flutter-passkeys/packages/passkeys/passkeys/example/lib/pages/sign_in_page.dart import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:passkeys/types.dart'; import 'package:passkeys_example/pages/base_page.dart'; import 'package:passkeys_example/providers.dart'; import 'package:passkeys_example/router.dart'; class SignInPage extends StatefulHookConsumerWidget { const SignInPage({super.key}); @override ConsumerState<SignInPage> createState() => _SignInPageState(); } class _SignInPageState extends ConsumerState<SignInPage> { final _emailController = TextEditingController(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final passkeyAuth = ref.watch(relyingPartyServerProvider); // As soon as the view has been loaded prepare the autocompleted passkey sign in. passkeyAuth .autocompletedLoginWithPasskey() .then((value) => context.go(Routes.profile)) .onError( (error, stackTrace) { if (error is PasskeyAuthCancelledException) { debugPrint( 'user cancelled authentication. This is not a problem. It can just be started again.'); return; } debugPrint('error: $error'); }, ); }); } @override Widget build(BuildContext context) { final error = useState<String?>(null); final passkeyAuth = ref.watch(relyingPartyServerProvider); return BasePage( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), child: Text( 'Tired of passwords?', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, ), ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Text( 'Sign in using your biometrics like fingerprint or face.', style: TextStyle( fontSize: 20, ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: TextField( controller: _emailController, autofillHints: [_getAutofillHint()], keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'email address', ), ), ), if (error.value != null) Text( error.value!, style: TextStyle(color: Theme.of(context).colorScheme.error), ) else Container(), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: () async { try { final email = _emailController.value.text; await passkeyAuth.loginWithPasskey(email: email); context.go(Routes.profile); } catch (e) { if (e is PasskeyAuthCancelledException) { debugPrint( 'user cancelled authentication. This is not a problem. It can just be started again.'); return; } error.value = e.toString(); debugPrint('error: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: Theme.of(context).primaryColor, content: Text('$error'), duration: const Duration(seconds: 10), ), ); } }, child: const Text('sign in'), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 50, child: OutlinedButton( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 10), side: BorderSide(width: 2, color: Theme.of(context).primaryColor), ), onPressed: () => context.go(Routes.signUp), child: const Text('I want to create a new account'), ), ), ], ), ); } String _getAutofillHint() { if (kIsWeb) { return 'webauthn'; } else { return AutofillHints.username; } } }

When the user clicks on the sign up button, the sign up is performed, and the widget is updated once the result is available:

sign_up_page.dart
// flutter-passkeys/packages/passkeys/passkeys/example/lib/pages/sign_up_page.dart import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:passkeys_example/pages/base_page.dart'; import 'package:passkeys_example/providers.dart'; import 'package:passkeys_example/router.dart'; class SignUpPage extends HookConsumerWidget { const SignUpPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final emailController = TextEditingController(); final error = useState<String?>(null); final passkeyAuth = ref.watch(relyingPartyServerProvider); return BasePage( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), child: Text( 'Tired of passwords?', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, ), ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Text( 'Sign up using your biometrics like fingerprint or face.', style: TextStyle( fontSize: 20, ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: TextField( controller: emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'email address', ), ), ), if (error.value != null) Text( error.value!, style: TextStyle(color: Theme.of(context).colorScheme.error), ) else Container(), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: () async { final email = emailController.value.text; try { await passkeyAuth.signUpWithPasskey(email: email); context.go(Routes.profile); } catch (e) { error.value = e.toString(); } }, child: const Text('sign up'), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 50, child: OutlinedButton( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 10), side: BorderSide(width: 2, color: Theme.of(context).primaryColor), ), onPressed: () => context.go(Routes.signIn), child: const Text('I already have an account'), ), ), ], ), ); } }

3. Run the example app

When you cloned the GitHub repo, make sure that you are now in the root directory to run the example app. Also, make sure that you have a running virtual device or a physical device connected to your machine.

Now, you are fully set and you can start signing up with your first passkey in the example by running the following command:

melos run example-passkeys-native

Note that you share a relying party server with other Flutter developers for this example. Its user table is flushed every day. We have built the example this way to make it very simple to set up. For an example, this works totally fine, but if you want to build your own app, you need your own relying party server. Moreover, you cannot run the example on physical iOS devices but only on a simulator (for Android, physical devices + emulators work).

4. Configure your relying party server

Note: If you just want to quickly try out passkeys in your Flutter app, you don't need to modify anything, because the previous example comes with a pre- configured relying party server that can serve passkeys without any additional setup.

The following parts only describe the settings you need to make if you have your own relying party server up and running and want to let your Flutter apps (with the passkeys package) communicate with your relying party server. Setting up your own relying party server from scratch is something that goes beyond the scope of this tutorial. There are several other resources that you can use. Moreover, we plan to publish a separate blog post for setting up a relying party server.

To make your relying party server ready for communication with your Flutter app, you need to obtain some platform-specific information that you need later and also need to modify some platform-specific settings.

4.1 Set up Android app

You will need your package name (e.g. com.passkeys.example) and your SHA-256 fingerprint (e.g.54:4C:94:2C:E9:...).

You can obtain the SHA-256 fingerprint of your debug signing certificate by executing the following command:

macOS / Linux:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

Windows:

keytool -list -v -keystore "\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android

Flutter Passkey Integration: Android Debug Keystore

This information is needed for step 5.1

If you encounter any issues, please have a look at Troubleshooting.

Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

4.2 Set up iOS app

4.2.1 Create an iOS app and configure the example in XCode

You need to establish trust between your iOS app and the relying party server. Your app will be identified through your Application Identifier Prefix (e.g. 9RF9KY77B2) and your Bundle Identifier(e.g. com.passkeys.example). You need an Apple developer account to set up both. If you haven't got one yet, set up a new account.

Note: When creating your Bundle Identifier, make sure that the "Associated Domains " capability is enabled.

Flutter Passkey Integration:  Integration Apple Bundle ID

The Application Identifier Prefix can be obtained by going to your Apple Developer Certificates, Identifier & Profiles associated with your Apple Developer account.

Open your app's code in Xcode now. In "Runner -> Signing & Capabilities" enter your Application Identifier Prefix and your Bundle Identifier.

4.2.2 Set up iOS Application Identifier Prefix and Bundle Identifier

To set up your iOS app, you will need your Application Identifier Prefix and your Bundle Identifier that we set up in step 4.2.1.

Afterwards, you need to host an apple-app-site-association file on your relying party server on the given relying party ID https://{your-relying-party-ID}/.well-known/apple-app-site-association. This file will be downloaded by iOS when you install your app. To tell iOS where to look for the file, we need the next step in our setup.

4.2.3 Configure your iOS project

In your Xcode workspace, you need to configure the following settings: In "Signing & Capabilities" tab, add the "Associated Domains " capability and add the following domain: webcredentials:{your-relying-party-ID}. Now, iOS knows where to download the apple-app-site-association file from.

If you forget about this step, the example will show you an error message like Your app is not associated with your relying party server. You have to add....

Your configuration inside Xcode should look similar to what is shown in the screenshot below (you will have a different Bundle Identifier and Associated Domain).

Flutter Passkey Integration:  Apple Signing Capabilities

Note, it's important to add the domain without the protocol (https://) and without a path.

5. (Optional) Customizations

Next, we will explore the options for customizing your passkey experience and show you how to switch to your own passkey infrastructure provider.

5.1 Bind passkeys to specific domain

Each passkey is bound to a certain domain, the so-called relying party ID. If you use the example, passkeys are bound to a shared Corbado subdomain at https://pro-6244024196016258271.frontendapi.corbado.io.

Of course, you can also use your own domain for passkey binding. To do so, you need to verify that both the native app and domain belong to you. Therefore, you need to host the configuration file on your domain:

To create these files on your own, you can copy the file structure from the shared Corbado subomain above. Then, you need to update the respective relying party ID / specific domain in the file.

  • Android : Update package_name and sha256_cert_fingerprints with the information from step 4.1 in assetlinks.json
  • iOS : Update webcredentials.app with the information from step 4.2.3 in apple-app-site-association

In the case of Android, for the assetlinks.json file, you can also use Android Studios Digital Asset Links File Generator to generate the file. You find it under Tools > App Links Assistant > Open Digital Asset Links File Generator in Android Studio.

Flutter Passkey Integration: Android Studio App Linkin Assistant

Flutter Passkey Integration: Android Studio App Linkin Assistant Details

Flutter Passkey Integration: Declare Website Association

5.2 (Optional) Integrate Your Own Passkeys Backend

If you want to build an application that is entirely hosted and maintained by you, you can do so by bringing your own passkey backend infrastructure, while still using the Flutter passkeys package for easy integration.

Therefore, you need to create a specific implementation in Flutter for the Relying party server adapter (2.2). We recommend to take a look at the sample implementation of the corbado_auth adapter (custom_corbado_auth.dart) and provide something similar for your own backend.

custom_corbado_auth.dart
// flutter-passkeys/packages/corbado_auth/lib/src/custom_corbado_auth.dart: import 'dart:async'; import 'package:corbado_auth/src/services/corbado/corbado.dart'; import 'package:corbado_auth/src/services/corbado/corbado_stub.dart' if (dart.library.html) 'package:corbado_auth/src/services/corbado/corbado_web.dart' if (dart.library.io) 'package:corbado_auth/src/services/corbado/corbado_native.dart'; import 'package:corbado_auth/src/types/auth_response.dart'; import 'package:corbado_auth/src/types/email_otp_state.dart'; import 'package:corbado_auth/src/types/webauthn/authentication.dart'; import 'package:corbado_auth/src/types/webauthn/registration.dart'; import 'package:passkeys/authenticator.dart'; /// The Cobardo Auth SDK helps you with bringing passkey authentication to your /// app. class CustomCorbadoAuth { /// Constructor CustomCorbadoAuth() : passkeyAuthenticator = PasskeyAuthenticator(); final PasskeyAuthenticator passkeyAuthenticator; late CorbadoService corbadoService; Future<void> init(String projectId, {String? customDomain}) async { corbadoService = await createClient(projectId, customDomain: customDomain); } /// Signs up a user by registering a new passkey (using the passkeys package). Future<AuthResponse> signUpWithPasskey({ required String email, String? fullName, }) async { final res1 = await corbadoService.startSignUpWithPasskey(email, fullName ?? email); final platformReq = res1.toPlatformType(); final platformResponse = await passkeyAuthenticator.register(platformReq); final req2 = FinishRegisterRequest.fromRegisterCompleteRequest(platformResponse); final res2 = await corbadoService.finishSignUpWithPasskey(req2); return res2; } /// Signs up a user by sending out an email OTP (one-time password) /// Calling this method only initiates the OTP. /// When the user has provided the code he received by email completeEmailCode /// must be called to finish the sign up. Future<EmailOTPState> startSignUpWithEmailCode({ required String email, String fullName = '', }) async { return corbadoService.startSignUpWithEmailCode( email, fullName, ); } /// Completes an email OTP transaction. /// This can either be a sign up or a sign in. Future<AuthResponse> finishEmailCode({ required String token, required String code, }) async { return corbadoService.completeEmailCode( token, code, ); } /// Init a user sign in using email OTP. /// Similarly to registerWithEmailCode a email code will be sent to the user. /// This code must then be provided by calling completeEmailCode. Future<EmailOTPState> startLoginWithEmailCode({ required String email, }) async { return corbadoService.startLoginWithEmailCode(email); } /// Signs in a user relying on a passkey. /// This is the recommended way to do sign in with passkeys as the user does /// not have to remember his username. /// /// This method should be called when the sign in page is loaded. /// Depending on the platform, the passkey ceremony will be started /// immediately without any user interaction (e.g. Android) or it requires /// additional user input (e.g. iOS or web where the user needs to click the /// TextField). Future<AuthResponse> autocompletedLoginWithPasskey() async { return _loginWithPasskey(conditional: true); } /// Signs in a user relying on a passkey. /// This is an alternative to autocompletedSignInWithPasskey. /// It should be called when the user explicitly wants to type in a username. Future<AuthResponse> loginWithPasskey({required String email}) async { return _loginWithPasskey(email: email, conditional: false); } Future<AuthResponse> _loginWithPasskey({ required bool conditional, String email = '', }) async { final res1 = await corbadoService.startLoginWithPasskey(email, conditional); final platformReq = res1.toPlatformType(conditional: conditional); final platformResponse = await passkeyAuthenticator.authenticate(platformReq); final req2 = FinishLoginRequest.fromPlatformType(platformResponse); final res2 = await corbadoService.finishLoginWithPasskey(req2); return res2; } }

Originally, we provided a general interface, but due to the heterogeneous nature of Relying party servers and the varied preferences of developers, we decided to discontinue it. Additionally, the maintenance and support for different Relying party servers became too cumbersome and ultimately unsustainable.

6. Troubleshooting

6.1 Weak SHA-1 algorithm for Android keystore

If youve executed one of the commands in step 2.1.1 to view your SHA-256 fingerprint and you see an error message like the following SHA1withRSA (weak):

Flutter Weak SHA-1 Algorithm

Then, you need to create a new Android keystore with a stronger algorithm. You can do so with one of the following commands:

  • macOS / Linux:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
  • Windows:
keytool -list -v -keystore "\.android\debug.keystore-alias androiddebugkey -storepass android -keypass android

6.2 Android screen lock not set up on virtual Android device

If you run the application in an emulator and it says that you can't create a passkey, you have to log into your Google account and properly set up a screen lock or biometrics on the device.

First, to log into your Google account, open settings, click on the icon in the top right and then on "Sign in to your Google Account".

Flutter Passkey Integration: Troubleshooting Sign in to Google Account

Secondly, to set up the screen lock, open the settings, search for security settings and add a PIN as well as a fingerprint as shown below (PIN is required for fingerprint):

Flutter Passkey Integration: Troubleshooting No Screen Lock

7. Next steps

So far, we've seen how to set up passkey authentication in a Flutter app with the passkeys package.

We now want to quickly go through the most common use cases for Flutter developers when using passkeys. We will try to show how this package can help with each use case. While use case 1 is a very basic one (exploration only), use case 2 and use case 3 are show cases in which real apps are built. Use case 4 and use case 5 are more advanced solutions that try to help you with building more complicated apps.

7.1 Prototype with passkeys

You just want to see passkeys in action in a Flutter app? Then the example of this package is the right point for you to start. There is no configuration required and you can go through sign up and login flows on your emulator.

Note:

  • You share a relying party server with other Flutter developers. Its user table is flushed every day. We have built the example this way to make it very simple to set up. For an example, this works totally fine, but if you want to build your own app, you will need your own relying party server.
  • You cannot run the example on physical iOS devices but only on a simulator (for Android, physical devices and emulators work).

7.2 Use passkeys for your app with an existing relying party server

If you already have a relying party server, you just need to tell this package how to interact with it. This is done by implementing a Relying party server adapter (2.2). The code below shows a sample implementation of the adapter for corbado_auth that you can use as a reference:

custom_corbado_auth.dart
// flutter-passkeys/packages/corbado_auth/lib/src/custom_corbado_auth.dart: import 'dart:async'; import 'package:corbado_auth/src/services/corbado/corbado.dart'; import 'package:corbado_auth/src/services/corbado/corbado_stub.dart' if (dart.library.html) 'package:corbado_auth/src/services/corbado/corbado_web.dart' if (dart.library.io) 'package:corbado_auth/src/services/corbado/corbado_native.dart'; import 'package:corbado_auth/src/types/auth_response.dart'; import 'package:corbado_auth/src/types/email_otp_state.dart'; import 'package:corbado_auth/src/types/webauthn/authentication.dart'; import 'package:corbado_auth/src/types/webauthn/registration.dart'; import 'package:passkeys/authenticator.dart'; /// The Cobardo Auth SDK helps you with bringing passkey authentication to your /// app. class CustomCorbadoAuth { /// Constructor CustomCorbadoAuth() : passkeyAuthenticator = PasskeyAuthenticator(); final PasskeyAuthenticator passkeyAuthenticator; late CorbadoService corbadoService; Future<void> init(String projectId, {String? customDomain}) async { corbadoService = await createClient(projectId, customDomain: customDomain); } /// Signs up a user by registering a new passkey (using the passkeys package). Future<AuthResponse> signUpWithPasskey({ required String email, String? fullName, }) async { final res1 = await corbadoService.startSignUpWithPasskey(email, fullName ?? email); final platformReq = res1.toPlatformType(); final platformResponse = await passkeyAuthenticator.register(platformReq); final req2 = FinishRegisterRequest.fromRegisterCompleteRequest(platformResponse); final res2 = await corbadoService.finishSignUpWithPasskey(req2); return res2; } /// Signs up a user by sending out an email OTP (one-time password) /// Calling this method only initiates the OTP. /// When the user has provided the code he received by email completeEmailCode /// must be called to finish the sign up. Future<EmailOTPState> startSignUpWithEmailCode({ required String email, String fullName = '', }) async { return corbadoService.startSignUpWithEmailCode( email, fullName, ); } /// Completes an email OTP transaction. /// This can either be a sign up or a sign in. Future<AuthResponse> finishEmailCode({ required String token, required String code, }) async { return corbadoService.completeEmailCode( token, code, ); } /// Init a user sign in using email OTP. /// Similarly to registerWithEmailCode a email code will be sent to the user. /// This code must then be provided by calling completeEmailCode. Future<EmailOTPState> startLoginWithEmailCode({ required String email, }) async { return corbadoService.startLoginWithEmailCode(email); } /// Signs in a user relying on a passkey. /// This is the recommended way to do sign in with passkeys as the user does /// not have to remember his username. /// /// This method should be called when the sign in page is loaded. /// Depending on the platform, the passkey ceremony will be started /// immediately without any user interaction (e.g. Android) or it requires /// additional user input (e.g. iOS or web where the user needs to click the /// TextField). Future<AuthResponse> autocompletedLoginWithPasskey() async { return _loginWithPasskey(conditional: true); } /// Signs in a user relying on a passkey. /// This is an alternative to autocompletedSignInWithPasskey. /// It should be called when the user explicitly wants to type in a username. Future<AuthResponse> loginWithPasskey({required String email}) async { return _loginWithPasskey(email: email, conditional: false); } Future<AuthResponse> _loginWithPasskey({ required bool conditional, String email = '', }) async { final res1 = await corbadoService.startLoginWithPasskey(email, conditional); final platformReq = res1.toPlatformType(conditional: conditional); final platformResponse = await passkeyAuthenticator.authenticate(platformReq); final req2 = FinishLoginRequest.fromPlatformType(platformResponse); final res2 = await corbadoService.finishLoginWithPasskey(req2); return res2; } }

7.3 Use passkeys for your app but you don't want to build your own relying party server

To use passkeys in your app, you need a relying party server. If you don't want to build one, you can use already existing solutions. To allow this package to integrate with a relying party server, the Relying party server adapter must be implemented for that particular relying party server.

Corbado lets you set up a relying party server. To save time and effort, you can use Corbado as a relying party server. Find an example how to do this including a step-by-step guide here.

You can use every other SaaS provider that allows you to set up a relying party server though. All you need to do is implement the Relying party server adapter.

7.4 Build an app that supports more advanced use cases with passkeys

While this package allows you to use passkeys for authentication, a few challenges remain unsolved, e.g.:

  • App users don't want to login every time they open an app, so how can we keep them logged in?
  • Not every device supports passkeys yet, how can we provide an alternative login for these users?
  • A user might have used your app on another device and now tries to login on a new device. His passkey is still on that old device, so how can we log him in on the new device?

Solving these challenges goes beyond the scope of this package.

As a solution, you can create your own authentication SDK that builds on top of the passkeys package. This can make sense if you want to build your own authentication backend.

Alternatively, you can save time and use our solution for that problem: corbado_auth. This is a separate flutter package that builds on top of the passkeys package to provide solutions for the above challenges. For more information, check out its documentation and examples.

7.5 Use Firebase together with passkeys

Firebase is a great platform that helps you with building your app. We offer a "Passkeys for Firebase" extension that you can install in the Firebase console to use the passkeys with Firebase Auth.

Flutter Icon

Add passkeys to your Flutter app.

Start For Free

8. Conclusion

In this tutorial, we showed how to add passkey authentication to a Flutter app using the passkeys package. The package was designed with flexibility in mind, so you can get started fast with Corbado, but also have the option to host everything yourself and only make use of the simple passkey integration.

While the Flutter passkeys package will always remain a clean modular package just for passkey signup and login, the corbado_auth package will add features that can help during the development of more complex apps. Examples of these feature are:

  • magic links / OTP as fallback (when an existing user has not set up a passkey on a new device)
  • keep a user logged in even when he closes the app
  • cross-sync passkeys with a web app

In an upcoming tutorial, well explain in detail how to set up these features with the corbado_auth package in your Flutter app.

Regarding the passkeys package itself, it's planned to

  • add more platform support, e.g. for macOS or Windows apps.
  • more tooling to develop and test your flutter integration (e.g. a hosted open-source relying party server)

Share this article


LinkedInTwitterFacebook

Table of Contents

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free