Getting Started with Serverpod: Authentication — Part 1

A Step-by-Step Guide to Implementing Email and Password Authentication in Serverpod

Isak
Serverpod
18 min readMay 11, 2023

--

Authentication Series

Part 1 — Email and Password Authentication
Part 2 — Google Authentication
Part 2.5 — Google API
Part 3 — Apple Authentication

Introduction

Welcome to Part 1 of our series on authentication with Serverpod! This article will focus on implementing email and password authentication in your Serverpod and Flutter applications. Email and password authentication is a common and essential feature for most applications, as it allows users to create accounts, log in, and securely access their data.

We’ll walk you through the entire process, covering everything from creating your Serverpod project to configuring the serverpod_auth module. We will also provide guidance on setting up server-side code and integrating with a third-party mail server, as well as building the user interface and connecting it to the server.

By the end of this article, you will have a solid understanding of how to implement email and password authentication in your Serverpod-Flutter application, providing a foundation for further exploration of additional authentication methods in the upcoming articles of this series.

The complete example project we are creating in this tutorial can be found here

Let’s get started!

Prerequisite

Before we dive into implementing email and password authentication with Serverpod, there are a few prerequisites you need to have in place. We assume that you have already installed the necessary tools, including the Serverpod CLI and Docker. If you haven’t set up these tools yet, please follow the official Serverpod documentation to get started.

Additionally, we recommend downloading and installing a database viewer such as Postico2, PgAdmin, or DBeaver to inspect the database and the tables we will create later in the article. You can use any database viewer that you prefer, in this article we will be using Postico2.

Creating your Serverpod project

Create a new Serverpod project: Run the following command to create a new Serverpod project:

serverpod create my_project

Navigate to the project directory: Change your working directory to the server directory within your project:

cd my_project/my_project_server

Windows only: if you are on windows you have to do this extra step during the setup to create the needed database tables for serverpod. Inside your server project you will find a cmd file called setup-tables.cmd run this script! It will execute the psql file located in generate/tables-serverpod.pgsql . On linux and mac this step is done automatically when creating the project.

Start the containers: Run the following command to start the Docker containers needed for your Serverpod project:

docker-compose up --build --detach

Installing and Configuring the serverpod_auth Module

The serverpod_auth module provides essential functionality for managing authentication in your Serverpod project. It includes features such as user registration, login, password hashing, and session management. In this section, we’ll guide you through the process of installing and configuring the serverpod_auth module and updating your database.

Server side setup

Add serverpod_auth to your dependencies: Open the pubspec.yaml file in your Serverpod project (my_project_server) and add the following line under the dependencies section:

dependencies:
serverpod_auth_server: ^2.0.0

Note: the version of all your serverpod dependencies should be the same! If you add serverpod_auth with version 2.0.0 make sure serverpod also have 2.0.0 as well as any other serverpod packages you may have installed.

For version 2.0 or later register the authenticationHandler on the serverpod object: Add the following code inside your main.dart file, this callback is used to authenticate the incoming requests from clients. Older versions does not need to complete this step.

import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth;

void run(List<String> args) async {
var pod = Serverpod(
args,
Protocol(),
Endpoints(),
authenticationHandler: auth.authenticationHandler, // Add this line
);

await pod.start();
}

Get the dependencies and generate the necessary files: Run the following commands in your project’s root directory to fetch the new dependency and generate the required files based on your server configuration:

dart pub get
serverpod generate

Update your existing database with the necessary tables: The Serverpod auth module comes with a set of database tables that are required for the module to work, let’s create them:

If you are running on Serverpod version 1.2 or later you should use the migration system! Create a new migration by running:

serverpod create-migration

And apply the new migration with:

dart bin/main.dart --apply-migrations --role=maintenance

Setting the role to maintenance means that the server will boot and connect to the database and then exit.

Version 1.1 or older:

  • Create a new SQL file: In the same folder as your existing tables-serverpod.pgsql file, create a new file called tables-serverpod-auth.pgsql. This file will contain the SQL code for creating the new tables.
  • Copy the SQL code: Open the following link to access the SQL code for creating the serverpod_auth module’s tables: serverpod_auth tables.pgsql. Copy the entire content of the file.
  • Paste the SQL code into the new file: Open the newly created tables-serverpod-auth.pgsql file and paste the copied SQL code into it.
  • Find your Docker container name: Locate your Docker container name by running docker ps or checking your Docker dashboard. You can also refer to the screenshot below for guidance.
