guides
Guide

Add Passwordless Authentication to Your Flutter App With Magic

Magic Staff · February 2, 2022
info

This guide is a guest post by Harshil Agrawal, Junior Developer Advocate n8n.io, as part of our Guest Author program.

Flutter is an open-source framework for building multi-platform applications from a single codebase. Flutter allows you to build apps for android, iOS, and the web, as well as desktop. Companies across the world are using Flutter to create beautiful apps.

Almost all applications require authentication. The traditional approach of authentication is email and password. However, this method is not secure, and history is your proof. The SDK provided by Magic allows you to integrate passwordless authentication using magic links. 

Magic uses Decentralized Identifier (DID) tokens in the authentication flow. Under the hood, Magic leverages the Ethereum blockchain and elliptic curve cryptography to generate verifiable proofs of identity and authorization. To learn more about DID tokens, refer to this documentation.

In this guide, you will learn to integrate the Magic SDK to implement authentication to your Flutter app. Your app will have a home screen, a login screen, and a logout screen.

#Prerequisites

  • Flutter: To install Flutter for your operating system follow the Install guide. The recent released version is Flutter 2.10

  • Magic Account: Create an account on Magic using this link.

#Quick Start

If you're familiar with Flutter and Magic and want to skip to the result, clone this Flutter Magic Sample.

Open the cloned repository in your code editor. In the main.dart file, replace YOUR_PUBLISHABLE_KEY, with your Magic Publishable Key.

Run the command flutter run to start the app.

If you want to learn in detail, follow the guide below.

#Creating Flutter App

In this step, you will create a new Flutter project. You will also add a login button to the home page.

To create a new Flutter app, run the command flutter create <APP_NAME>, where <APP_NAME> is the name of your application. Next, run the command cd <APP_NAME> to change the directory. Run the command flutter run to start your application.

The Flutter SDK generates a new project with example code. Open the project repository in your code editor (in this guide, I am using VS Code, you can also use Android Studio). Your application code will live in the lib directory. Diving deeper into the project structure is out of the scope of this article. Replace the example code in the /lib/main.dart file with the following code.

import 'package:flutter/material.dart';
 
void main() {
 runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Magic',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: const HomePage(),
   );
 }
}
 
class HomePage extends StatelessWidget {
 const HomePage({Key? key}) : super(key: key);
 
 
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       leading: IconButton(
         icon: const Icon(Icons.menu),
         onPressed: () {},
       ),
       title: const Text('Flutter Magic ✨'),
     ),
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           ElevatedButton(onPressed: () {}, child: Text('Login'))
         ],
       ),
     ),
   );
 }
}

Flutter allows you to build beautiful UI using UI libraries like Material UI and Cupertino. The first line of the code imports the Material UI package. The main() function is the starting point of your app. It calls the MyApp class. The MyApp class returns a material app widget where you’ve defined the app name, the theme color, and the home page.

The code for your home page. It returns a scaffold widget that contains the app bar and the body of the home page. Save the code, and run the command flutter run to start the app. If your app is already running, type r in the terminal to hot reload the app. Your app will look like the following image.

Click on the login button. You will notice that nothing happens. In the next step, you will learn to add a new page to your app and add in-app navigation.

#Create the Login Route and add navigation

In this step, you will create a Login page and add navigation that allows you to navigate from the home page to the login page and back.

note

In Flutter, screens and pages are called routes. The remainder of this guide refers to routes.

To add the Login route, create a new file login.dart under the lib directory. This file will contain the code for the Login route. Since this route will pass on data to the Magic SDK, you will create a Stateful widget.

Copy and paste the following code in the login.dart file.

import 'package:flutter/material.dart';
 
class LoginPage extends StatefulWidget {
 const LoginPage({Key? key}) : super(key: key);
 
 
 _LoginPageState createState() => _LoginPageState();
}
 
