how to implement Flutter Parse Back4App Authentication, securing routes & CRUD

Amit Shukla
6 min readJul 16, 2024

--

Service Delivery Management App

welcome back friends,

In our previous blog, we presented an expedited demonstration of the Ride or Service Delivery Management application, which utilizes the Flutter platform coupled with the Parse back-end service by Back4App.

You can access the complete source code here.

In case you prefer video blogs, here is a YouTube video tutorial.

If you are finding this blog for the first time, please visit previous blogs for a quick demo, about how to setup your project, build first hand UI pages. Implement Dark/Light Mode functionality, implement multilingual functionality, data model and dynamic data validators and implemented user authentication pages etc.

In today’s session, we are going to make our app functional by adding authentication process. We will setup user sign up, reset / forget password, login, login using social authentication and defining user settings pages, which eventually setup roles and privileges based on user profile. At last, we will also cover basics of CRUD operations on user transactions into database collections.

Before we move forward, kindly ensure that you have incorporated the parser server SDK and initialized it, as outlined in our previous blog post where we elaborated on setting up UI defaults and so on.

// Add the Parse SDK to your project dependencies running:
flutter pub add parse_server_sdk_flutter

signup & reset password

First, let’s establish initial guidelines for consistent API back-end interactions that include data insertion, to be adhered to in all subsequent calls.

These rules may seem unnecessary and complex in the beginning, but pays off big later on.

1. never call back-end API directly (switch back-end API calls to back-end cloud functions).

2. authenticate and validate every single HTTP REST API call.

3. keep all back-end API calls in a separate directory/service folder.

4. never pass direct values in back-end API call functions, use data models instead.

// refer back to data model we created previously
class LoginDataModel {
String email;
String password;
LoginDataModel({required this.email, required this.password});
}
// a complete sample data model
class RideModel {
String objectId;
String uid;
String dttm;
String from;
String to;
String message;
String loadType;
String status;
String fileURL;
RideModel({required this.objectId, required this.uid,
required this.dttm, required this.from, required this.to,
required this.message, required this.loadType,
required this.status, required this.fileURL});
factory RideModel.fromJson(Map<String, dynamic> json) {
return RideModel(
objectId: json['objectId'],
uid: json['uid'],
dttm: json['dttm'],
from: json['from'],
to: json['to'],
message: json['message'],
loadType: json['loadType'],
status: json['status'],
fileURL: json['fileURL']);
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['objectId'] = objectId;
_data['uid'] = uid;
_data['dttm'] = dttm;
_data['from'] = from;
_data['to'] = to;
_data['message'] = message;]
_data['loadType'] = loadType;
_data['status'] = status;
_data['fileURL'] = fileURL;
return _data;
}
}

use data model to store values from `Form` and then pass these model to back-end API call.

// signup.dart
// first initialize data model in State and then define setData function to call backend
class SignUpState extends State<SignUp> {
..
final _formKey = GlobalKey<FormState>();
LoginDataModel model = LoginDataModel(email: 'noreply@duck.com', password: 'na',);
void setData(String loginType) async {
toggleSpinner();
var userAuth;
userAuth = await authBloc.signUpWithEmail(model);
if (userAuth.success) {
sendMessage("Congratulations, your account is created, please update your settings.");
showMessage(true, "success", "Account created, an email is sent to your email ID, please Reset your password using that link and login back.");
await Future.delayed(const Duration(seconds: 2));
// navigateToUser();
} else {
showMessage(true, "error", userAuth.error!.message);
}
toggleSpinner();
}
// afterwards, use model to store values on user input
// onChanged: (value) => model.email = value,
...
Container(
width: 300.0,
margin: const EdgeInsets.only(top: 25.0),
child: TextFormField(
controller: _emailController,
cursorColor: Colors.blueAccent,
keyboardType: TextInputType.emailAddress,
maxLength: 50,
obscureText: false,
// always use data mode
onChanged: (value) => model.email = value,
validator: (value) {
return Validators().evalEmail(value!);
},
decoration: InputDecoration(
icon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0)),
hintText: 'username@domain.com',
labelText: 'EmailID *',
// errorText: snapshot.error,
),
)),

now, call back-end API to set values while passing model values. PS-> this function is written in a separate file.

// blocs/auth.bloc.dart
signUpWithEmail(LoginDataModel model) async {
final username = model.email.trim();
final email = model.email.trim();
final password = model.email.trim();

final user = ParseUser(username, password, email);
var response = await user.signUp();
return response;
}

forgotPassword(String uname) async {
final ParseUser user = ParseUser(null, null, uname);
final ParseResponse parseResponse = await user.requestPasswordReset();
return parseResponse;
}

login, logout

similar to signup function, define actual login and logout back-end calls into `auth.block.dart` back-end service.

// auth.block.dart
logInWithEmail(LoginDataModel model) async {
final username = model.email.trim();
final password = model.password.trim();
final user = ParseUser(username, password, null);
var response = await user.login();
return response;
}

