flutter-passkey-authentication

Tutorial: How to Get Full Flutter Passkey Auth SDK in <1h

This tutorial describes how to use the corbado_auth Flutter package incl. session and user management to offer your users passkeys with fallback solutions.

Blog-Post-Author

Lukas

Created: September 5, 2023

Updated: February 17, 2025


Hello Flutter developers,

In our last tutorial, we introduced the open-source Flutter passkeys package that allows an easy and flexible integration of passkeys into your Flutter app with any passkey backend provider.

To go one step further, we now want to showcase how you can build a Flutter app with a passkey-first authentication and user management solution (not just the passkey login itself). This solution comes in the form of the corbado_auth package. Besides the pure passkey login and sign-up functionality (which are offered by the passkeys package already), the corbado_auth package currently comes with the following additional features:

  • Session management
  • Email OTPs as fallback (and double opt-in)

Setting up passkeys on your own is still a struggle, but also taking care of these additional features on your own requires a lot of developer resources if done properly, so we want to take that burden for you and let you focus on the core features of your Flutter app.

The corbado_auth package together with Corbados service is a full authentication SDK that covers all aspects of user management and authentication for you (e.g. recovery, fallbacks, etc.). The corbado_auth package currently supports Android, iOS and web.

You can use the package as well as Corbado for free.

The main goals of the corbado_auth Flutter package are:

  • Simplicity : Build an easy-to-use Flutter passkey package that enables every Flutter developer to implement passkey-based authentication with little effort.
  • Holistic : All aspects are covered, so you can delegate user management and authentication to Corbado.
  • Support : There are a few pitfalls when setting up authentication. We try to provide guidance and help for common mistakes.

This tutorial is structured as follows:

The code of this tutorial is taken from our corbado_auth integration example, which you can find here.

Heres a preview of the resulting app on a virtual iOS device:

First, you'll have a login and a sign up page, which will look as follows:

Flutter passkeys signin & signup

You'll also have a profile page as well as a token-details page that will display the information that the user's short-term session (stored as JWT) contains. This JWT is automatically refreshed by the corbado_auth package:

Flutter passkeys profile and token details

The screenshots above are the result of the following tutorial, including login and sign up screens with user and session management out of the box, as well as a profile page and a screen to view session token details.

1. The corbado_auth Package

To better understand the architecture of the corbado_auth package, we look at the relationship of the corbado_auth package and the passkeys package.

corbado_auth is Corbados full-fledged authentication SDK which utilizes the passkeys package for passkey authentication. While any passkey backend can theoretically be plugged into the passkeys package, the corbado_auth package plugs in Corbado's passkeys backend, so no additional infrastructure is needed.

For a more detailed overview of the passkeys package, and further elaboration on the role of passkey backend providers, please have a look at the overview of the first Flutter blog post.

2. Configure Corbado project

Go to the Corbado developer panel and create a new account and project. In the project setup wizard, begin by selecting an appropriate name for your project. For the product selection, opt for "Corbado Complete". Subsequently, specify your technology stack and select "DEV along with "Corbado session management" options. Afterwards, youll get more foundational setup guidance. The subsequent sections of this article will delve into the integration part.

Now, some platform-specific settings have to be made. Navigate to the Native Apps settings and add a new Android / iOS app as shown in the next steps. If you dont know some of those settings yet, come back to this section once you got your app running.

2.1. Add Android app

Set up an Android app by clicking "Add new". You will need your package name (e.g. com.corbado.passkeys.example) and your SHA-256 fingerprint (e.g.54:4C:94:2C:E9:...).

The package name of your app is defined in /android/app/build.gradle (applicationId). Its default value for the example app is com.corbado.passkeys.example.

Create Android App Passkeys

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

Keytool

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

2.2. Add iOS app

2.2.1. Create an iOS app and configure the example in XCode

We 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.corbado.passkeys). 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.

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 the example in Xcode now by opening /packages/passkeys/passkeys/example/ios. In "Runner -> Signing & Capabilities" enter your Application Identifier Prefix and your Bundle Identifier.

2.2.2. Set up iOS Application Identifier Prefix and Bundle Identifier