class _LoginPageState extends State<LoginPage> {
 final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
 final TextEditingController _emailController = TextEditingController();
 
 
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Login'),
     ),
     body: Column(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
         Form(
           key: _formKey,
           child: Column(
             children: [
               Padding(
                 padding: const EdgeInsets.symmetric(horizontal: 8.0),
                 child: TextFormField(
                   decoration: const InputDecoration(
                       hintText: 'Enter your email',
                       border: OutlineInputBorder()),
                   validator: (String? value) {
                     if (value == null || value.isEmpty) {
                       return 'Please enter an email address';
                     }
                     return null;
                   },
                   controller: _emailController,
                 ),
               ),
               Padding(
                 padding: const EdgeInsets.symmetric(vertical: 16.0),
                 child: ElevatedButton(
                   onPressed: () {
                     if (_formKey.currentState!.validate()) {
                       debugPrint('Email: ${_emailController.text}');
                       ScaffoldMessenger.of(context).showSnackBar(
                           const SnackBar(content: Text('Check you email')));
                     }
                   },
                   child: const Text('Login'),
                 ),
               )
             ],
           ),
         )
       ],
     ),
   );
 }
}

The Login route is also a Material widget, and hence you import the Material package. Getting into the details of Stateful widgets is out of the scope of this guide. If you’re interested in learning more about Stateful and Stateless widgets, refer to the documentation here.

You’re returning a Scaffold that contains an app bar and the body. The body of this route contains a text field with a validation function and a login button.

In the main.dart file, import the login route you created above and copy and paste the following code snippet inside the onPressed parameter of the ElevatedButton widget.

{
                 Navigator.push(
                   context,
                   MaterialPageRoute(builder: (context) => const LoginPage()),
                 );
               },

Your home route now has the following code.

import 'package:flutter/material.dart';
import 'package:flutter_magic_sample/login.dart';
 
void main() {
 runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Magic',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: const HomePage(),
   );
 }
}
 
class HomePage extends StatelessWidget {
 const HomePage({Key? key}) : super(key: key);
 
 
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       leading: IconButton(
         icon: const Icon(Icons.menu),
         onPressed: () {},
       ),
       title: const Text('Flutter Magic ✨'),
     ),
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           ElevatedButton(
               onPressed: () {
                 Navigator.push(
                   context,
                   MaterialPageRoute(builder: (context) => const LoginPage()),
                 );
               },
               child: const Text('Login'))
         ],
       ),
     ),
   );
 }
}

In your terminal, press r to hot reload the app. In the emulator, click on the Login button. You will be navigated to the Login route where you can enter the email address.

If you enter the email address and click on the Login button, you will notice that a snackbar gets displayed, but nothing happens. In the next step, you will learn to implement authentication with Magic.

#Implement Passwordless Auth with Magic

In this step, you will import the Magic SDK and implement passwordless authentication for your app.

Magic provides an SDK for Flutter that enables you to add passwordless authentication in your app. To add the SDK to your app, open the pubspec.yml file and the following under dependencies.

magic_sdk: ^0.4.0

Use CTRL+C in your terminal to stop the development server. Run the command flutter pub get to install the Magic SDK.

Paste the following import statement below the material import statement in your main.dart file to import the Magic SDK. 

import 'package:flutter/material.dart';
import 'package:magic_sdk/magic_sdk.dart';
 
import 'package:flutter_magic_sample/login.dart';

You now have to initiate Magic with your API key. Go to your Magic dashboard and copy the PUBLISHABLE API KEY present in the Install Magic section. 

In your main.dart file, initiate Magic inside the main() function using the following code. Replace YOUR_PUBLISHABLE_KEY with the publishable API key you copied earlier.

Magic.instance = Magic("YOUR_PUBLISHABLE_KEY");

Your main() function should be as follow.

void main() {
 runApp(const MyApp());
 Magic.instance = Magic("YOUR_PUBLISHABLE_KEY");
}

Now that you have initiated Magic, you will be able to use various methods provided by the SDK.

In your main.dart file, return the MaterialApp widget. This widget returns a Stack widget with another MaterialApp widget as its child. Add Magic.instance.relayer to the children of Stack to ensure the best performance. Your MyApp class will be as follows.

class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 
 Widget build(BuildContext context) {
   return MaterialApp(
       home: Stack(children: [
     MaterialApp(
       title: 'Flutter Magic',
       theme: ThemeData(
         primarySwatch: Colors.blue,
       ),
       home: const HomePage(),
     ),
     Magic.instance.relayer
   ]));
 }
}

In your login.dart file, add the following import statement to import the Magic SDK.

import 'package:magic_sdk/magic_sdk.dart';

In the _LoginPageState class, initialize the Magic instance and assign it to magic.