Extract the container name for the postgres container

Copy the PostgreSQL file to the container and execute the sql code: Replace <container_name> with the name of your Docker container.

docker cp ./tables-serverpod-auth.pgsql <container_name>:/docker-entrypoint-initdb.d/tables-serverpod-auth.pgsql

docker exec -u postgres <container_name> psql my_project postgres -f /docker-entrypoint-initdb.d/tables-serverpod-auth.pgsql

Connect Postico to database

To get started, open Postico2 and click on “New Server” to create a new connection. You will need to enter the connection details for your local PostgreSQL server, which can be found in the config/development.yaml and config/passwords.yaml files in your Serverpod project.

Connecting to the database
You can see all the ables created on the left side

Everything looks good, all tables have been created, let’s move on to the next step.

Client library setup

To use the serverpod_auth module on the client-side, we need to add the serverpod_auth_client dependency to our client project.

Open the pubspec.yaml file in your client project and add the following lines under the dependencies section:

dependencies:
...
serverpod_auth_client: ^2.0.0

This package contains the client-side library code that we need to make API calls to the server for email and password authentication. While this dependency is not strictly required if you are using the pre-built UI components provided by serverpod_auth_email_flutter, it comes with all the generated auth endpoints that you can interact with, so it is a good idea to add it here.

Flutter app setup

After implementing the necessary changes to the server side for email and password authentication with Serverpod, the next step is to integrate it with the Flutter app. Luckily, Serverpod provides pre-built UI components to make this process as smooth as possible.

First, we need to add the required client-side dependencies to our Flutter app. Open the pubspec.yaml file in your Serverpod project (my_project_flutter) and add the following lines under the dependencies section:

dependencies:
...
serverpod_auth_email_flutter: ^2.0.0
serverpod_auth_shared_flutter: ^2.0.0

After adding the required dependencies to your pubspec.yaml file, be sure to run flutter pub get in your terminal to update your dependencies.

These packages include pre-built UI components and other tools to make integrating with the server as easy as possible. However, if you prefer to build your own UI components, you can still integrate with the generated client library and if you go down that route you do not need these dependencies.

Starting the server and running the client

Congrats! We now have a project with all the required dependencies setup let’s make sure everything runs before we move on.

To start the server, navigate to the my_project_server directory in your terminal and run the following command:

cd my_project_server
dart bin/main.dart
This is what it should look like if everything is working

Next, navigate to the my_project_flutter directory in a new terminal window and run the following command to start the Flutter app:

cd my_project_flutter
flutter run

Select to run in chrome and test to send a message!

Write something in the text box and click “Send to Server”

Troubleshooting

If you’re encountering issues there are a few things you can check to help diagnose the problem:

  • Make sure that you have followed all the previous steps in this guide correctly.
  • Check that the server is running without any errors. If there are errors, they will be displayed in the terminal where you started the server.
  • Verify that all necessary database tables have been created. You can check this by connecting to your database using a tool like Postgres or pgAdmin and looking at the tables in the public schema. If any tables are missing, make sure that you have run the SQL scripts to create them.
  • If you are still encountering issues, check the output in your app’s console or logs for any error messages that might provide additional context.

Implementing email/password authentication

Now that we have set up the server and client libraries, it’s time to implement authentication in our Flutter app. We will use the serverpod_auth_email_flutter and serverpod_auth_shared_flutter packages that we previously added as dependencies.

The first step is to initialize the Client and SessionManager objects. Let’s first create a new file called serverpod_client.dart put it inside my_project_flutter/lib/src/ .

import 'package:my_project_client/my_project_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 {
// The android emulator does not have access to the localhost of the machine.
// const ipAddress = '10.0.2.2'; // Android emulator ip for the host

// On a real device replace the ipAddress with the IP address of your computer.
const ipAddress = 'localhost';

// 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://$ipAddress: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();
}

This function sets up a singleton Client object that can be used to communicate with the server from anywhere in the app. It also initializes the SessionManager object that keeps track of the signed-in state of the user.

