Authentication is the process by which a user verifies their identity to access specific resources in an application. Typically, this involves logging in with a username and password. The client submits these credentials, and the server then compares them against its database to confirm the user's identity.
Username and password authentication is a common method in web applications. This guide will detail how to implement such authentication in Flutter, using Strapi as the backend server.
To continue with this article, it is helpful to have the following:
Let's set up an Authentication server using Strapi on your local machine. Strapi simplifies the process of bootstrapping a server for API development. To get started with Strapi CMS, navigate to your desired directory and execute the following command:
npx create-strapi-app@latest strapi-auth
This command creates a new directory named strapi-auth, installing all necessary Strapi CMS dependencies. To verify if the setup was successful, switch to the new directory:
cd strapi-auth
Then, initiate the development environment:
npm run develop
Upon successful startup, you'll be prompted to either log in or sign up on the Strapi authentication page.
Note: Post-installation, Strapi may automatically redirect you to the authentication dashboard in your browser. If this doesn't happen, simply follow the commands outlined above.
In this project, we'll set up user authentication using Strapi and Flutter. Once authenticated, users gain access to the application's content. Specifically, we will focus on post data as the accessible content post-authentication. This requires modeling both user information and post data in Strapi to be displayed in the Flutter app.
To configure these in your Strapi dashboard: 1. Select the Content Type Builder link in the left pane. 2. On the resulting page, you can view and edit the fields for a user as shown:
The default fields are typically adequate, but you can add more by clicking the Add another field button.
To set up post content:
posts
:
Choose the necessary fields for your posts. For the scope of this article, we'll use two: - Title (string) - Description (rich text)
Your configured fields for the posts should look like this:
Click Save to finalize and apply the data architecture for your content.
To securely access Strapi content externally, an API token is required. Follow these steps to generate one:
In the form, fill in the Name, Description, and select the Token type as Full access for user creation purposes. Remember to copy and save the generated API Token immediately, as it cannot be retrieved once you navigate away from the token generation panel.
You can add data to your posts in Strapi in two ways: using the Strapi Content Manager or an API platform like Postman.
In the Strapi dashboard, navigate to the Content Manager and select the posts collection type.
Click the Create new entry button and enter the details for your post.
Enter the post details and ensure you click the image.png button to add the new entry.
http://localhost:1337/api/posts
To enter a new post, add, in the request body, enter a JSON like payload as below:
1 {
2 "title":"dummy title one",
3 "description":"dummy description one"
4 }
If you added additional fields to the post's collection type, make sure to include them in your API request payload. After submitting the request, you should receive a response similar to this:
1 {
2 "data": {
3 "id": 5,
4 "attributes": {
5 "title": "Post five",
6 "description": "Dummy post five",
7 "createdAt": "2022-09-17T14:36:54.353Z",
8 "updatedAt": "2022-09-17T14:36:54.353Z",
9 "publishedAt": "2022-09-17T14:36:54.350Z"
10 }
11 },
12 "meta": {}
13 }
Feel free to create as many posts as you need. With our backend now fully configured, let's proceed to set up the Flutter application.
To initiate your Flutter project, execute the following command from your chosen directory:
flutter create flutter_auth_project
To confirm that Flutter is functioning correctly, navigate to the newly created Flutter project directory:
cd flutter_auth_project
Then execute:
flutter run
This command will launch a basic Flutter application, adapted to your testing environment, such as a mobile device or web browser.
Great! Your Flutter environment is now ready to integrate with Strapi.
Several elements are key to connecting Flutter with a Strapi server, including server endpoints and the Strapi API access token. By configuring these, we can facilitate local access to Strapi and enable server-side operations using Flutter as the client.
Start by managing these configurations in a .env
file. Install the Flutter Dotenv library in your application with this command:
flutter pub add flutter_dotenv
In the root directory of your Flutter project, create a .env
file and define your Strapi constants like so:
1 baseUrl=http://localhost:1337/api
2 usersEndpoint=/users
3 accesToken=your_access_token
4 postsEndpoint=/posts
Ensure to replace your_access_token
with the actual API token generated from Strapi.
Next, in your project’s pubspec.yaml
file, include the .env
file in your assets bundle:
1 flutter:
2 assets:
3 - .env
To interact with the Strapi backend, we will utilize the HTTP package for HTTP requests. Install it using this command:
flutter pub add http
You're making great progress! Now, let's start implementing the Flutter authentication.
After setting up, go to the flutter_auth_project
directory. Here, we'll create several directories inside the lib
folder:
models
: To store models for users and posts.screens
: To house different application screens such as login, sign up, and dashboard.utils
: For server request functions.To interact with the user details, you need to model the user details. Navigate to the lib/models
and create a user.dart
. Then create a user model as follows:
1 import 'dart:convert';
2
3 // getting a list of users from json
4 List<User> UserFromJson(String str) =>
5 List<User>.from(json.decode(str).map((x) => User.fromJson(x)));
6 // getting a single user from json
7 User singleUserFromJson(String str) => User.fromJson(json.decode(str));
8
9 // user class
10 class User {
11 User({
12 required this.id,
13 required this.username,
14 required this.email,
15 required this.provider,
16 required this.confirmed,
17 required this.blocked,
18 required this.createdAt,
19 required this.updatedAt,
20 });
21
22 int id;
23 String username;
24 String email;
25 String provider;
26 bool confirmed;
27 bool blocked;
28 DateTime createdAt;
29 DateTime updatedAt;
30
31 factory User.fromJson(Map<String, dynamic> json) => User(
32 id: json["id"],
33 username: json["username"],
34 email: json["email"],
35 provider: json["provider"],
36 confirmed: json["confirmed"],
37 blocked: json["blocked"],
38 createdAt: DateTime.parse(json["createdAt"]),
39 updatedAt: DateTime.parse(json["updatedAt"]),
40 );
41 }
Likewise, create a model to display the post data. Inside the create a post.dart
file as such:
1 List<Post> postFromJson(List<dynamic> post) =>
2 List<Post>.from(post.map((x) => Post.fromJson(x)));
3
4 class Post {
5 Post({
6 required this.title,
7 required this.description,
8 required this.createdAt,
9 required this.updatedAt,
10 required this.id,
11 });
12
13 String title;
14 DateTime createdAt;
15 String description;
16 int id;
17 DateTime updatedAt;
18
19 factory Post.fromJson(Map<dynamic, dynamic> json) {
20 return Post(
21 title: json['attributes']['title'],
22 description: json['attributes']['description'],
23 createdAt: DateTime.parse(json['attributes']['createdAt']),
24 id: json['id'],
25 updatedAt: DateTime.parse(json['attributes']['updatedAt']));
26 }
27 }
To utilize the defined user and post models, Flutter requires a method to retrieve and process them through Strapi. This is achieved by leveraging the Flutter HTTP package for server communication. Implement these models based on the API endpoints, incorporating Strapi's Authorization headers for secure data handling.
Firstly, create a server.dart
file in the lib/utils
directory. Add the following code to handle server communications:
But first, create a server.dart
file inside the lib/utils
directory as follows:
1 import 'dart:convert';
2 import 'dart:developer';
3 import 'package:http/http.dart' as http;
4 import 'package:flutter_dotenv/flutter_dotenv.dart';
5 import 'package:flutter_auth_project/models/user.dart';
6 import 'package:flutter_auth_project/models/post.dart';
7
8 class ApiService {
9 // Getting users
10 Future<List<User>?> getUsers() async {
11 try {
12 var url = Uri.parse(dotenv.get('baseUrl') + dotenv.get('usersEndpoint'));
13 var response = await http.get(url,
14 headers: {"Authorization": "Bearer ${dotenv.get('accesToken')}"});
15 if (response.statusCode == 200) {
16 List<User> _model = UserFromJson(response.body);
17 return _model;
18 } else {
19 String error = jsonDecode(response.body)['error']['message'];
20 throw Exception(error);
21 }
22 } catch (e) {
23 log(e.toString());
24 }
25 }
26
27 // Adding user
28 Future<User?> addUser(String email, String username, String password) async {
29 try {
30 var url = Uri.parse(dotenv.get('baseUrl') + dotenv.get('usersEndpoint'));
31 var response = await http.post(url,
32 headers: {"Authorization": "Bearer ${dotenv.get('accesToken')}"},
33 body: {"email": email, "username": username, "password": password});
34
35 if (response.statusCode == 201) {
36 User _model = singleUserFromJson(response.body);
37 return _model;
38 } else {
39 String error = jsonDecode(response.body)['error']['message'];
40 throw Exception(error);
41 }
42 } catch (e) {
43 throw Exception(e);
44 }
45 }
46
47 // Getting posts
48 Future<List<Post>?> getPosts() async {
49 try {
50 var url = Uri.parse(dotenv.get('baseUrl') + dotenv.get('postsEndpoint'));
51 var response = await http.get(url,
52 headers: {"Authorization": "Bearer ${dotenv.get('accesToken')}"});
53
54 if (response.statusCode == 200) {
55 var _model = postFromJson(jsonDecode(response.body)['data']);
56 return _model;
57 } else {
58 throw Exception(jsonDecode(response.body)["error"]["message"]);
59 }
60 } catch (e) {
61 throw Exception(e);
62 }
63 }
64 }
Users need to set and fill in their data while creating/registering an account. Let’s dive in and create a form for submitting the authentication data. This will use material components to easily build any frontend components.
Navigate to the lib/screens
directory and create a signup_screen.dart
file. Then add the following code to this file:
Add the needed dependencies and modules:
1 import 'package:flutter/material.dart';
2 import 'package:flutter_auth_project/screens/login_screen.dart';
3 import 'package:flutter_auth_project/models/user.dart';
4 import 'package:flutter_auth_project/utils/server.dart';
5 import 'package:flutter_auth_project/screens/dashboard_screen.dart';
Here, you require the Material library to create the form elements. The user models to get what details the user needs to submit. And the server executes the user endpoint.
Also Note: These imports include two screens:
We will create them later in this guide.
Create a Stateful Signup Widget as follows:
1 class Signup extends StatefulWidget {
2 static const namedRoute = "signup-screen";
3 const Signup({Key? key}) : super(key: key);
4 }
Inside Signup
, create a state for user registration details as follows:
1 @override
2 State<Signup> createState() => _SignupState();
Implement the states using _SignupState
as follows:
1 class _SignupState extends State<Signup> {
2 String _username = "";
3 String _email = "";
4 String _password = "";
5 String _error = "";
6
7 void _signup() async {
8 try {
9 User? createduser =
10 await ApiService().addUser(_email, _username, _password);
11 if (createduser != null) {
12 // navigate to the dashboard.
13 Navigator.pushNamed(context, Dashboard.namedRoute);
14 }
15 } on Exception catch (e) {
16 setState(() {
17 _error = e.toString().substring(11);
18 });
19 }
20 }
21 }
From the above:
addUser()
method from the server. This will execute the user endpoint using the POST HTTP method to send a request body with user information. These are username, password, and email.Let’s now create a form to submit these details to the Strapi server. Inside the_SignupState
class add your Widget that’s created the user signup screen as follows:
1 @override
2 Widget build(BuildContext context) {
3 return Scaffold(
4 appBar: AppBar(
5 title: const Text('Create Account'),
6 ),
7 body: SingleChildScrollView(
8 child: Column(
9 children: <Widget>[
10 Padding(
11 padding: const EdgeInsets.only(top: 60.0),
12 child: Center(
13 child: Column(
14 children: const [
15 SizedBox(
16 height: 150,
17 ),
18 Text("Strapi App"),
19 SizedBox(
20 height: 20,
21 )
22 ],
23 ),
24 )),
25 if (_error.isNotEmpty)
26 Column(
27 children: [
28 Text(
29 _error,
30 style: const TextStyle(color: Colors.red),
31 ),
32 const SizedBox(
33 height: 10,
34 )
35 ],
36 ),
37 Padding(
38 //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
39 padding: const EdgeInsets.symmetric(horizontal: 15),
40 child: TextField(
41 onChanged: (value) {
42 setState(() {
43 _username = value;
44 });
45 },
46 decoration: const InputDecoration(
47 border: OutlineInputBorder(),
48 labelText: 'Username',
49 hintText: 'Enter valid username e.g. Paul'),
50 ),
51 ),
52 const SizedBox(height: 15),
53 Padding(
54 //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
55 padding: const EdgeInsets.symmetric(horizontal: 15),
56 child: TextField(
57 onChanged: (value) {
58 setState(() {
59 _email = value;
60 });
61 },
62 decoration: const InputDecoration(
63 border: OutlineInputBorder(),
64 labelText: 'Email',
65 hintText: 'Enter valid email id as abc@gmail.com'),
66 ),
67 ),
68 Padding(
69 padding: const EdgeInsets.only(
70 left: 15.0, right: 15.0, top: 15, bottom: 0),
71 //padding: EdgeInsets.symmetric(horizontal: 15),
72 child: TextField(
73 obscureText: true,
74 onChanged: (value) {
75 setState(() {
76 _password = value;
77 });
78 },
79 decoration: const InputDecoration(
80 border: OutlineInputBorder(),
81 labelText: 'Password',
82 hintText: 'Enter secure password'),
83 ),
84 ),
85 const SizedBox(height: 15),
86 Container(
87 height: 50,
88 width: 180,
89 decoration: BoxDecoration(
90 color: Colors.blue, borderRadius: BorderRadius.circular(20)),
91 // ignore: deprecated_member_use
92 child: TextButton(
93 onPressed: () => _signup(),
94 child: const Text(
95 'Create Account',
96 style: TextStyle(color: Colors.white, fontSize: 16),
97 ),
98 ),
99 ),
100 const SizedBox(
101 height: 30,
102 ),
103 // ignore: deprecated_member_use
104 TextButton(
105 onPressed: () {
106 // navigate to the signup screen
107 Navigator.push(
108 context, MaterialPageRoute(builder: (_) => Login()));
109 },
110 child: const Text(
111 'Already have an account? Login',
112 style: TextStyle(fontSize: 16),
113 ),
114 ),
115 ],
116 ),
117 )
118 );
119 }
From the above:
If the values are valid and an account is created, the user is directed to the dashboard screen. Let’s dive in and create this screen.
Build a Dashboard Screen
This page will only be displayed to a user who has been either successfully signed up or logged in. Here we will display the list of posts we created earlier with Strapi.
Navigate to the lib/screens
directory, and create a dashboard_screen.dart
. Then Fetch the posts data as follows:
Import the:
1 import 'package:flutter/material.dart';
2 import 'package:flutter_auth_project/models/post.dart';
3 import 'package:flutter_auth_project/utils/server.dart';
Create a Dashboard StatefulWidget and the state for the post information.
1 class Dashboard extends StatefulWidget {
2 static const namedRoute = "dashboard-screen";
3 const Dashboard({Key? key}) : super(key: key);
4
5 @override
6 State<Dashboard> createState() => _DashboardState();
7 }
Implement the states using _SignupState
as follows:
1 class _DashboardState extends State<Dashboard> {
2 }
1List<Post> _posts = [];
2 String _error = "";
3 @override
4 void initState() {
5 super.initState();
6 _getData();
7 }
8
9 void _getData() async {
10 try {
11 _posts = (await ApiService().getPosts())!;
12 Future.delayed(const Duration(seconds: 1)).then((value) => setState(() {
13 _posts = _posts;
14 _error = "";
15 }));
16 } on Exception catch (e) {
17 _error = e.toString();
18 }
19 }
1@override
2 Widget build(BuildContext context) {
3 return Scaffold(
4 appBar: AppBar(
5 title: const Text('Dashboard'),
6 ),
7 body: _posts.isEmpty
8 ? const Center(
9 child: CircularProgressIndicator(),
10 )
11 : ListView.builder(
12 itemCount: _posts.length,
13 itemBuilder: (BuildContext ctx, int index) {
14 Post data = _posts[index];
15 return ListTile(
16 contentPadding: const EdgeInsets.all(5.0),
17 title: Text(data.title),
18 subtitle: Text(data.description),
19 leading: CircleAvatar(
20 backgroundColor: Colors.grey,
21 child: Text((data.id).toString()),
22 ),
23 );
24 }
25 ),
26 );
27 }
From this screen, we are simply fetching posts created from earlier and presenting them to the authenticated user. Upon signup, you will be directed to the dashboard screen.
In lib/screens
directory, create a login_screen.dart
file as below:
1 import 'package:flutter/material.dart';
2 import 'package:flutter_auth_project/screens/signup_screen.dart';
3 import 'package:flutter_auth_project/screens/dashboard_screen.dart';
4 import 'package:flutter_auth_project/utils/server.dart';
5 import 'package:flutter_auth_project/models/user.dart';
6
7 class Login extends StatefulWidget {
8 static const namedRoute = "login-screen";
9 const Login({Key? key}) : super(key: key);
10
11 @override
12 State<Login> createState() => _LoginState();
13
14 }
15
16 class _LoginState extends State<Login> {
17 String _email = "";
18 String _password = "";
19 String _error = "";
20
21 void _login() async {
22 try {
23 List<User> users = (await ApiService().getUsers())!;
24 late User? loggedInUser;
25 if (users.isNotEmpty) {
26 for (var i = 0; i < users.length; i++) {
27 if (users[i].email == _email) {
28 loggedInUser = users[i];
29 break;
30 }
31 }
32 }
33 if (loggedInUser == null) {
34 setState(() {
35 _error = "Your account does not exist.";
36 });
37 }
38 else {
39 // navigate to the dashboard screen.
40 Navigator.pushNamed(context, Dashboard.namedRoute);
41 }
42 } on Exception catch (e) {
43 setState(() {
44 _error = e.toString().substring(11);
45 });
46 }
47 }
48
49 @override
50 Widget build(BuildContext context) {
51 return Scaffold(
52 appBar: AppBar(
53 title: const Text('Login'),
54 ),
55 body: SingleChildScrollView(
56 child: Column(
57 children: <Widget>[
58 Padding(
59 padding: const EdgeInsets.only(top: 60.0),
60 child: Center(
61 child: Column(
62 children: const [
63 SizedBox(
64 height: 150,
65 ),
66 Text("Strapi App"),
67 SizedBox(
68 height: 20,
69 )
70 ],),
71 )),
72 if (_error.isNotEmpty)
73 Column(
74 children: [
75 Text(
76 _error,
77 style: const TextStyle(color: Colors.red),
78 ),
79 const SizedBox(
80 height: 10,
81 )
82 ],
83 ),
84 Padding(
85 //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
86 padding: const EdgeInsets.symmetric(horizontal: 15),
87 child: TextField(
88 onChanged: (value) {
89 setState(() {
90 _email = value;
91 });
92 },
93 decoration: const InputDecoration(
94 border: OutlineInputBorder(),
95 labelText: 'Email',
96 hintText: 'Enter valid email id as abc@gmail.com'),
97 ),
98 ),
99 Padding(
100 padding: const EdgeInsets.only(
101 left: 15.0, right: 15.0, top: 15, bottom: 0),
102 //padding: EdgeInsets.symmetric(horizontal: 15),
103 child: TextField(
104 obscureText: true,
105 onChanged: (value) {
106 setState(() {
107 _password = value;
108 });
109 },
110 decoration: const InputDecoration(
111 border: OutlineInputBorder(),
112 labelText: 'Password',
113 hintText: 'Enter secure password'),
114 ),
115 ),
116 const SizedBox(
117 height: 50,
118 ),
119 Container(
120 height: 50,
121 width: 250,
122 decoration: BoxDecoration(
123 color: Colors.blue, borderRadius: BorderRadius.circular(5)),
124 // ignore: deprecated_member_use
125 child: TextButton(
126 onPressed: () => _login(),
127 child: const Text(
128 'Login',
129 style: TextStyle(color: Colors.white, fontSize: 16),
130 ),
131 ),
132 ),
133 const SizedBox(
134 height: 130,
135 ),
136 // ignore: deprecated_member_use
137 TextButton(
138 onPressed: () {
139 // navigate to the signup screen
140 Navigator.push(
141 context, MaterialPageRoute(builder: (_) => Signup()));
142 },
143 child: const Text(
144 'New user? Create Account',
145 style: TextStyle(fontSize: 14),
146 ),
147 ),
148 ],
149 ),
150 ));
151 }
152 }
From above:
email
and a password
field.Not to execute all these screens, edit the main.dart
file as below:
1 import 'package:flutter/material.dart';
2 import 'package:flutter_auth_project/screens/dashboard_screen.dart';
3 import 'package:flutter_auth_project/screens/login_screen.dart';
4 import 'package:flutter_auth_project/screens/signup_screen.dart';
5
6 void main() {
7 runApp(const Home());
8 }
9
10 class Home extends StatefulWidget {
11 const Home({Key? key}) : super(key: key);
12
13 @override
14 _HomeState createState() => _HomeState();
15 }
16
17 class _HomeState extends State<Home> {
18 @override
19 Widget build(BuildContext context) {
20 return MaterialApp(
21 title: "Strapi App",
22 home: const Login(),
23 routes: {
24 Dashboard.namedRoute: (ctx) => const Dashboard(),
25 Login.namedRoute: (ctx) => const Login(),
26 Signup.namedRoute: (ctx) => const Signup()
27 },
28 onGenerateRoute: (settings) {
29 return MaterialPageRoute(builder: (context) => const Dashboard());
30 },
31 onUnknownRoute: (settings) {
32 return MaterialPageRoute(builder: (context) => const Dashboard());
33 },
34 );
35 }
36 }
From above, we are:
Start the development server by running the below command:
flutter run
At first, you will be presented with the login screen:
Then, once you click that you do not have an account, you will be directed to the signup screen:
Upon signup or login, you will be directed to the dashboard screen:
Strapi is a great content manager for your application. It allows you to model and data and consumes it with a fronted framework of your choice. I hope this Flutter Strapi tutorial was helpful. Happy coding!
Joseph is fluent in Android Mobile Application Development and has a lot of passion for Fullstack web development. He loves to write code and document them in blogs to share ideas.