Set up an iOS app by clicking "Add New". You will need your Application Identifier Prefix and your Bundle Identifier that we set up in step 2.2.1.

Create iOS App Passkeys

Afterwards, the Corbado relying party server will host an apple-app-site- association file at: https://{CORBADO_PROJECT_ID}.frontendapi.corbado.io/.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.

2.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:{CORBADO_PROJECT_ID}.frontendapi.corbado.io (no protocol should be added). 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 your own Corbado project ID and a different Bundle Identifier).

Apple Signing Capabilities

3. Set Up Your Flutter Application

In the following, we explain step-by-step how to implement a basic Flutter app. If you want to see the final version of the example, please check the GitHub repo.

We start by creating our project with:

flutter create passkeys_demo_flutter -t app --platforms android,ios,web

Next, we install all required packages:

flutter pub add corbado_auth get_it go_router json_editor url_launcher flutter_hooks riverpod flutter_riverpod hooks_riverpod

3.1 Set Up Business Logic

In the updated approach starting 3.0.0, we no longer encapsulate our authentication logic inside a dedicated AuthService class. Instead, we rely on Corbado’s built-in blocks to handle sign-up, login, and passkey flows directly in the UI.

However, we still need to set up a few Riverpod providers that make the Corbado SDK and its data streams available throughout our app. Create a file named auth_provider.dart with the following code:

auth_provider.dart
import 'package:corbado_auth/corbado_auth.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // Corbado SDK provider. This will be used by other providers to // e.g. expose user state. final corbadoProvider = Provider<CorbadoAuth>( (ref) => throw UnimplementedError("no instance of corbadoAuth"), ); // Make the user available throughout the app. final userProvider = StreamProvider<User?>((ref) async* { final corbado = ref.watch(corbadoProvider); await for (final value in corbado.userChanges) { yield value; } }); // Make the auth state available throughout the app. final authStateProvider = StreamProvider<AuthState>((ref) async* { final corbado = ref.watch(corbadoProvider); await for (final value in corbado.authStateChanges) { yield value; } });
  • corbadoProvider: Initializes a CorbadoAuth instance. In your production app, this might read configuration values (like projectId) from environment variables or flavors.
  • userProvider: Emits changes to the currently authenticated user as a Stream. Any widget that depends on this provider will rebuild whenever the User? changes.
  • authStateProvider: Similarly, emits changes to the user’s authentication state (AuthState), allowing you to react to whether the user is signed in, signed out, or in the middle of a passkey setup flow.

This replaces the need for a standalone AuthService. Instead, Corbado handles registration, login, and passkey interactions automatically within its blocks (we’ll see how in the UI code).

Now, we can set up our app’s routing in router.dart. Here, we create pages for authentication and the profile. We also define a redirect function that enforces whether the user is signed in or out.

router.dart
import 'package:corbado_auth/corbado_auth.dart'; import 'package:my_app/auth_provider.dart'; import 'package:my_app/pages/auth_page.dart'; import 'package:my_app/pages/profile_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class Routes { static const auth = '/auth'; static const profile = '/profile'; } GoRoute _defaultTransitionGoRoute({ required String path, required Widget Function(BuildContext, GoRouterState) builder, }) { return GoRoute( path: path, pageBuilder: (context, state) => _customPageBuilder(builder, context, state), ); } Page<dynamic> _customPageBuilder( Widget Function(BuildContext, GoRouterState) builder, BuildContext context, GoRouterState state, ) { return CustomTransitionPage<void>( key: state.pageKey, transitionDuration: const Duration(milliseconds: 150), child: builder(context, state), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: CurveTween(curve: Curves.easeIn).animate(animation), child: child, ); }, ); } final routerProvider = Provider<GoRouter>((ref) { final authState = ref.watch(authStateProvider); return GoRouter( initialLocation: Routes.auth, routes: [ _defaultTransitionGoRoute( path: Routes.auth, builder: (context, state) => AuthPage(), ), _defaultTransitionGoRoute( path: Routes.profile, builder: (context, state) => ProfilePage(), ), ], redirect: (BuildContext context, GoRouterState state) { // If user is not logged in -> should be on /auth // If user is logged in -> should be on /profile final onAuthRoute = state.fullPath == Routes.auth; if (authState.value == null) { return null; // Still loading } switch (authState.value!) { case AuthState.None: if (!onAuthRoute) { return Routes.auth; } break; case AuthState.SignedIn: if (onAuthRoute) { return Routes.profile; } break; } return null; }, ); });