To use the Client object created in the previous step, we need to initialize it as a singleton instance. This can be done in the main() function in your main.dart file. First, we need to call WidgetsFlutterBinding.ensureInitialized() to ensure that Flutter is fully initialized before the SessionManager is used. Then we can call the initializeServerpodClient() function we just created. Finally, we can call runApp() to start the app. Here's an example of what the updated main() function might look like:

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

await initializeServerpodClient();

runApp(const MyApp());
}

Next, we will create a SignInPage widget that will display a login form to the user. We will also create a AccountPage widget that will be displayed to the user after they have successfully logged in.

Creating the SignIn Page

To enable email and password authentication in our Flutter app, we will create a sign-in page using a pre-built widget from the serverpod_auth_email_flutter package called SignInWithEmailButton. This widget generates a sign-in button with a dialog to handle the sign-in flow.

To start, create a new file named sign_in_page.dart in the lib/src/widgets folder of the Flutter app.

Next, create a new class named SignInPage that extends StatelessWidget. This class will return the sign-in button wrapped in a Dialog widget:

import 'package:flutter/material.dart';
import 'package:serverpod_auth_email_flutter/serverpod_auth_email_flutter.dart';
import 'package:my_project_flutter/src/serverpod_client.dart';

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

@override
Widget build(BuildContext context) {
return Center(
child: Dialog(
child: Container(
width: 260,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SignInWithEmailButton(
caller: client.modules.auth,
),
],
),
),
),
);
}
}

In the code above, the SignInWithEmailButton widget takes an argument called caller which is an instance of the generated client library provided by Serverpod. This allows the widget to communicate with the Serverpod server.

Add the SignInPage into the Home Page

To add the SignInPage into our app's home page, we need to modify the MyHomePage class defined in the main.dart file located in the root of the lib/ folder.

Replace the existing MyHomePage class with the following code. Note that anything below this class can be removed.

class MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const SignInPage(),
);
}
}

Don’t forget to add the import statement for the SignInPage widget as well:

import 'package:my_project_flutter/src/widgets/sign_in_page.dart';

Your entire main.dart should look like this:

import 'package:my_project_flutter/src/serverpod_client.dart';
import 'package:my_project_flutter/src/widgets/sign_in_page.dart';
import 'package:flutter/material.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(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Serverpod Example'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

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

class MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const SignInPage()
);
}
}

Refreshing the app we can now see the login button! While we are not done yet it should actually communicate with the server as is!

If you have done everything correctly so far you should see this on your flutter app!

We have now integrated the sign-in page into our app’s home page and can create an account. However, we still need a verification code sent by the server to complete the login process. To sign up for an account, the user can fill in the required fields on the sign-up page and submit the form. Once submitted, the server will send a verification code to the provided email address. Currently, we do not have access to the verification code, so we won’t be able to complete the login process yet. Let’s fix that!

Integrating Email Verification Callbacks

To complete the sign-up process, we need to add callbacks for sending verification emails. Serverpod provides a few handy callbacks to send mails for the verification code for signups and for password resets. For now let’s implement a quick and dirty solution just to test things out by printing the code to the console,

In the server.dart file in the server project, we can add the following configuration to set the callbacks:

import 'package:serverpod_auth_server/module.dart' as auth;

void run(List<String> args) async {
...
auth.AuthConfig.set(auth.AuthConfig(
sendValidationEmail: (session, email, validationCode) async {
// TODO: integrate with mail server
print('Validation code: $validationCode');
return true;
},
sendPasswordResetEmail: (session, userInfo, validationCode) async {
// TODO: integrate with mail server
print('Validation code: $validationCode');
return true;
},
));

...
await pod.start();
}

We add this code before calling pod.start() inside the run()method to ensure that the callbacks are properly set up.

To make the changes we just made take effect, we need to restart the Serverpod server. Go to the terminal where you started the server and press “CTRL + C” to stop it. Then, run “dart bin/main.dart” to start the server again.

Let’s test things out!

Create the sign in button, and fill in the information!

Add your user details to create an account

For now we will not receieve an email, you will have to look for the verification code in the terminal where you launched the server.

Copy the verification code
Enter the verification code you copied

Verifying User Creation in the Database

Now that we have set up the sign-up functionality for our application, we need to verify that the user data is being saved correctly to the database. Since we have not implemented the user page in our Flutter app yet, we can verify the user creation using Postico2.

