DEV Community

Cover image for User Authentication in Flutter Apps with Serverpod: A Step-by-Step Guide
Godwin Mathias
Godwin Mathias

Posted on

User Authentication in Flutter Apps with Serverpod: A Step-by-Step Guide

Most enterprise software needs a method of authenticating its users. Depending on the type of User, it could be a programmer trying to access a resource through an Application Programming Interface (API), or an end-user directly performing a task through forms on their website or apps.

Serverpod is a backend framework that provides feature-rich authentication modules and methods for your app needs. It is suitable for building both simple and complex backends for Flutter apps. However, Serverpod can be complex to set up at first, and the folder structure may not be intuitive for beginners. This tutorial aims to help you get started with Serverpod and make the process as smooth as possible.

What To Expect in the Tutorial

This tutorial provides an overview of authentication in Serverpod. It also demonstrates authentication with Email and how to connect the backend (Serverpod) setup with your frontend (Flutter app). You should be able to apply the knowledge to other authentication factors

This tutorial assumes some prior knowledge of Serverpod and Flutter. You should check this tutorial on how to get started with Serverpod.


Serverpod's email auth screen.


Getting Started

Serverpod comes with built-in user management and authentication. You can either build your custom authentication method or use the serverpod_auth module. The module makes it easy to authenticate with email or social sign-ins - From the documentation.

You will need an understanding of Modules in Serverpod to be able to add the authentication method used in this tutorial.

You will be using the serverpod_auth module for this tutorial.

Clone this simple noteapp project from GitHub. The cloned project has some setup that was covered in this tutorial. You will be working with the project.

You could skip the Modules explanation part below if you have prior knowledge of how it works without missing anything.


What Are Modules in Serverpod

As stated in the documentation:

Serverpod is built around the concept of modules. A Serverpod module is similar to a Dart package but contains both server and client code and Flutter widgets. A module contains its namespace for endpoints and methods to minimize module conflicts.

This means modules in Serverpod are like your flutter packages that are reusable and publishable to pub.

Modules in Serverpod consist of two folders, the Client and the Server folder.

Execute the command below to generate a module called user_module to see its structure:

serverpod create --template module user_module
Enter fullscreen mode Exit fullscreen mode

You will get this folder structure:

├── user_module
│   ├── user_module_client
│   └── user_module_server
Enter fullscreen mode Exit fullscreen mode

How To Set up a Module

There are four (4) steps needed to fully add an existing module to your project.

Let's go through the steps with a demonstration.

Assuming your project is called app:

├── app
│   ├── app_client
│   ├── app_flutter
│   ├── app_server
Enter fullscreen mode Exit fullscreen mode

If you want to add the user_module, follow the steps below:

Step 1: Database Setup

Serverpod generates a set of database tables whenever you create a new Server or Module project with the framework. These tables must be added to your database source before Serverpod could function properly.

Your database tables live in {name}_server/generated/ of your Server folder. For the generated user_module, the database is in user_module_server/generated/.

Copy the content of {name}_server/generated/tables.pgsql from both your current project (app) and module (user_module) into your database client. Then, run the query to add the necessary tables. Do this one after the other

Step 2: Server setup

Add the module's server (user_moduble/user_module_server/) to your project's server (app/app_server/) as a dependency.

dependencies:
  user_module_server: ^1.x.x # Assuming it was hosted
Enter fullscreen mode Exit fullscreen mode

Then open your project's config/generator.yaml file and
add a name known as nickname for your module, primarily used for referencing the module from your app.

modules:
  user_module:
    nickname: user
Enter fullscreen mode Exit fullscreen mode

You will reference the module with the nickname from your Flutter app like this:

// ...
final user = client.user;
Enter fullscreen mode Exit fullscreen mode

Execute the command below inside your project's server folder (app/app_server/) to get the module and generate the necessary files.

dart pub get
Enter fullscreen mode Exit fullscreen mode

Then...

serverpod generate
Enter fullscreen mode Exit fullscreen mode

Step 3: Client setup

Add the module's client as a dependency on your project's client.

dependencies:
  user_module_client: ^1.x.x
Enter fullscreen mode Exit fullscreen mode

Then execute the command below.

dart pub get
Enter fullscreen mode Exit fullscreen mode

Step 4: Flutter app setup

Finally, a module can have Flutter package(s). The Flutter packages are usually separated.
An example is the serverpod_auth_google_flutter package for the serverpod_auth module.

Consider adding the flutter packages to your project's flutter app (app/app_flutter/) as a dependency.

The preceding information is enough to get you started with this tutorial but you can Check the documentation for more details.


How Does Authentication Work in Serverpod?