Lastly, we also need to set up our main.dart file to run our app.

Your environment will need to contain the following values:

  • CORBADO_PROJECT_ID: Retrieved from the Corbado developer panel under Settings > General > Project ID
  • CORBADO_CUSTOM_DOMAIN (optional): Your custom domain which can be set here under Create CNAME DNS address and will need to be specified in your environment if it differs from the default value.

Once corbadoAuth is initialized, we can override the unimplemented corbadoProvider with it in our main.dart file. Until corbadoAuth is initialized, we'll render a loading page. The app will then simply render our router.

main.dart
import 'package:corbado_auth/corbado_auth.dart'; import 'package:my_app/auth_provider.dart'; import 'package:my_app/router.dart'; import 'package:my_app/pages/loading_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // First, show a loading page while we await initialization. runApp(const LoadingPage()); // Initialize the CorbadoAuth SDK with your projectId & (optional) customDomain. final projectId = const String.fromEnvironment('CORBADO_PROJECT_ID'); final customDomain = const String.fromEnvironment('CORBADO_CUSTOM_DOMAIN'); final corbadoAuth = CorbadoAuth(); await corbadoAuth.init( projectId: projectId, customDomain: customDomain, ); // Override the corbadoProvider with our fully initialized CorbadoAuth instance. runApp( ProviderScope( overrides: [ corbadoProvider.overrideWithValue(corbadoAuth), ], child: MyApp(), ), ); } class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); return MaterialApp.router( routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, routeInformationProvider: router.routeInformationProvider, theme: ThemeData( useMaterial3: false, colorScheme: const ColorScheme( brightness: Brightness.light, primary: Color(0xFF1953ff), onPrimary: Colors.white, secondary: Colors.white, onSecondary: Color(0xFF1953ff), error: Colors.redAccent, onError: Colors.white, background: Color(0xFF1953ff), onBackground: Colors.white, surface: Color(0xFF1953ff), onSurface: Color(0xFF1953ff), ), ), ); } }

3.2 Setup the UI

All UI files can be found here under screens and pages. Explaining every UI component is out of the scope of this blog article.

in the newer 3.0.0 version and later, Instead of manually calling signUpWithPasskey or loginWithPasskey, all passkey logic is handled by Corbado’s UI Blocks. We simply implement screens for each block type (e.g. signup, login, verify email, etc.) and wire them up in an “auth container” page.

pages/auth_page.dart
import 'package:corbado_auth/corbado_auth.dart'; import 'package:my_app/auth_provider.dart'; import 'package:my_app/screens/email_verify_otp.dart'; import 'package:my_app/screens/login_init.dart'; import 'package:my_app/screens/passkey_append.dart'; import 'package:my_app/screens/passkey_verify.dart'; import 'package:my_app/screens/signup_init.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class AuthPage extends HookConsumerWidget { const AuthPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final corbadoAuth = ref.watch(corbadoProvider); return Scaffold( appBar: AppBar(title: const Text('Corbado authentication')), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 500), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: CorbadoAuthComponent( corbadoAuth: corbadoAuth, components: CorbadoScreens( signupInit: SignupInitScreen.new, loginInit: LoginInitScreen.new, emailVerifyOtp: EmailVerifyOtpScreen.new, passkeyAppend: PasskeyAppendScreen.new, passkeyVerify: PasskeyVerifyScreen.new, ), ), ), ), ), ); } }

CorbadoAuthComponent is the container widget that orchestrates the display of each block.
CorbadoScreens is a set of 5 optional/possible screens that you can provide:

  • signupInit: Collects the user’s email and starts the sign-up flow.
  • loginInit: Collects the user’s email and starts the login flow.
  • emailVerifyOtp: Asks the user to enter a 6-digit email OTP for verification.
  • passkeyAppend: Creates a new passkey for secure, passwordless authentication.
  • passkeyVerify: Verifies an existing passkey for logging in.