Open Postico2 and connect to your database as we did previously in the setup section. Once you have connected to the database, click on the ‘serverpod_user_info’ table. You should see the user that you just created.

You should see something similar to this

Implementing the Account Page

Now that we have the sign-in page working, let’s implement the account page to show information about the signed-in user. In order to do this, we’ll use the SessionManager object that we created earlier. The SessionManager keeps track of the signed-in state of the user and provides access to information about the user.

Let’s take a look at some of the functions provided by the SessionManager:

  • isSignedIn(): This function returns true if the user is currently signed in, and false otherwise.
  • getSignedInUser(): This function returns an object of type UserInfo that contains information about the currently signed-in user, such as their email address and display name.
  • signOut(): This function signs the user out and clears the authentication state.

The CircularUserImage widget is a pre-built widget provided by the Serverpod framework that takes a UserInfo object as input and displays a circular profile image. Combining the sessionManager and this widget we can create a good looking account page.

We’ll create a new file called account_page.dart inside the lib/src/widgets directory and add the following code:

import 'package:flutter/material.dart';
import 'package:serverpod_auth_shared_flutter/serverpod_auth_shared_flutter.dart';

import 'package:my_project_flutter/src/serverpod_client.dart';

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

@override
Widget build(BuildContext context) {
return 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'),
),
),
],
);
}
}

We can use the UserInfo object to retrieve the user’s name and email and display it in the UI. We can also provide a Sign out button that allows the user to log out of the application, by calling sessionManager.signOut(); when the button is pressed.

Now we need to show the AccountPage if the user is logged in. To toggle between the login page and the account page, depending on if the user is logged in or not. Modify your main.dart file and inside the build method, replace the const SignInPage() with this code:

sessionManager.isSignedIn ? const AccountPage() : const SignInPage(),

This ternary operator will render the AccountPage() if the user is signed in, and the SignInPage() if the user is not signed in.

To make sure that our app updates the user interface depending on the session state changes, we need to add the following code inside the MyHomePageState in the main.dart

@override
void initState() {
super.initState();

// Make sure that we rebuild the page if signed in status changes.
sessionManager.addListener(() {
setState(() {});
});
}

This code sets up a listener on the sessionManager that rebuilds the page whenever there is a change in the session state, allowing the app to update the user interface depending on whether the user is signed in or not.

The full class should look like this:

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(
appBar: AppBar(
title: Text(widget.title),
),
body:
sessionManager.isSignedIn ? const AccountPage() : const SignInPage(),
);
}
}

With these changes, the app will show the login page when the user is not signed in, and show the user account page when the user is signed in.

AccountPage with a nicely rendered UserInfo widget

Try it out yourself! You now have a fully functional signup and login flow implemented completed with user data being stored in the database! Last step we have postponed until now is integrating with a mail server.

Integrating with a Mail Server

To send verification codes and password reset links, we need to integrate our app with an external mail server. There are several options available for this, including SendGrid, Mailjet, and others. However, for the purposes of this tutorial, we will use Gmail as the mail server.

It’s important to note that this is not a good solution for a production app. Using Gmail to send emails from your app can lead to delivery issues, you may be banned for spam, etc. For a production app, we strongly recommend using a professional email service.

With that said, let’s go ahead and integrate our app with Gmail.

First, we need to add the mailer package to our server project. We can do this by running the following command in the terminal:

dart pub add mailer

Next, we need to set up a Gmail account to use for sending emails. If you don’t have a Gmail account, you can create one for free at https://accounts.google.com/signup.

Once you have a Gmail account set up, follow these steps:

  1. Go to the Google Account Security page.
  2. Under “How you sign in to Google” turn on “2-Step Verification.”
  3. Follow the prompts to set up 2-Step Verification for your account. You will need to provide a phone number to receive verification codes.
  4. Once you have set up 2-Step Verification, create an App Password for your account. This will be the password that our app will use to send emails.
Select Mail, and Other to add a custom name
Copy the password in the yellow box

Let’s now add the password to our passwords file in the serverpod project, you can find this file under config/passwords.yaml . A word of caution here, never store this file in version control, infact the serverpod project comes preconfigured with this file added to .gitignore. Instead always manage your secrets outside of your project, and during production deploy keep them as secret variables in your CI/CD pipeline.

Add the key/valuesgmailEmailand gmailPassword