resetPassword() async {
var username = await getUser();
final ParseUser user = ParseUser(null, null, username?.get("username"));
final ParseResponse parseResponse = await user.requestPasswordReset();
return parseResponse;
}
logout() async {
ParseUser? currentUser = await ParseUser.currentUser() as ParseUser?;
if (currentUser == null) {
return false;
}
//Checks whether the user's session token is valid
final ParseResponse? parseResponse = await ParseUser.getCurrentUserFromServer(currentUser.sessionToken!);
if (parseResponse?.count == null || parseResponse!.count < 1) {
//Invalid session. Logout
return true;
} else {
await currentUser.logout();
return true;
}
}
}

now, define login function into login.dart file.

void login(String loginType) async {
toggleSpinner();
var userAuth;
userAuth = await authBloc.logInWithEmail(model);
if (userAuth.success) {
showMessage(true, "success",
AppLocalizations.of(context)!.cMsg1);
await Future.delayed(const Duration(seconds: 2));
navigateToUser();
} else {
showMessage(true, "error", userAuth.error!.message);
}
toggleSpinner();
}

securing routes

first we will need to define few functions which helps app get user valid sessions.

please see, not all of these functions are required, sometimes you just need to know if a user is signed in or not. other times, you want to know user name and `auth state`, and occasionally, you will need all user role level information.

Define these functions in your back-end `auth.bloc.dart` file and use it as appropriate.

Future<ParseUser?> getUser() async {
ParseUser? currentUser = await ParseUser.currentUser() as ParseUser?;
return currentUser;
}

isSignedIn() async {
ParseUser? currentUser = await ParseUser.currentUser() as ParseUser?;
if (currentUser == null) {
return false;
}
//Checks whether the user's session token is valid
final ParseResponse? parseResponse = await ParseUser.getCurrentUserFromServer(currentUser.sessionToken!);
if (parseResponse?.success == null || !parseResponse!.success) {
return false;
} else {
return true;
}
}

Future<List<ParseObject>> getUserType() async {
var username = await authBloc.getUser();
var uid = (username?.get("objectId") == null) ? "-" : username?.get("objectId");
QueryBuilder<ParseObject> parseQuery =
QueryBuilder(ParseObject("Settings"));
parseQuery.whereEqualTo('uid', uid);
parseQuery.orderByDescending('createdAt');
final ParseResponse apiResponse = await parseQuery.query();
if (apiResponse.success && apiResponse.results != null) {
return apiResponse.results as List<ParseObject>;
} else {
return [];
}
}

Implement logic for redirecting users with active sessions to dashboards or fetching roles to display specific user interfaces dynamically.

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

void loadAuthState() async {
final userState = await authBloc.isSignedIn();
setState(() => isUserValid = userState);
if (isUserValid) {
var username = await authBloc.getUserType();
if (username.isNotEmpty) {
setState(() => userType = username[0]["userType"]);
}
}
}
// then after, show appropriate container/pages based on user role or auth state
body: Material(
child: Container(
child: (isUserValid == true)
? settingsPage(context)
: userForm(context))));
....

CRUD

To get started, we can apply the same idea to CREATE or UPDATE user documents.

`Note:` We’ll improve this code later as it’s not suitable for production use yet.

To ensure our back-end is secure, we need to add authentication, user roles, and permissions. Additionally, we should authenticate every API request to keep our system safe.

For more information on implementing security measures, please see the linked blog post which focuses specifically on securing APIs.

Future<bool> setRide(String classId, model) async {
ParseObject data;
if (model.objectId == "-") {
data = ParseObject(classId)
// ..objectId = model.uid
..set('uid', model.uid)
..set('dttm', model.dttm)
..set('from', model.from)
..set('to', model.to)
..set('message', model.message)
..set('loadType', model.loadType)]
..set('status', model.status)
..set('fileURL', model.fileURL);
} else {
data = ParseObject(classId)
..objectId = model.objectId
..set('uid', model.uid)
..set('dttm', model.dttm)
..set('from', model.from)
..set('to', model.to)
..set('message', model.message)
..set('loadType', model.loadType)
..set('status', model.status)
..set('fileURL', model.fileURL);
}
final ParseResponse apiResponse = await data.save();
if (apiResponse.success && apiResponse.results != null) {
return true;
} else {
return false;
}
}

similarly, use this code to perform READ/DELETE

Future<List<ParseObject>> getDoc(String classId, String docId) async {
// userID
var username = await authBloc.getUser();
var uid = (username?.get("objectId") == null) ? "-" : username?.get("objectId");
QueryBuilder<ParseObject> parseQuery =
QueryBuilder(ParseObject(classId));
parseQuery.whereEqualTo('uid', uid);
parseQuery.whereEqualTo('objectId', docId);
parseQuery.orderByDescending('createdAt');
final ParseResponse apiResponse = await parseQuery.query();
if (apiResponse.success && apiResponse.results != null) {
return apiResponse.results as List<ParseObject>;
} else {
return [];
}
}

Future<void> delDoc(String classId, String docId) async {
var todo = ParseObject(classId)..objectId = docId;
await todo.delete();
}

Please do not forget to like and bookmark this article and subscribe to my YouTube and X accounts for more content.

--

--

Amit Shukla
Amit Shukla

Written by Amit Shukla

Build and Share Quality code for Desktop and Mobile App Software development using Web, AI, Machine Learning, Deep Learning algorithms. Learn, Share & Grow.

No responses yet