Corbado internally decides which screen to present next depending on the user’s current state or any fallback paths you configure.

Let's now take a look at the ProfilePage which is an authenticated-only view that demonstrates how to retrieve user information and trigger a sign-out flow via CorbadoAuth.

profile_page.dart
import 'package:corbado_auth_example/auth_provider.dart'; import 'package:corbado_auth_example/widgets/filled_text_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class ProfilePage extends ConsumerWidget { ProfilePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userProvider); final corbado = ref.watch(corbadoProvider); return Scaffold( appBar: AppBar(title: const Text('Corbado authentication')), body: Center( child: Container( constraints: const BoxConstraints(maxWidth: 500), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Welcome', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 10), Text( user.value?.email ?? '', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 10), const Text( 'You are currently logged in. You have a JWT token that you can use to make calls to your backend.', style: TextStyle( fontSize: 20, ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 50, child: FilledTextButton( onTap: corbado.signOut, content: 'sign out', ), ), ], ), ), ), ), ); } }
  • We use ref.watch(userProvider) to read the current user’s email address.
  • We also watch corbadoProvider to get a reference to the CorbadoAuth instance, which offers a convenient signOut method.
  • FilledTextButton is a custom widget (you can replace it with a standard Flutter ElevatedButton) that initiates the sign-out flow when pressed.
  • Once the user signs out, authStateProvider will emit the updated authentication state, which triggers our GoRouter redirect logic to return the user to the AuthPage.

With these pieces in place, your Flutter app fully delegates sign-up, login, and passkey creation to Corbado’s blocks. You no longer need an external AuthService(as in older version): CorbadoAuth manages passkey generation and fallback logic for you, and Riverpod ensures your UI responds instantly to authentication state changes.

Feel free to customize the look and feel of each block screen. As you can see, you only need to:

  1. Set up the Corbado provider (auth_provider.dart)
  2. Initialize Corbado in main.dart
  3. Configure your routing to show an auth page or profile page depending on authStateProvider.
  4. Implement block screens (e.g. SignupInitBlock) to gather user inputs and call the appropriate methods (like block.submitSignupInit() or block.passkeyVerify()).

4. (Optional) Bind passkeys to specific domain

Each passkey is bound to a certain domain, the so-called relying party ID.

If you use the Corbado default settings, passkeys are bound to your project-specific Corbado subdomain at https://<Corbado project ID>.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 a configuration file on your domain. For Corbado default settings, you can find these files here (it can take up to 5 minutes after you added the native app information in the Corbado developer panel until the files are deployed):

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

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.

Passkey Android App Linking Assistant

Passkey Android App Linking Assistant

Passkey Android App Linking Assistant

If youre using Corbado as passkey infrastructure provider, but want to bind the passkey to a specific domain (other than the default one), you need to make sure to also adapt the relying party ID in the Corbado developer panel under Settings > General > URLs to be the same as your newly selected, specific domain.

5. Run the example app

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:

flutter run lib/main.dart dart-define=CORBADO_PROJECT_ID=<your-corbado- project-id>

If you run the example, please make sure to either:

  • set the CORBADO_PROJECT_ID environment variable to your Corbado project ID
  • replace const String.fromEnvironment (CORBADO_PROJECT_ID) directly in the example with your Corbado project ID

Note: You can't use passkeys when running your flutter app on a physical iOS device, so rather test on a virtual iOS device or a virtual / physical Android device.

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):

Keytool

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 a virtual Android device, and it says that you can't create a passkey, you have to properly set up a screen lock or biometrics on the device. To do so, 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 Troubleshooting Android

7. Conclusion

Congrats! You have successfully set up your authentication and user management for your Flutter app with Corbado leveraging the corbado_auth package.

Besides the current focus on passkeys as authentication method, we plan to add more features to the corbado_auth package that can help for more complex apps. Examples of these feature are:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

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.