You'll be using Serverpod's auth module which handles basic user information such as user profile picture or names and how to retrieve/update the information.

Before we dive in into the explanation of the Serverpod auths module, Let's have a basic understanding of how authentication works in Serverpod and how it can help us build a better backend app.

  • Route guard

Serverpod provides the logic to restrict access to an endpoint. For instance, you can require the user to log-in before accessing the full features of the app or website.

For example:

class AppEndpoint extends Endpoint {
  // ... 
  @override
  bool get requireLogin => true;

}
Enter fullscreen mode Exit fullscreen mode

Restrictions can also be scoped.

Scoping restricts access to a resource to a specific type of user, such as an admin or any user (logged-in or not logged-in).

class SomeEndpoint extends Endpoint {
  /// ...
  /// This requires that the user is a `driver` to access 
  this endpoint
  @override
  Set<Scope> get requiredScopes => {Scope('driver')};
}
Enter fullscreen mode Exit fullscreen mode

With serverpod_auth_server, You can update the scope of a user with the snippet below.

/// Elevate the user’s scope to tell Serverpod that this user is now a `driver`
await Users.updateUserScopes(session, userId, {Scope('driver')});

Enter fullscreen mode Exit fullscreen mode
  • User information

Serverpod provides a handy Session object that stores information about the current user.

You can access the logged-in user with the help of the Session object as follow.

