flutter-passkey-authenticationPasskeys Implementation

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: August 12, 2024


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

First, well set up an AuthService class to encapsulate all authentication logic in lib/auth_service.dart

auth_service.dart
import 'dart:async'; import 'package:corbado_auth/corbado_auth.dart'; import 'package:flutter/material.dart'; // This service encapsulates all authentication functionality. // It makes use of the corbado SDK through CorbadoAuth. class AuthService { final CorbadoAuth _auth; AuthService(this._auth); Future<String?> register({required String email}) async { try { await _auth.signUpWithPasskey(email: email, fullName: email); return null; } on PasskeyAuthCancelledException { return null; } on ValidationException catch (e) { return 'validation error: ${e.toString()}'; } on UnexpectedBackendException catch (e) { debugPrint(e.toString()); return 'An unexpected error happened during registration. Please try again later.'; } catch (e) { return e.toString(); } } Future<String?> signIn({required String email}) async { try { await _auth.loginWithPasskey(email: email); return null; } on PasskeyAuthCancelledException { return null; } on NoPasskeyForDeviceException { return 'No passkey has been setup on this device for ${email}.'; } on UnknownUserException { return 'Incorrect user identifier. Please check your email.'; } catch (e) { return e.toString(); } } Future<void> signInWithAutocomplete() async { return await _auth.autocompletedLoginWithPasskey(); } Future<void> signOut() { return _auth.signOut(); } }

Next, well set up all providers to be used across our app. This will allow us to retrieve authentication related information and react to changes.

auth_provider.dart
import 'package:corbado_auth/corbado_auth.dart'; import 'package:passkeys_demo_flutter/auth_service.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 authentication service available throughout the app. final authServiceProvider = Provider<AuthService>((ref) { final corbado = ref.watch(corbadoProvider); return AuthService(corbado); }); // 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((ref) async* { final corbado = ref.watch(corbadoProvider); await for (final value in corbado.authStateChanges) { yield value; } });

The corbadoProvider will be overridden once running our app inside the main.dart file.

Now, we can set up our apps routing in the lib/router.dart file. Here you can already see which pages well create. The router also implements redirecting when trying to access pages which require authentication and page transitions.

router.dart
import 'package:corbado_auth/corbado_auth.dart'; import 'package:passkeys_demo_flutter/auth_provider.dart'; import 'package:passkeys_demo_flutter/pages/profile_page.dart'; import 'package:passkeys_demo_flutter/pages/sign_in_page.dart'; import 'package:passkeys_demo_flutter/pages/sign_up_page.dart'; import 'package:passkeys_demo_flutter/pages/tokendetails_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 signUp = '/sign-up'; static const signIn = '/sign-in'; static const profile = '/profile'; static const tokenDetails = '/tokendetails'; } 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.signUp, routes: [ _defaultTransitionGoRoute( path: Routes.signUp, builder: (context, state) => SignUpPage(), ), _defaultTransitionGoRoute( path: Routes.signIn, builder: (context, state) => SignInPage(), ), _defaultTransitionGoRoute( path: Routes.profile, builder: (context, state) => ProfilePage(), ), _defaultTransitionGoRoute( path: Routes.tokenDetails, builder: (context, state) => TokenDetailsPage(), ), ], redirect: (BuildContext context, GoRouterState state) { final onLoggedOutRoutes = [ Routes.signIn, Routes.signUp, ].contains(state.fullPath); if (authState.value == null) { return null; } switch (authState.value!) { case AuthState.None: // if the user is not logged in but currently on a page that should // only be visible for logged in users => redirect to signIn page. if (!onLoggedOutRoutes) { return Routes.signIn; } break; case AuthState.SignedIn: // if the user is logged in but currently on a page that should // only be visible for logged out users => redirect to profile page. if (onLoggedOutRoutes) { return Routes.profile; } break; case AuthState.AskForPasskeyAppend: // we are not handling passkey append in this example (yet) return Routes.signIn; } 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:passkeys_demo_flutter/auth_provider.dart'; import 'package:passkeys_demo_flutter/pages/loading_page.dart'; import 'package:passkeys_demo_flutter/router.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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 projectId = const String.fromEnvironment('CORBADO_PROJECT_ID'); final customDomain = const String.fromEnvironment('CORBADO_CUSTOM_DOMAIN'); final corbadoAuth = CorbadoAuth(); await corbadoAuth.init(projectId, customDomain: customDomain); // Finally we override the providers that needed initialization. // Now the real app can be loaded. 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: 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 widgets and pages. Explaining every UI component is out of the scope of this blog article, but we will demonstrate how to use the values received from our providers by taking a look at our profile page found under lib/pages/profile_page.dart.

profile_page.dart
import 'package:passkeys_demo_flutter/auth_provider.dart'; import 'package:passkeys_demo_flutter/pages/base_page.dart'; import 'package:passkeys_demo_flutter/router.dart'; import 'package:passkeys_demo_flutter/widgets/filled_text_button.dart'; import 'package:passkeys_demo_flutter/widgets/outlined_text_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class ProfilePage extends ConsumerWidget { ProfilePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final authService = ref.watch(authServiceProvider); final user = ref.watch(userProvider); return BasePage( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: const Text( 'Welcome', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Text( user.value?.username ?? '', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: 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, ), ), ), SizedBox(height: 10), SizedBox( width: double.infinity, height: 50, child: FilledTextButton( onTap: () => context.push(Routes.tokenDetails), content: 'token details', )), SizedBox(height: 10), SizedBox( width: double.infinity, height: 50, child: OutlinedTextButton( onTap: () => authService.signOut(), content: 'sign out', ), ), ])); } }

In our widgets build function we watch the user value and our authService, which will update whenever our providers value changes. We can now access user information and use our authService for logout functionality. Youll now also need to add all other widgets and pages to your project. Feel free to adjust the UI however you like.

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:

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