DEV Community

loading...

Authentication System in Flutter with Laravel Passport + Stacked (State Management)

Carlo Miguel Dy
Software Engineer
Updated on ・7 min read

Introduction

In this article I will be sharing how I implement an authentication system in Flutter, you can use any back-end framework that implements a token-based API authentication. In this tutorial I will be using Laravel Passport for handling the API authentication. So the pre-requisites for this tutorial are as follows:

  • Basic Knowledge of REST APIs
  • Understanding of how token-based authentication works
  • Basic knowledge in Flutter
  • Basic understanding of the stacked architecture by FilledStacks
  • Dependency injection with get_it and injectable package

Source code will be available from the repository.

If you have these pre-requisites then you wouldn’t have a hard time in following what I am about to share. And I am not gonna go through setting up the server in this article, I will assume that you have already set up this part. So if you haven’t already, go and set up your server that implements a token-based authentication.

Getting Started

To start things off let’s install the dependencies that we need to set up the stacked architecture.

pubspec.yaml

pubspec.yaml

Go to your pubspec.yaml file and add these dependencies below and run flutter pub get in your terminal.

# data
dio: ^3.0.9
json_serializable: ^3.3.0
json_annotation: ^3.0.1
shared_preferences: ^0.5.8
# state
stacked: ^1.7.3+2
# inversion of control
injectable:
observable_ish:
get_it: ^4.0.2
Enter fullscreen mode Exit fullscreen mode

And once that is done, let us clean up the main.dart file and structure our project that follows the stacked architecture convention.

File Structure

When you are using the stacked architecture, make sure you follow the conventional way of doing things as it would make things easier for you in the long term. However if you think you have a better way of organizing your code base then it’s fine.

lib/
 # here we put our locator.dart which handles the dependency 
 # injection and probably some helper class; basically just 
 # anything what our app needs
 - app/
   - locator.dart
   # this is my helper class for HTTP requests 
   # where I set up the baseURL for the API
   - http_client.dart
 # models are housed in this directory
 - datamodels/ 
   - user.dart
   - auth_response.dart
 # all of the services are housed in this directory 
 - services/ 
   - authentication_service.dart
 # all of our UI files are placed in this directory
 - ui/ 
   - ui/views
     - home/
       - home_view.dart
       - home_viewmodel.dart
Enter fullscreen mode Exit fullscreen mode

The file structure (/lib directory)

The file structure (/lib directory)

App Directory

If you are following along, go ahead and create those files. From the screenshot shown it would show that there are errors in the code, and that is true. Since it is expecting a file that gets auto generated via build_runner package, if you used this in the past then you would understand why there is an error indicator.
We start by putting code inside the locator.dart file.

lib/app/locator.dart

lib/app/locator.dart

We will be injecting the AuthenticationService via injectable package using the @lazySingleton annotation from the package where it would automatically handle registering of the singleton with-in the setupLocator() method.

Then also write up the code for our HTTP helper where we define a constant for the API_BASE_URL and the instance of the Dio HTTP client.

lib/app/http_client.dart

lib/app/http_client.dart

Define the Models

Create a User model and an AuthenticationResponse model, there we define the properties we expect to receive from our API. I will be using the json_serializable package so I don’t have to write boilerplate code. We also have to put part 'model.g.dart' because it is required by the package as it’s how it recognizes it.

user.dart

user.dart

auth_response.dart

auth_response.dart

Views

Then cleaning up our main.dart, because we don’t need the Counter App. And make sure to call setupLocator() before we initialize our app, so the services that we registered is readily available.

lib/main.dart

After that create the home_view.dart and home_viewmodel.dart files that are found in the following directory:

  • lib/ui/views/home/home_view.dart
  • lib/ui/views/home/home_viewmodel.dart

Then let’s write some basic code with the stacked architecture in our HomeView and HomeViewModel.

lib/ui/views/home/home_view.dart

lib/ui/views/home/home_viewmodel.dart

Inside our HomeViewModel class we extend the ReactiveViewModel so that allows us to get the current state from our AuthenticationService class.

Authentication Service

Next is write code for the AuthenticationService class and from there we define the relevant methods to handling the token based authentication. Just the usual token-based authentication pattern, we utilize the local storage using the shared_preferences package then that allows to create an auto login functionality when the user re-opens the app so our user does not have to re sign-in every time he opens our app.

Create the class and use the ReactiveServiceMixin from the stacked package and register this class as a lazy singleton using the @lazySingleton annotation from the injectable package so that our locator would be aware the existence of this class and their state.

Now let’s define the reactive properties inside the AuthenticationService class and also instantiate SharedPreferences and assign it in a private property named “_localStorage”. And also a string constant for the key name that we’ll be using in the local storage.

We are using the observable_ish package since it handles the persistence of our state inside our AuthenticationService and we initialize a value so it does not cause us errors. And also define getters for it and yes we have to access the value property to get the actual value and not the class instance itself.

These are the following methods available inside the AuthenticationService:

  • Future loginWithEmail({@required String email, @required String password})
  • Future fetchUser()
  • Future logout()
  • Options get authorizationHeader
  • void setToken(String token)
  • void deleteToken()
  • void handleError(DioError error)

Future loginWithEmail({@required String email, @required String password})

This method is asynchronous and it takes 2 named parameters which will be the user’s email and their password. These are string and are required, we use the @required annotation from the flutter/material package.

Future fetchUser()

This method is also asynchronous where it fetches the current authenticated user and it sets the state inside our AuthenticationService class. By fetching the current authenticated user in our API, it requires the access token. So the best way to fetch the user is right after our user is authenticated.

Future logout()

This method is asynchronous too, because we want the request in the API to be successfully before we delete the token from the state and local storage.

Options get authorizationHeader

This will be the header where we put our Bearer token in a Map, so we don’t have to hard code the Bearer token in every time we make an API call that requires authentication. And make sure you also pass the “accept”: “application/json” because our API returns us JSON responses.

void setToken(String token).

An asynchronous function, because we have to initialize the SharedPreferences class before we can use it. And it takes in a string parameter, which is our access token then we save it in local storage and as well as set the state inside the authentication service since our authorizationHeader getter is dependent on it.

void deleteToken()

Also an asynchronous function since it needs to use the SharedPreferences class as well. Then basically just delete the token from local storage and remove it from the state also the user. So that our view models who are listening to the state of this class would be able to react to changes when our token and user have logged out.

void handleError(DioError error)

Since the nature of asynchronous functions would require to use try-catch blocks, this handles all our authentication related errors so we avoid the DRY principle. Inside this function is a switch case where we may be able to throw an error message by using a dialog service or a snackbar service from stacked_services package. But for the simplicity of this tutorial, let’s just print out the error messages.

Generate Code

Right after we define the code that we want to auto-generate for us, run flutter pub run build_runner build from your terminal and let it auto generated the code for us.

After that let’s just create a form that accepts the input, then on submit we will call the method loginWithEmail.

Let’s update the HomeViewModel.

And in our HomeView,

build method

_loginForm (part 1)

_loginForm (part 2)

_loggedInWidget

Output

When we log in, and it is a success, let’s just display the token. And logging out would get back to login form.

Thanks for reading. I hope this tutorial helps you if you are using a JWT auth in your API. Cheers!

Discussion (2)

Collapse
dmsherazi profile image
Dost Muhammad Shah

Can you provide link to the repo

Collapse
carlomigueldy profile image
Carlo Miguel Dy Author

Hi, here is the link of the repo.

github.com/carlomigueldy/flutter-j...