Add Passwordless Authentication to Your Flutter App With Magic
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.
01import 'package:flutter/material.dart';
02
03void main() {
04 runApp(const MyApp());
05}
06
07class MyApp extends StatelessWidget {
08 const MyApp({Key? key}) : super(key: key);
09
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 title: 'Flutter Magic',
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: const HomePage(),
17 );
18 }
19}
20
21class HomePage extends StatelessWidget {
22 const HomePage({Key? key}) : super(key: key);
23
24
25 Widget build(BuildContext context) {
26 return Scaffold(
27 appBar: AppBar(
28 leading: IconButton(
29 icon: const Icon(Icons.menu),
30 onPressed: () {},
31 ),
32 title: const Text('Flutter Magic ✨'),
33 ),
34 body: Center(
35 child: Column(
36 mainAxisAlignment: MainAxisAlignment.center,
37 children: <Widget>[
38 ElevatedButton(onPressed: () {}, child: Text('Login'))
39 ],
40 ),
41 ),
42 );
43 }
44}
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.
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.
01import 'package:flutter/material.dart';
02
03class LoginPage extends StatefulWidget {
04 const LoginPage({Key? key}) : super(key: key);
05
06
07 _LoginPageState createState() => _LoginPageState();
08}
09
10class _LoginPageState extends State<LoginPage> {
11 final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
12 final TextEditingController _emailController = TextEditingController();
13
14
15 Widget build(BuildContext context) {
16 return Scaffold(
17 appBar: AppBar(
18 title: const Text('Login'),
19 ),
20 body: Column(
21 mainAxisAlignment: MainAxisAlignment.center,
22 children: [
23 Form(
24 key: _formKey,
25 child: Column(
26 children: [
27 Padding(
28 padding: const EdgeInsets.symmetric(horizontal: 8.0),
29 child: TextFormField(
30 decoration: const InputDecoration(
31 hintText: 'Enter your email',
32 border: OutlineInputBorder()),
33 validator: (String? value) {
34 if (value == null || value.isEmpty) {
35 return 'Please enter an email address';
36 }
37 return null;
38 },
39 controller: _emailController,
40 ),
41 ),
42 Padding(
43 padding: const EdgeInsets.symmetric(vertical: 16.0),
44 child: ElevatedButton(
45 onPressed: () {
46 if (_formKey.currentState!.validate()) {
47 debugPrint('Email: ${_emailController.text}');
48 ScaffoldMessenger.of(context).showSnackBar(
49 const SnackBar(content: Text('Check you email')));
50 }
51 },
52 child: const Text('Login'),
53 ),
54 )
55 ],
56 ),
57 )
58 ],
59 ),
60 );
61 }
62}
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.
01{
02 Navigator.push(
03 context,
04 MaterialPageRoute(builder: (context) => const LoginPage()),
05 );
06 },
Your home route now has the following code.
01import 'package:flutter/material.dart';
02import 'package:flutter_magic_sample/login.dart';
03
04void main() {
05 runApp(const MyApp());
06}
07
08class MyApp extends StatelessWidget {
09 const MyApp({Key? key}) : super(key: key);
10
11 Widget build(BuildContext context) {
12 return MaterialApp(
13 title: 'Flutter Magic',
14 theme: ThemeData(
15 primarySwatch: Colors.blue,
16 ),
17 home: const HomePage(),
18 );
19 }
20}
21
22class HomePage extends StatelessWidget {
23 const HomePage({Key? key}) : super(key: key);
24
25
26 Widget build(BuildContext context) {
27 return Scaffold(
28 appBar: AppBar(
29 leading: IconButton(
30 icon: const Icon(Icons.menu),
31 onPressed: () {},
32 ),
33 title: const Text('Flutter Magic ✨'),
34 ),
35 body: Center(
36 child: Column(
37 mainAxisAlignment: MainAxisAlignment.center,
38 children: <Widget>[
39 ElevatedButton(
40 onPressed: () {
41 Navigator.push(
42 context,
43 MaterialPageRoute(builder: (context) => const LoginPage()),
44 );
45 },
46 child: const Text('Login'))
47 ],
48 ),
49 ),
50 );
51 }
52}
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
.
01magic_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.
01import 'package:flutter/material.dart';
02import 'package:magic_sdk/magic_sdk.dart';
03
04import '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.
01Magic.instance = Magic("YOUR_PUBLISHABLE_KEY");
Your main()
function should be as follow.
01void main() {
02 runApp(const MyApp());
03 Magic.instance = Magic("YOUR_PUBLISHABLE_KEY");
04}
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.
01class MyApp extends StatelessWidget {
02 const MyApp({Key? key}) : super(key: key);
03
04 Widget build(BuildContext context) {
05 return MaterialApp(
06 home: Stack(children: [
07 MaterialApp(
08 title: 'Flutter Magic',
09 theme: ThemeData(
10 primarySwatch: Colors.blue,
11 ),
12 home: const HomePage(),
13 ),
14 Magic.instance.relayer
15 ]));
16 }
17}
In your login.dart
file, add the following import statement to import the Magic SDK.
01import 'package:magic_sdk/magic_sdk.dart';
In the _LoginPageState
class, initialize the Magic instance and assign it to magic.
01final 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.
01Future loginFunction({required String email}) async {
02 try {
03 await magic.auth.loginWithMagicLink(email: _emailController.text);
04 } catch (e) {
05 debugPrint('Error: $e');
06 }
07 }
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:
01onPressed: () {
02 if (_formKey.currentState!.validate()) {
03 loginFunction(
04 email: _emailController.text,
05 );
06 ScaffoldMessenger.of(context).showSnackBar(
07 const SnackBar(content: Text('Check you email'))
08 );
09 }
10},
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.
You can hide the message UI in your app. Set showUI
to false
in the loginWithMagicLink
function.
01await 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.
01import 'package:flutter/material.dart';
02import 'package:magic_sdk/magic_sdk.dart';
03
04class LogoutPage extends StatefulWidget {
05 const LogoutPage({Key? key}) : super(key: key);
06
07
08 State<LogoutPage> createState() => _LogoutPageState();
09}
10
11class _LogoutPageState extends State<LogoutPage> {
12 final magic = Magic.instance;
13
14 Future logout() async {
15 await magic.user.logout();
16 Navigator.pop(context);
17 }
18
19
20 Widget build(BuildContext context) {
21 return Scaffold(
22 appBar: AppBar(
23 title: const Text('Logout'),
24 ),
25 body: Center(
26 child: Column(
27 mainAxisAlignment: MainAxisAlignment.center,
28 children: [
29 const Text('Successfully Logged In'),
30 ElevatedButton(
31 child: const Text('Logout'),
32 onPressed: logout,
33 ),
34 ],
35 ),
36 ));
37 }
38}
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.
01Future loginFunction({required String email}) async {
02 try {
03 await magic.auth.loginWithMagicLink(email: _emailController.text);
04 Navigator.push(
05 context, MaterialPageRoute(builder: (context) => const LogoutPage()));
06 } catch (e) {
07 debugPrint('Error: $e');
08 }
09 }
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.
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.
01onPressed: () async {
02 var token = await magic.auth.loginWithSMS(phoneNumber: textController.text);
03 debugPrint('token, $token');
04 },
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 :)