diff --git a/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx b/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx index db387068b51..ad11ad53020 100644 --- a/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx +++ b/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx @@ -10,19 +10,19 @@ date: '2023-05-04' toc_depth: 2 --- -Multi-factor authentication or MFA is an essential part of security for any kind of app. +Multi-factor authentication or MFA is an essential part of security for any kind of app. -We will take a look at an example app where a user has to sign in via MFA in order to view the contents of the app to demonstrate how easy it is to get started with MFA on Flutter. +We will take a look at an example app where a user has to sign in via MFA in order to view the contents of the app to demonstrate how easy it is to get started with MFA on Flutter. ## What is Multi-Factor Authentication? Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), is an additional security layer on top of traditional login methods such as email and password login. -There are several forms of MFA, such as with an SMS or through using an authenticator app such as Google Authenticator. It is considered a best practice to use MFA whenever possible because it protects users against weak passwords or compromised social accounts. +There are several forms of MFA, such as with an SMS or through using an authenticator app such as Google Authenticator. It is considered a best practice to use MFA whenever possible because it protects users against weak passwords or compromised social accounts. ## Why Multi-Factor Authentication matters for Flutter apps -In the context of Flutter apps, MFA is important because it helps protect sensitive user data and prevent unauthorized access to user accounts. By requiring users to provide an additional factor, MFA adds an extra layer of security that makes it harder for attackers to gain access to user accounts. +In the context of Flutter apps, MFA is important because it helps protect sensitive user data and prevent unauthorized access to user accounts. By requiring users to provide an additional factor, MFA adds an extra layer of security that makes it harder for attackers to gain access to user accounts. Given how Flutter is widely used MFA might be a requirement rather than a nice-to-have. Implementing MFA in a Flutter app can improve overall security and give users peace of mind knowing that their data is better protected. @@ -42,13 +42,13 @@ You can find the complete code created in this article [here](https://github.com ### Step 1: Setup the scenes -Let’s start with the `flutter create` command. +Let’s start with the `flutter create` command. ```bash flutter create mfa_app ``` -Also, if you do not have a Supabase project yet, create one by heading to [database.new](https://dstabase.new). Within a few minutes, you will have a new Supabase project. +Also, if you do not have a Supabase project yet, create one by heading to [database.new](https://dstabase.new). Within a few minutes, you will have a new Supabase project. ### Step 2: Add the dependencies @@ -58,7 +58,7 @@ Install the [supabase_flutter](https://pub.dev/packages/supabase_flutter) packag dart pub add supabase_flutter ``` -Then update your `lib/main.dart` file to initialize Supabase in the main function. You should be able to find your Supabase URL and AnonKey from the `settings -> api` section of your dashboard. We will also extract the `SupabaseClient` for easy access to our Supabase instance. +Then update your `lib/main.dart` file to initialize Supabase in the main function. You should be able to find your Supabase URL and AnonKey from the `settings -> api` section of your dashboard. We will also extract the `SupabaseClient` for easy access to our Supabase instance. ```dart import 'package:flutter/material.dart'; @@ -76,13 +76,13 @@ void main() async { final supabase = Supabase.instance.client; ``` -Also, add [go_router](https://pub.dev/packages/go_router) to handle our routing and redirects. +Also, add [go_router](https://pub.dev/packages/go_router) to handle our routing and redirects. ```dart dart pub add go_router ``` -We will set up the routes towards the end when we have created all the pages we need. With this, we are ready to jump into creating the app. +We will set up the routes towards the end when we have created all the pages we need. With this, we are ready to jump into creating the app. Also, if we want to support iOS and Android, we need to set up deep links so that a session can be obtained upon clicking on the confirmation link sent to the user’s email address. @@ -117,7 +117,7 @@ For iOS, open `ios/Runner/info.plist` file and add the following deep link confi ``` -For Android, open `android/app/src/main/AndroidManifest.xml` file and add the following deep link configuration. +For Android, open `android/app/src/main/AndroidManifest.xml` file and add the following deep link configuration. ```xml @@ -145,7 +145,7 @@ For Android, open `android/app/src/main/AndroidManifest.xml` file and add the f After, we will add the deep link as one of the redirect URLs in our Supabase dashboard. -Go to `Authentication > URL Configuration` and add `mfa-app://callback/*` as a redirect URL. Make sure you don’t add any extra slashes or anything because if you do, deep linking will not work properly. +Go to `Authentication > URL Configuration` and add `mfa-app://callback/*` as a redirect URL. Make sure you don’t add any extra slashes or anything because if you do, deep linking will not work properly. ![Supabase Dashboard](/images/blog/flutter-mfa/supabase-dashboard.png) @@ -161,7 +161,7 @@ That is all the dependencies that we need. Let’s dive into coding! ![Register flow](/images/blog/flutter-mfa/register.png) -Let’s first create a signup flow. Again, the user will register with the app using email and password, and after confirming their email address, they will enroll in MFA using an authenticator app. +Let’s first create a signup flow. Again, the user will register with the app using email and password, and after confirming their email address, they will enroll in MFA using an authenticator app. The register page contains a form with an email and password field for the user to create a new account. We are just calling the [.signUp()](https://supabase.com/docs/reference/dart/auth-signup) method with it. As you can see in the code below at `emailRedirectTo` option of the `.signUp()`method, upon clicking on the confirmation link sent to the user, they will be taken to MFA enrollment page, which we will implement later. @@ -243,7 +243,7 @@ class _RegisterPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unexpected error occured'))); + const SnackBar(content: Text('Unexpected error occurred'))); } if (mounted) { setState(() { @@ -407,7 +407,7 @@ class _MFAEnrollPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Unexpected error occured'))); + content: Text('Unexpected error occurred'))); } }, ), @@ -424,7 +424,7 @@ class _MFAEnrollPageState extends State { ![Login flow](/images/blog/flutter-mfa/login.png) -Now that we have created a registration flow, we can get to the login flow for returning existing users. Again, the login page has nothing fancy going. We are just collecting the user’s email and password, and calling the good old [.signInWithPassword()](https://supabase.com/docs/reference/dart/auth-signinwithpassword) method. Upon signing in, the user will be taken to a verify page where the user will then enter their verification code from their authenticator app. +Now that we have created a registration flow, we can get to the login flow for returning existing users. Again, the login page has nothing fancy going. We are just collecting the user’s email and password, and calling the good old [.signInWithPassword()](https://supabase.com/docs/reference/dart/auth-signinwithpassword) method. Upon signing in, the user will be taken to a verify page where the user will then enter their verification code from their authenticator app. Create `lib/pages/auth/login_page.dart` and add the following. @@ -494,7 +494,7 @@ class _LoginPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unexpected error occured'))); + const SnackBar(content: Text('Unexpected error occurred'))); } }, child: const Text('Login'), @@ -506,7 +506,7 @@ class _LoginPageState extends State { } ``` -Once a returning user logs in, they are taken to the verification page where they are asked to enter the TOTP from their authenticator app. +Once a returning user logs in, they are taken to the verification page where they are asked to enter the TOTP from their authenticator app. This verification page has the same text field as the enrollment page, and upon entering the code, they are taken to the home page. @@ -592,7 +592,7 @@ class _MFAVerifyPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unexpected error occured'))); + const SnackBar(content: Text('Unexpected error occurred'))); } }, ), @@ -607,40 +607,40 @@ class _MFAVerifyPageState extends State { ![Home Page](/images/blog/flutter-mfa/home.png) -The home page is where the “secure” contents are displayed. We will create a dummy table with some dummy secure contents for demonstration purposes. +The home page is where the “secure” contents are displayed. We will create a dummy table with some dummy secure contents for demonstration purposes. -First, we create dummy content. Run the following SQL to create the table and add some content. +First, we create dummy content. Run the following SQL to create the table and add some content. ```sql -- Dummy table that contains "secure" information -create table if not exists public.private_posts ( +create table + if not exists public.private_posts ( id int generated by default as identity primary key, content text not null -); + ); -- Dmmy "secure" data -insert into public.private_posts - (content) +insert into + public.private_posts (content) values - ('Flutter is awesome!'), - ('Supabase is awesome!'), - ('Postgres is awesome!'); + ('Flutter is awesome!'), + ('Supabase is awesome!'), + ('Postgres is awesome!'); ``` -Now, we can add some [row security policy](https://supabase.com/docs/guides/auth/row-level-security) to lock those data down so that only users who have signed in using MFA can view them. +Now, we can add some [row security policy](https://supabase.com/docs/guides/auth/row-level-security) to lock those data down so that only users who have signed in using MFA can view them. Run the following SQL to secure our data from malicious users. ```sql -- Enable RLS for private_posts table -alter table public.private_posts enable row level security; +alter table + public.private_posts enable row level security; -- Create a policy that only allows read if they user has signed in via MFA -create policy "Users can view private_posts if they have signed in via MFA" - on public.private_posts - for select - to authenticated - using (auth.jwt()->>'aal' = 'aal2'); +create policy "Users can view private_posts if they have signed in via MFA" on public.private_posts for +select + to authenticated using (auth.jwt () - > > 'aal' = 'aal2'); ``` `aal` here stands for [Authenticator Assurance Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), and it will be `aal1` for users who have only signed in with 1 sign-in method, and `aal2` for users who have completed the MFA flow. Checking the `aal` inside RLS policy ensures that the data cannot be viewed by users unless they complete the entire MFA flow. @@ -649,7 +649,7 @@ The nice thing about RLS is that it gives us the flexibility to control how user Now that we have the secure data in our Supabase instance, all we need to do is to display them in the HomePage. We can simply query the table and display it using a `FutureBuilder`. -Create a `lib/pages/home_page.dart` file and add the following. +Create a `lib/pages/home_page.dart` file and add the following. ```dart import 'package:flutter/material.dart'; @@ -806,9 +806,9 @@ class ListMFAPage extends StatelessWidget { ### Step 6: Putting the pieces together with go_router -Now that we have all the pages, it’s time to put it all together with the help of [go_router](https://pub.dev/packages/go_router). +Now that we have all the pages, it’s time to put it all together with the help of [go_router](https://pub.dev/packages/go_router). -go_router, as you may know, is a routing package for Flutter, and its redirect feature is particularly helpful for implementing the complex requirement this app had. Particularly, we wanted to make sure that a user who has not yet set up MFA is redirected to the MFA setup page, and only users who have signed in land on the home page. +go_router, as you may know, is a routing package for Flutter, and its redirect feature is particularly helpful for implementing the complex requirement this app had. Particularly, we wanted to make sure that a user who has not yet set up MFA is redirected to the MFA setup page, and only users who have signed in land on the home page. Another helpful feature of go_router comes when using deep links, and it automatically redirects the users to the correct path of the deep link. Because of this, we can ensure that user lands on the MFA setup page upon confirming their email address. @@ -921,9 +921,9 @@ class MyApp extends StatelessWidget { ## Conclusions and future iterations -We looked at how to incorporate Multi-Factor Authentication into a Flutter app with a complete enrollment and verification flow for new and existing users. We saw how we are able to control how the users can interact with the data using their MFA status. +We looked at how to incorporate Multi-Factor Authentication into a Flutter app with a complete enrollment and verification flow for new and existing users. We saw how we are able to control how the users can interact with the data using their MFA status. -Another common use case is to make MFA optional and allow the user to opt-in whenever they are ready. Optionally enrolling in MFA will require some tweaks in the code, but might be a fun one to try out. +Another common use case is to make MFA optional and allow the user to opt-in whenever they are ready. Optionally enrolling in MFA will require some tweaks in the code, but might be a fun one to try out. ## More Flutter content diff --git a/examples/auth/flutter-mfa/lib/pages/auth/login_page.dart b/examples/auth/flutter-mfa/lib/pages/auth/login_page.dart index 63e31eab24f..19fecb7eab8 100644 --- a/examples/auth/flutter-mfa/lib/pages/auth/login_page.dart +++ b/examples/auth/flutter-mfa/lib/pages/auth/login_page.dart @@ -63,7 +63,7 @@ class _LoginPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unexpected error occured'))); + const SnackBar(content: Text('Unexpected error occurred'))); } }, child: const Text('Login'), diff --git a/examples/auth/flutter-mfa/lib/pages/auth/register_page.dart b/examples/auth/flutter-mfa/lib/pages/auth/register_page.dart index a73fb7386b0..09d05dee70d 100644 --- a/examples/auth/flutter-mfa/lib/pages/auth/register_page.dart +++ b/examples/auth/flutter-mfa/lib/pages/auth/register_page.dart @@ -72,7 +72,7 @@ class _RegisterPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unexpected error occured'))); + const SnackBar(content: Text('Unexpected error occurred'))); } if (mounted) { setState(() { diff --git a/examples/auth/flutter-mfa/lib/pages/mfa/enroll_page.dart b/examples/auth/flutter-mfa/lib/pages/mfa/enroll_page.dart index ddde7714868..15f129ccd2d 100644 --- a/examples/auth/flutter-mfa/lib/pages/mfa/enroll_page.dart +++ b/examples/auth/flutter-mfa/lib/pages/mfa/enroll_page.dart @@ -122,7 +122,7 @@ class _MFAEnrollPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Unexpected error occured'))); + content: Text('Unexpected error occurred'))); } }, ), diff --git a/examples/auth/flutter-mfa/lib/pages/mfa/verify_page.dart b/examples/auth/flutter-mfa/lib/pages/mfa/verify_page.dart index bb61f3aa74f..45b00b2eca5 100644 --- a/examples/auth/flutter-mfa/lib/pages/mfa/verify_page.dart +++ b/examples/auth/flutter-mfa/lib/pages/mfa/verify_page.dart @@ -77,7 +77,7 @@ class _MFAVerifyPageState extends State { .showSnackBar(SnackBar(content: Text(error.message))); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Unexpected error occured'))); + const SnackBar(content: Text('Unexpected error occurred'))); } }, ),