# These are passwords used when running the server locally in development mode
development:
database: '9S8rYW7XeIA8bmGY9FBzOSLwQZtQEFNr'
redis: 'V7YogaG9K2rnIpS1odXIKrqsW8kkfddt'
gmailEmail: '<your gmail email>'
gmailPassword: '<your gmail key>'

# The service secret is used to communicate between servers and to access the
# service protocol.
serviceSecret: 'IWtaP1Z-Db-F70IBJpWGf3D7x9F3AYGg'

We can easily add custom keys and retrieve them later in our code by adding them in the passwords file. This is a convenient way to inject secrets into our server.

Now that we have a Gmail account and secrets set up, we can implement the logic to send the validationCode with an email. To do that we have to modify the AuthConfig we created previously inside server.dart in the server project.

First, we retrieve the credentials for the Gmail SMTP server from the session.serverpod object calling thegetPassword function. This function will retrieve the secrets we added in the previous step.

// Retrieve the credentials
final gmailEmail = session.serverpod.getPassword('gmailEmail')!;
final gmailPassword = session.serverpod.getPassword('gmailPassword')!;

Next, we create an SMTP client for Gmail using the retrieved email and password:

// Create a SMTP client for Gmail.
final smtpServer = gmail(gmailEmail, gmailPassword);

Then, we create an email message using the validation code:

// Create an email message with the validation code.
final message = Message()
..from = Address(gmailEmail)
..recipients.add(email)
..subject = 'Verification code for Serverpod'
..html = 'Your verification code is: $validationCode';

Finally, we attempt to send the email message using the send function from the mailer package. If sending the email fails, we return false:

// Send the email message.
try {
await send(message, smtpServer);
} catch (_) {
// Return false if the email could not be sent.
return false;
}

return true;

Putting it all together the code should look something like this when implementing both sendValidationEmail and sendPasswordResetEmail .

// Configuration for sign in with email.
auth.AuthConfig.set(auth.AuthConfig(
sendValidationEmail: (session, email, validationCode) async {
// Retrieve the credentials
final gmailEmail = session.serverpod.getPassword('gmailEmail')!;
final gmailPassword = session.serverpod.getPassword('gmailPassword')!;

// Create a SMTP client for Gmail.
final smtpServer = gmail(gmailEmail, gmailPassword);

// Create an email message with the validation code.
final message = Message()
..from = Address(gmailEmail)
..recipients.add(email)
..subject = 'Verification code for Serverpod'
..html = 'Your verification code is: $validationCode';

// Send the email message.
try {
await send(message, smtpServer);
} catch (_) {
// Return false if the email could not be sent.
return false;
}

return true;
},
sendPasswordResetEmail: (session, userInfo, validationCode) async {
// Retrieve the credentials
final gmailEmail = session.serverpod.getPassword('gmailEmail')!;
final gmailPassword = session.serverpod.getPassword('gmailPassword')!;

// Create a SMTP client for Gmail.
final smtpServer = gmail(gmailEmail, gmailPassword);

// Create an email message with the password reset link.
final message = Message()
..from = Address(gmailEmail)
..recipients.add(userInfo.email!)
..subject = 'Password reset link for Serverpod'
..html = 'Here is your password reset code: $validationCode>';

// Send the email message.
try {
await send(message, smtpServer);
} catch (_) {
// Return false if the email could not be sent.
return false;
}

return true;
},
));

Don’t forget to restart your server to make sure the new changes take effect. Once you’ve done that, it’s time to test it out! Try creating a new account and see if you receive the verification code via email. If everything works as expected, then congratulations! You’ve successfully integrated Serverpod with an email service provider.

Conclusion

In this tutorial, we have covered the basics of integrating serverpod_auth_google_flutter with a Flutter app, enabling us to create user accounts, log in and verify email addresses. We also briefly discussed implementing mail server integration with the mailer package and using Gmail as a sender for testing purposes.

In the next part of this series, we will discuss how to integrate Google social login with serverpod_auth_google_flutter to enable users to log in with their Google accounts. This will allow us to provide an easy and convenient way for users to sign up and sign in without the need to create a new account.

By the end of this series, you will have a solid foundation in integrating authentication into your Flutter app with serverpod_auth, enabling your users to create accounts, log in, and access secure content.

--

--