Getting Started with Serverpod: Authentication — Part 1
A Step-by-Step Guide to Implementing Email and Password Authentication in Serverpod
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 calledtables-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.
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.
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
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!
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!
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!
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.
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.
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 returnstrue
if the user is currently signed in, andfalse
otherwise.getSignedInUser()
: This function returns an object of typeUserInfo
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.
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:
- Go to the Google Account Security page.
- Under “How you sign in to Google” turn on “2-Step Verification.”
- Follow the prompts to set up 2-Step Verification for your account. You will need to provide a phone number to receive verification codes.
- 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.
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/valuesgmailEmail
and 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.