Future<void> doSomething(Session session) async {
  final isSignedIn = await session.auth.authenticatedUserId != null;
  if (isSignedIn) {
   //Do something here
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Session management

Serverpod also provides a SessionManager object that tracks the user's state. The SessionManager can be used to sign out a user.

late SessionManager sessionManager;
late Client client;

void main() async {
   // ...
  client = Client('http://localhost:8080/', authenticationKeyManager: 
  FlutterAuthenticationKeyManager(),
  )..connectivityMonitor = FlutterConnectivityMonitor();   
  // The session manager keeps track of the signed-in state of the user. You
  // can query it to see if the user is currently signed in and get information
  // about the user.
  sessionManager = SessionManager(caller: client.modules.auth,
);
  await sessionManager.initialize();
}
Enter fullscreen mode Exit fullscreen mode

Now that you have the basic knowledge of how Serverpod handles endpoint restrictions, Let's integrate the authentication module into the cloned noteapp project.

You need to start docker and Serverpod to continue.

Start docker and execute the command below inside noteapp/noteapp_server/ to start the necessary docker containers and to start Serverpod.

docker-compose up --build --detach
Enter fullscreen mode Exit fullscreen mode

Then:

dart bin/main.dart
Enter fullscreen mode Exit fullscreen mode

You should see output like this on your terminal if everything goes fine.

Starting Severpod and docker container

Configuring serverpod_auth Module

Follow the steps below to setup serverpod_auth for your project.

Step 1: Database setup

Add the database content of serverpod_auth_server to your project's database. Copy the database query generated for the serverpod_auth module from the source here into your database client and run the query.

Step 2: Server setup

Add serverpod_auth_server to your project's server (noteapp/noteapp_server/) as a dependency.

dependencies:
  serverpod_auth_server: ^1.x.x
Enter fullscreen mode Exit fullscreen mode

Then open config/generator.yaml file and
add a nickname for your module.

modules:
  serverpod_auth:
    nickname: auth
Enter fullscreen mode Exit fullscreen mode

Execute the command below inside your server folder (noteapp/noteapp_server/) to get the module and generate the necessary files.

dart pub get
Enter fullscreen mode Exit fullscreen mode

Then:

serverpod generate
Enter fullscreen mode Exit fullscreen mode

Step 3: Client setup

Add the modules' client as a dependency on your project's client.

dependencies:
  user_module_client: ^1.x.x
Enter fullscreen mode Exit fullscreen mode

Then execute the command below.

dart pub get
Enter fullscreen mode Exit fullscreen mode

Step 4: Flutter app setup

Add the following packages to your project Flutter app (noteapp/noteapp_flutter/) as a dependency.

dependencies:
  # ...
  serverpod_auth_shared_flutter: ^1.x.x
  serverpod_auth_email_flutter: ^1.x.x
Enter fullscreen mode Exit fullscreen mode

Some of the tables that will appear in your database after the preceding setup are...

After adding auth query to database

Configuring Email Authentication

You will need a Simple Mail Transfer Protocol (SMTP) provider and a package that can send email from Serverpod to properly set up email sign-in in your project.

One of the email sender packages is mailer. It's actively maintained and easy to set up. Check this tutorial on how to send email from a dart server by Suragch for further information.

You can use brevo for the SMTP provision part - Other options are Mailchimp and Mailgun.

Follow the steps below to add email auth.

Step 1: Server setup

Considering you've set up brevo SMTP provider, Open noteapp_server/lib/server.dart, and copy and paste the email auth configuration below inside the file.

void run(List<String> args) async {
  // Initialize Serverpod and connect it with your generated code.
  // ... other codes

  Future<bool> sendVerificationEmail({
    required String emailAddress,
    required String verificationCode,
  }) async {
    bool? isSent;

     // SMTP server details
    final smtp = "smtp-relay.sendinblue.com";
    final username = "email@domain.com";
    final password = "password";
    final port = 111;

    final smtpServer = SmtpServer(
      smtp,
      port: port,
      username: username,
      password: password,
    );

    final message = Message()
      ..recipients.add(emailAddress)
      ..from = Address(username, "Company\'s name")
      ..subject = "Verification code"
      ..text = "Hi, \n This is your verification code: $verificationCode.";
    try {
      await send(message, smtpServer);
      isSent = true;
    } on MailerException {
      isSent = false;
    }
    return isSent;
  }

  auth.AuthConfig.set(
    auth.AuthConfig(
      sendValidationEmail: (session, email, validationCode) async {
        final isSent = await sendVerificationEmail(
            emailAddress: email, verificationCode: validationCode);
        return isSent;
      },
      sendPasswordResetEmail: (session, userInfo, validationCode) async {
        // Add password reset email logic.

        ///  The function requires a boolean value to be returned
        /// if the email was sent or not. returning `true` for demonstration
        return Future.value(true);
      },
    ),
  );


// ... pod start code below
}
Enter fullscreen mode Exit fullscreen mode

Now, restart your server.

Step 2: Flutter app setup

Create three (3) empty files named: serverpod_client.dart, account_page.dart, note_page.dart, login_page.dart.

  • Replace the content in your main.dart with the code below.
import 'package:flutter/material.dart';
import 'package:noteapp_flutter/login_page.dart';
import 'package:noteapp_flutter/note_page.dart';
import 'package:noteapp_flutter/serverpod_client.dart';


void main() async {
  // Need to call this as SessionManager is using Flutter bindings before runApp
  // is called.
  WidgetsFlutterBinding.ensureInitialized();
  await initializeServerpodClient();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Serverpod Demo',
      theme: ThemeData(
        useMaterial3: true,
        primarySwatch: Colors.yellow,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
    // Make sure that we rebuild the page if signed in status changes.
    sessionManager.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: sessionManager.isSignedIn ? NotePage() : LoginPage(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Copy and paste the codes below into it respective files.

  • serverpod_client.dart
import 'package:noteapp_client/noteapp_client.dart';
import 'package:serverpod_auth_shared_flutter/serverpod_auth_shared_flutter.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';

late SessionManager sessionManager;
late Client client;

Future<void> initializeServerpodClient() async {
  // Sets up a singleton client object that can be used to talk to the server from
  // anywhere in our app. The client is generated from your server code.
  // The client is set up to connect to a Serverpod running on a local server on
  // the default port. You will need to modify this to connect to staging or
  // production servers.
  client = Client(
    'http://localhost:8080/',
    authenticationKeyManager: FlutterAuthenticationKeyManager(),
  )..connectivityMonitor = FlutterConnectivityMonitor();

  // The session manager keeps track of the signed-in state of the user. You
  // can query it to see if the user is currently signed in and get information
  // about the user.
  sessionManager = SessionManager(
    caller: client.modules.auth,
  );
}
Enter fullscreen mode Exit fullscreen mode
  • login_page.dart
import 'package:flutter/material.dart';
import 'package:noteapp_flutter/serverpod_client.dart';
import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart';


/// TODO: Finish the docs
/// LoginPage to...
class LoginPage extends StatelessWidget {
  /// Static named route for page
  static const String route = 'Login';

  /// Static method to return the widget as a PageRoute
  static Route go() => MaterialPageRoute<void>(builder: (_) => LoginPage());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Dialog(
          child: Container(
            width: 260,
            padding: const EdgeInsets.all(16),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Icon(
                  Icons.security,
                  size: 200,
                ),
                SignInWithEmailButton(
                  caller: client.modules.auth,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  • account_page.dart
import 'package:flutter/material.dart';
import 'package:noteapp_flutter/serverpod_client.dart';
import 'package:serverpod_auth_shared_flutter/serverpod_auth_shared_flutter.dart';


/// TODO: Finish the docs
/// AccountPage to...
class AccountPage extends StatelessWidget {
  /// Static named route for page
  static const String route = 'Account';

  /// Static method to return the widget as a PageRoute
  static Route go() => MaterialPageRoute<void>(builder: (_) => AccountPage());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title:const Text("Account"),),
        body: ListView(
      children: [
        ListTile(
          contentPadding:
              const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
          leading: CircularUserImage(
            userInfo: sessionManager.signedInUser,
            size: 42,
          ),
          title: Text(sessionManager.signedInUser!.userName),
          subtitle: Text(sessionManager.signedInUser!.email ?? ''),
        ),
        Padding(
          padding: const EdgeInsets.all(16),
          child: ElevatedButton(
            onPressed: () {
              sessionManager.signOut();
            },
            child: const Text('Sign out'),
          ),
        ),
      ],
    ));
  }
}

Enter fullscreen mode Exit fullscreen mode
  • note_page.dart
import 'package:flutter/material.dart';
import 'package:noteapp_client/noteapp_client.dart';
import 'package:noteapp_flutter/account_page.dart';
import 'package:noteapp_flutter/serverpod_client.dart';

/// TODO: Finish the docs
/// NotePage to...
class NotePage extends StatefulWidget {
  /// Static named route for page
  static const String route = 'Note';

  /// Static method to return the widget as a PageRoute
  static Route go() => MaterialPageRoute<void>(builder: (_) => NotePage());

  @override
  State<NotePage> createState() => _NotePageState();
}

class _NotePageState extends State<NotePage> {
  final _textEditingController = TextEditingController();

  // Calls the `createNote` method of the `note` endpoint. Will set `_errorMessage` field,
  void _callNote() async {
    try {
      await client.note.createNote(Note(
        data: _textEditingController.text,
        date: DateTime.now(),
      ));
    } catch (e) {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Take Note'), actions: [
        IconButton(
            icon: const Icon(Icons.person),
            onPressed: () {
              Navigator.of(context).push(AccountPage.go());
            })
      ]),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 16.0),
              child: TextField(
                controller: _textEditingController,
                decoration: const InputDecoration(
                  hintText: 'Enter your Note',
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(bottom: 16.0),
              child: ElevatedButton(
                onPressed: _callNote,
                child: const Text('Send to Server'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Note ⚠️:
Due to known issues with setup, you might need to remove the SMTP configuration part from the server code in order to successfully execute the program on your machine
.

The SMTP server configuration will not work properly if you are not running the Flutter app on Android or iPhone.

You will have a hard time making the SMTP work even if you are running the Flutter app on Android or iPhone and your server is on localhost.

For demonstration purposes, update the noteapp/noteapp_server/lib/server.dart file.

import 'package:serverpod/serverpod.dart';

import 'package:noteapp_server/src/web/routes/root.dart';
import 'package:serverpod_auth_server/module.dart' as auth;

import 'src/generated/protocol.dart';
import 'src/generated/endpoints.dart';

// This is the starting point of your Serverpod server. In most cases, you will
// only need to make additions to this file if you add future calls,  are
// configuring Relic (Serverpod's web-server), or need custom setup work.

void run(List<String> args) async {
  // Initialize Serverpod and connect it with your generated code.
  final pod = Serverpod(
    args,
    Protocol(),
    Endpoints(),
  );

  // If you are using any future calls, they need to be registered here.
  // pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall');

  // Setup a default page at the web root.
  pod.webServer.addRoute(RouteRoot(), '/');
  pod.webServer.addRoute(RouteRoot(), '/index.html');
  // Serve all files in the /static directory.
  pod.webServer.addRoute(
    RouteStaticDirectory(serverDirectory: 'static', basePath: '/'),
    '/*',
  );


  auth.AuthConfig.set(
    auth.AuthConfig(
      sendValidationEmail: (session, email, validationCode) async {
        print(validationCode);
        return Future.value(true);
      },
      sendPasswordResetEmail: (session, userInfo, validationCode) async {
        // Add password reset email logic.

        ///  The function requires a boolean value to be returned
        /// if the email was sent or not. returning `true` for demonstration
        return Future.value(true);
      },
    ),
  );

  // Start the server.
  await pod.start();
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Serverpod is a very versatile backend framework. It provides features that make it easy to implement user authentication for Flutter apps.

Below are some features of authentication in Serverpod:

  • User registration and login
  • Password reset
  • Account verification
  • Session management
  • Route guard


Note app email authentication

If you are having trouble with the code and configuration, you can switch to the auth branch of the cloned repository to see the code used in this tutorial.

git checkout auth
Enter fullscreen mode Exit fullscreen mode

Where To Go From Here?

You may want to consider adding other authentication methods, such as Sign in with Google or building a custom authentication module.

Reference

Credits


Top comments (0)