final magic = Magic.instance;

You also need to create a login function that gets called when the user clicks on the Login button. This function will be asynchronous and will contain the email parameter. Paste the following function in the _LoginPageState class.

Future loginFunction({required String email}) async {
   try {
     await magic.auth.loginWithMagicLink(email: _emailController.text);
   } catch (e) {
     debugPrint('Error: $e');
   }
 }

The function contains a try-catch block. You’re using the loginWithMagicLink function and passing the email address that the user provides. This function will register the user if they’re not already registered. It will also send an email with the login link. If the try block fails, the error gets printed in the terminal.

Now that you have the function that handles login, you need to call this function when the Login button gets pressed. In the ElevatedButton widget, inside the onPressed function, you want to call the login function only if the validation is true. Hence, call the login function and pass the email inside the if block. Your onPressed function should be as follows:

onPressed: () {
     if (_formKey.currentState!.validate()) {
       loginFunction(
         email: _emailController.text,
       );
       ScaffoldMessenger.of(context).showSnackBar(
         const SnackBar(content: Text('Check you email'))
       );
     }
},

Enter your email address and click on the login button. On successful login, you will see a message in the app. You will also receive a login link on the email address you entered.

note

You can hide the message UI in your app. Set showUI to false in the loginWithMagicLink function.

await m.auth.loginWithMagicLink({ email: 'hello@example.com', showUI: false });

In this step, you implemented passwordless login using the Magic SDK. However, once the user is signed in, they will still see the login route. In the next step, you will add a new route that will display a success message and a log out button.

#Implement Log out

You will now add a logout button that will allow the user to log out of the app. Create a new file logout.dart inside the lib folder and paste the following code in the logout.dart file.

import 'package:flutter/material.dart';
import 'package:magic_sdk/magic_sdk.dart';
 
class LogoutPage extends StatefulWidget {
 const LogoutPage({Key? key}) : super(key: key);
 
 
 State<LogoutPage> createState() => _LogoutPageState();
}
 
class _LogoutPageState extends State<LogoutPage> {
 final magic = Magic.instance;
 
 Future logout() async {
   await magic.user.logout();
   Navigator.pop(context);
 }
 
 
 Widget build(BuildContext context) {
   return Scaffold(
       appBar: AppBar(
         title: const Text('Logout'),
       ),
       body: Center(
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             const Text('Successfully Logged In'),
             ElevatedButton(
               child: const Text('Logout'),
               onPressed: logout,
             ),
           ],
         ),
       ));
 }
}

You import the Material package and the Magic SDK. Since interactivity gets added to the route, you created a stateful widget. You initialized the Magic instance and created an asynchronous function called logout. This function gets called when the user clicks on the Logout button.

The logout function uses the logout method provided by the Magic SDK. When the user logs out, the app navigates the user back to the login route.

In the login.dart file, update the loginFunction to navigate the user to the logout route. Your loginFunction should be as follow.

Future loginFunction({required String email}) async {
   try {
     await magic.auth.loginWithMagicLink(email: _emailController.text);
     Navigator.push(
         context, MaterialPageRoute(builder: (context) => const LogoutPage()));
   } catch (e) {
     debugPrint('Error: $e');
   }
 }

In your terminal, hot restart the app using the command shift+R. Try logging in again with an email. Once logged in, you will get navigated to the Logout route. Press on the logout button, and you get logged out of the app and navigated back to the login screen.

note

From the logout route, if you click on the back button, you will be navigated back to the login route still logged in. Teaching how to add protected routes is out of the scope of this guide.

#Summary

In this guide, you learned to use the Magic SDK to add passwordless authentication to your Flutter application. You created a Flutter app from scratch, added navigation, and implemented the login and logout functionality.

#What’s next?

Now that you have learned to add authentication to a Flutter app, go ahead and add authentication to your project. To dive deeper, try creating a profile route. This profile route can display users' information and also let the user update their email addresses.

Also, try adding SMS as authentication. See this doc for more info.

onPressed: () async {
  var token = await magic.auth.loginWithSMS(phoneNumber: textController.text);
  debugPrint('token, $token');
  },

If you run into an error or need more help, reach out to me on Twitter. If this guide helped you get started, share it with others. I would love to see what you build :)

Let's make some magic!