Implementing authentication feature for chipIn using Appwrite - Appwrite & Flutter: Event app tutorial

Introduction

In the previous blog post, we embarked on our journey with Flutter and Appwrite by setting up Appwrite for our application, chipIn. We registered on Appwrite Cloud, explored its intuitive sections, created our first Appwrite project, and finally integrated Appwrite with our Flutter application.

We are ready to take the next important step in developing our event planning and management app, chipIn. In this blog post, we will focus on implementing one of the most crucial features of any app - Authentication. Our goals will be to set up user registration and login features and implement OAuth using GitHub. By the end of this blog, you'll understand how to implement authentication in your Flutter apps using Appwrite, including registration, login, and OAuth integrations. Let's proceed!

Why implement authentication

Authentication is one of the most fundamental aspects of any application that requires user interaction. It's not just about security. It also plays a significant role in delivering personalized user experiences. Let's take a closer look at its importance:

  1. Security: Authentication ensures that only authorized users gain access to their accounts in your app.

  2. Personalization: Once a user is authenticated, you can tailor the app experience based on their preferences, history, and more.

  3. Data Integrity: With authentication, you can ensure that changes made to data in your app (such as creating or editing events in chipIn) are done by authenticated and authorized users.

It becomes clear why implementing authentication is essential to integrate into my app. Appwrite helps me manage user logins and registration; we'll look further into that aspect.

Overview of Appwrite's authentication feature

Whether we are developing a mobile app or a web application, Appwrite has got us covered. Let's delve into the key features that I find helpful when building the chipIn app:

  1. Simplicity: Appwrite's authentication system simplifies integrating security into my application. It provides me with a straightforward set of API endpoints for all the everyday authentication tasks, such as registration, login, verification, password recovery, and much more. We will work on that later in this blog.

  2. OAuth2 Support: In addition to traditional email-password-based authentication, Appwrite supports OAuth2. I offer users the convenience of choosing between creating a new account or logging in with their existing accounts from popular providers such as Facebook, Google, Github, and others. This provides an additional layer of security for them. I would always go for an OAuth signup process whenever an option is available. It's a convenient and quick way to register/log in, and that is an experience that I want my users to have when using this app.

  3. And more!

Again, these are just some of the features I find helpful for my app. If you like to know more, visit the Appwrite Documentation - Authentication.

Setting things up

Exploring the codebase structure: Your app as a house

To better understand the inner workings of our event planning and management app, let's take a closer look at the codebase structure. Imagine your app as a house with different rooms serving specific purposes. By visualizing it this way, we can simplify our understanding of how the codebase is organized and how each component contributes to the app's overall functionality.

lib/
├── appwrite
│   ├── appwrite_constants.dart
│   └── appwrite_service.dart
├── features // <-- New folder added and below are the sub folders and files
│   ├── authentication
│   │   ├── controller
│   │   │   └── auth_controller.dart
│   │   ├── services
│   │   │   ├── auth_service.dart
│   │   │   └── oauth_service.dart
│   │   └── views
│   │       ├── auth_main_view.dart
│   │       ├── login_view.dart
│   │       └── signup_view.dart
│   └── home_page.dart
└── main.dart

Let me explain the newly added folder in our Flutter project's codebase structure, specifically inside the lib/ folder.

  1. features folder: Different rooms in a house

    • Like a house has different rooms, our app is divided into "features," each dedicated to specific functionality.

    • The "features" folder in the codebase represents these different rooms. For example, our app includes features like event management, notifications, and user authentication, each residing in its own "room" within the codebase.

  2. authentication sub-folder: Managing who can enter the house

    • Consider authentication a room dedicated to managing who can enter the house.

    • The "authentication" feature handles user access and security. It ensures that only authorized users can enter and use the app. In our analogy, this feature is like a room dedicated to managing visitors and granting access.

  3. controller sub-folder of authentication: The Person Responsible for Managing Access

    • Just as a house might have a person responsible for managing who can enter, the controller takes charge of user access in the app.

    • The "controller" file, such as auth_controller.dart, acts as the person responsible for managing user access. It receives commands from the user interface and determines what actions to take based on those commands.

  4. services sub-folder of authentication: Assisting in managing who can enter

    • Just as services might assist in managing access to a house, the services in the codebase help manage user access.

    • The "services" folder represents different services that assist in managing user access. For example, the auth_service.dart file provides functions and methods related to authentication, ensuring a smooth user experience.

  5. views sub-folder of authentication: Rooms with windows, providing a view of the entrance

    • Imagine different rooms in a house with windows, giving a view of the entrance. Similarly, views allow users to interact with the authentication process.

    • The "views" folder contains screens or views that users interact with during authentication. For instance, the login_view.dart and signup_view.dart files represent rooms with windows where users can enter their login or signup details.

Go ahead and create these folders and files in your Flutter project.

Adding a platform for your Appwrite project

The Add a Platform section in the Appwrite Cloud Console lets you connect different types of applications or services to your app's backend. You can add a Web App for a browser-based app, a Flutter App for a mobile app, an Apple App for iOS devices, or an Android App for Android devices. We are going to choose Flutter app because that is the technology we are using the build the app. Here the complete steps for you to follow:

  1. Choose the Flutter App option

  2. Register your Flutter app

    • This is the place where you can register your Flutter app and choose the platform you want to target which is a way of telling Appwrite which device type you want your app to work on.

    • The Name field is where you can give your app a unique and descriptive name, such as "chipIn Android". This name will be displayed in the Appwrite Console and will help you identify your app among other projects.

    • The Package Name field is where you can specify the identifier of your app, which is used by the platform to recognize your app. The package name must follow a specific format and be unique across all apps on the same platform.

      • For example, on Android, the package name usually starts with "com." followed by your company name and then your app name, such as "com.example.chipIn".

      • On iOS, the package name is also called the bundle identifier and usually follows a reverse domain name notation, such as "io.appwrite.chipIn".

    • If you are using a Windows machine, you can choose Android App for now. If you are using macOS, you can choose iOS. Either way, you can still add more target platforms in the future. In my case, I am going to use the iOS platform.

  3. Next > Next > Next > Take me to my Dashboard

    • Click the Next button three times until you see the Take me to my dashboard button. We have already added Appwrite as a dependency to our Flutter project so there's no need to do this part (see our previous blog).

It's time for coding

Launching our device emulator

The Device Emulator in Flutter is like my virtual playground where I can test my app on different types of devices without actually having to use each one. It's like trying on different outfits before deciding which one to wear, so you can see how you look in each one and choose the best one. With the Device Emulator, you can make sure you app works well on all kinds of devices and platforms, just like how you would want your outfit to look good in any setting. To launch it, here are the steps:

  1. Open the Command Pallete by going to the settings and selecting "Command Pallete"

  2. Type Flutter: Launch Emulator

  3. Choose which device emulator you prefer and wait for it to launch

  4. Wait for it to load

Create a UI for the authentication main view, login view, and signup view

I will provide you with the code for each dart files to create the UI.

auth_main_view.dart

import 'package:flutter/material.dart';
import 'login_view.dart';
import 'signup_view.dart';

class AuthMainView extends StatefulWidget {
  const AuthMainView({super.key});

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

class _AuthMainViewState extends State<AuthMainView> {
  int _selectedIndex = 0;

  static final List<Widget> _widgetOptions = <Widget>[
    LoginView(),
    SignUpView(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'chipIn',
          style: TextStyle(
            fontSize: 32,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      body: Center(
        child: _widgetOptions.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.login),
            label: 'Login',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_add),
            label: 'Sign Up',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
    );
  }
}

This code represents the main authentication screen in a Flutter app. It includes a bottom navigation bar that allows users to switch between the login and sign-up views. The selected view is displayed in the center of the screen.

login_view.dart

import 'package:flutter/material.dart';

class LoginView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Form(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // Add your logic here soon. UI for now
              },
              child: const Text('Login with GitHub'),
            ),
            const SizedBox(height: 16),
            const Divider(thickness: 1),
            const SizedBox(height: 16),
            TextFormField(
              decoration: const InputDecoration(
                hintText: 'Email',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your email';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),
            TextFormField(
              obscureText: true,
              decoration: const InputDecoration(
                hintText: 'Password',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your password';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              child: const Text('Login'),
              onPressed: () async {
                // Add your logic here soon. UI for now
              },
            ),
          ],
        ),
      ),
    );
  }
}

This code represents the login view in a Flutter app. It includes input fields for email and password, a "Login with GitHub" button, and a "Login" button. Users can enter their credentials and choose to log in with GitHub. Validations are performed before submitting the login form.

signup_view.dart

import 'package:flutter/material.dart';

class SignUpView extends StatelessWidget {
  SignUpView({Key? key});

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                // Add your logic here
              },
              child: const Text('Sign Up with GitHub'),
            ),
            const SizedBox(height: 16),
            const Divider(thickness: 1),
            const SizedBox(height: 16),
            Form(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  TextFormField(
                    decoration: const InputDecoration(
                      hintText: 'First Name',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your first name';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    decoration: const InputDecoration(
                      hintText: 'Last Name',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your last name';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    decoration: const InputDecoration(
                      hintText: 'Email',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your email';
                      }
                      if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
                          .hasMatch(value)) {
                        return 'Please enter a valid email address';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    decoration: const InputDecoration(
                      hintText: 'Username',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your username';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    decoration: const InputDecoration(
                      hintText: 'Password',
                    ),
                    obscureText: true,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter a password';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    decoration: const InputDecoration(
                      hintText: 'Confirm Password',
                    ),
                    obscureText: true,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please confirm your password';
                      }
                      if (value != '') {
                        return 'Passwords do not match';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    child: const Text('Sign Up'),
                    onPressed: () async {
                      // Add your logic here
                    },
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

This code is for the sign-up view in a Flutter app. It includes input fields for first name, last name, email, username, and password. Users can sign up with their information and choose to sign up with GitHub. Form validation is performed before submitting the sign-up form.

home_page.dart

import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Welcome to the HomePage!'),
      ),
    );
  }
}

This code represents the home page of a Flutter app. It includes a scaffold with an app bar that displays the title "Welcome to the HomePage!". When the user successfully creates an account or logs in, they should be routed to this screen.

auth_controller.dart

import 'package:chipin_blogpost/features/authentication/services/auth_service.dart';
import 'package:chipin_blogpost/features/home_page.dart';
import 'package:flutter/material.dart';

class LoginController {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  Future<void> login(BuildContext context) async {
    final email = emailController.text;
    final password = passwordController.text;

    try {
      await AuthService.createSession(context, email, password);

      emailController.text = '';
      passwordController.text = '';

      Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) => const HomePage(),
        ),
      );

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Logged in successfully')),
      );
    } catch (error) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error logging in: $error')),
      );
    }
  }
}

class SignUpController {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  final TextEditingController firstNameController = TextEditingController();
  final TextEditingController lastNameController = TextEditingController();
  final TextEditingController emailController = TextEditingController();
  final TextEditingController usernameController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  final TextEditingController confirmPasswordController =
      TextEditingController();

  Future<void> signUp(BuildContext context) async {
    final firstname = firstNameController.text;
    final lastname = lastNameController.text;
    final email = emailController.text;
    final username = usernameController.text;
    final password = passwordController.text;

    try {
      await AuthService.createUser(
        firstname,
        lastname,
        username,
        email,
        password,
      );

      firstNameController.text = '';
      lastNameController.text = '';
      emailController.text = '';
      usernameController.text = '';
      passwordController.text = '';
      confirmPasswordController.text = '';

      const snackBar = SnackBar(
        content: Text('Account Created!'),
      );
      ScaffoldMessenger.of(context).showSnackBar(snackBar);

      await AuthService.createSession(context, email, password);

      Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) => const HomePage(),
        ),
      );
    } catch (error) {
      final snackBar = SnackBar(
        content: Text('Error creating account: $error'),
      );
      ScaffoldMessenger.of(context).showSnackBar(snackBar);
    }
  }
}

The LoginController and SignUpController classes handle the login and sign-up processes in a Flutter app. Upon success, they retrieve user input, call authentication methods, and navigate to the home page. Snackbar messages are displayed for feedback.

auth_service.dart

import 'package:appwrite/models.dart';
import 'package:chipin_blogpost/appwrite/appwrite_service.dart';
import 'package:flutter/material.dart';
import 'package:appwrite/appwrite.dart';

class AuthService {
  static final Client client = AppwriteService.getClient();
  static final Account account = Account(client);
  static final Databases databases = Databases(client);

  // * SIGN UP METHOD
  static Future<void> createUser(String firstname, String lastname,
      String username, String email, String password) async {
    try {
      await account.create(
        name: '$firstname $lastname',
        userId: username,
        email: email,
        password: password,
      );

      print('User created successfully');
    } catch (error) {
      print('Error creating user: $error');
      throw error;
    }
  }

  // * LOGIN METHOD
  static Future<void> createSession(
      BuildContext context, String email, String password) async {
    try {
      await account.createEmailSession(
        email: email,
        password: password,
      );
      print('Session created successfully');
    } catch (error) {
      print('Error creating session: $error');
      throw error;
    }
  }

  // * This method retrieves the currently logged in user's ID
  static Future<String> getCreatorId() async {
    try {
      // Use the get method of the account object to retrieve the currently logged in user's details
      User response = await account.get();
      return response.$id; // Return the user's ID
    } catch (e) {
      print('Failed to get user ID: $e');
      throw e;
    }
  }

  // * LOGOUT METHOD
  static Future<void> logout() async {
    try {
      // Use the deleteSession method of the account object to delete the current session
      await account.deleteSession(sessionId: 'current');
      print('Logged out successfully');
    } catch (e) {
      print('Failed to logout: $e');
      throw e;
    }
  }

// * This method retrieves the currently logged in user's details and returns their name
  static Future<String> getUserName() async {
    try {
      // Use the get method of the account object to retrieve the currently logged in user's details
      User response = await account.get();
      return response.name; // Return the user's name
    } catch (e) {
      print('Failed to get user name: $e');
      throw e;
    }
  }
}

The AuthService class handles user authentication and related operations in a Flutter app. It provides methods for creating user accounts, creating sessions, logging out, and retrieving user information such as the user's ID and name. These methods utilize the Appwrite SDK and AppwriteService to interact with the Appwrite backend service.

oauth_service.dart

import 'package:chipin_blogpost/appwrite/appwrite_service.dart';
import 'package:chipin_blogpost/features/home_page.dart';
import 'package:flutter/material.dart';
import 'package:appwrite/appwrite.dart';

class OAuthService {
  static final Client client = AppwriteService.getClient();
  static final Account account = Account(client);
  // * Function for GithHub OAuth
  static Future<void> initiateGithubOAuth(context) async {
    try {
      await account.createOAuth2Session(
        provider: 'github',
      );
      // Navigate to the home screen
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => const HomePage()),
      );
    } on AppwriteException catch (e) {
      print('Appwrite Exception: ${e.message}');
    } catch (e) {
      print('Unknown Exception: $e');
    }
  }
}

The OAuthService class handles GitHub OAuth authentication in a Flutter app. It initiates the authentication process by calling the createOAuth2Session method of the Account object from the Appwrite service with the 'github' provider. If successful, it navigates to the home page. Any exceptions are caught and appropriate error messages are printed.

What does it look like

After you used these codes for each respective files, your app should look something like these:

Let's test it

But first... Bundle ID to your XCode/AndroidManifest

This process is important as it make sure that Appwrite recognizes your device emulator through the Bundle ID. I remember that I took so much of my time debugging this because I was getting the same error in my debug console over and over and over again.

You need to add your Bundle ID to XCode/AndroidManifest. I will provide you with the steps for both iOS platform and Android platform.

For iOS

  1. Go to your appwrite console and click the iOS platform

  2. Copy the Bundle ID

  3. In your Flutter project, right-click on the iOS folder and select Open in XCode

  4. Replace the value in the Bundle Identifier field with the Bundle ID you copied from the Appwrite Console

  5. Replace the value in the Bundle Identifier field with the Bundle ID you copied from the Appwrite Console

  6. Once done, go ahead and quit the XCode

For Android

  1. You may need to create a new platform for that if you haven't already. Click the Add Platform > Select Flutter App

  2. Then choose the Android. You know the drill: Next > Next > Next > Take me to my Dashboard

  3. Go back to your Flutter Project, open the command palette, and search for build.gradle. Select the first option

  4. In the file, navigate to the applicationId section and replace it with the actual BundleId you created in Appwrite

  5. Then Save

That's about it for both iOS and Android.

Setup GitHub OAuth provider in Appwrite

As I've mentioned earlier, Appwrite allows you to authenticate users using their GitHub account through the GitHub OAuth2 provider.

Enabling the GitHub provider

Before you can use GitHub to authenticate users, you need to enable the provider in your Appwrite console. Here are the steps:

  1. Navigate to your Appwrite project

  2. Navigate to Auth > Settings

  3. Find and open the OAuth provider 4. In the GitHub OAuth2 Settings modal, use the toggle to enable the provider

    Don't close this modal; we'll need to create a GitHub OAuth app to complete this form.

Create GitHub OAuth app

To use GitHub OAuth with Appwrite, you must create an OAuth app on your GitHub account. You can do this by following the Creating an OAuth App guide from GitHub. When prompted to provide an Authorization callback URL, provide the URI found in the GitHub OAuth2 Settings modal from your Appwrite console.

After you've created your GitHub OAuth app, you can head back to your Appwrite console to complete the form in the GitHub OAuth2 Settings modal.

  • Find the Client ID in your GitHub OAuth app and provide this in the App ID field in the GitHub OAuth2 Settings modal from the Appwrite console.

  • Navigate to the Client Secrets section in your GitHub OAuth app and click Generate a new client secret.

  • Copy your new client secret and provide this in the App Secret field in the GitHub OAuth2 Settings modal from the Appwrite console.

  • Then hit Update

Great job!

Let's start testing

Creating an account

Perfect! Now, try to log in using the same credentials you created. You should be routed to a new screen.

GitHub OAuth

GitHub OAuth is up and running!

Conclusion

If you made it this far, great job! This blog post has taken us on a journey through implementing authentication in our Appwrite-powered application. We've covered the importance of authentication and Appwrite's authentication feature and walked you through setting up, coding, and testing the various authentication features. In the next blog post, we'll proceed to our next feature: Events. Talk to you there!

Additional resources and support

  1. Appwrite's Official Documentation: To understand more about Appwrite's various features, you can visit Appwrite's Official Documentation. Here, you'll find detailed explanations about every feature and how to use them effectively.

  2. Flutter's Official Documentation: Visit Flutter's Official Documentation for more details about Flutter and its features, which can help you while creating the Flutter project.

  3. Appwrite's Discord Channel: If you face any issues or have any queries regarding Appwrite, Appwrite's Discord Channel is the place to go. You'll find a healthy community ready to assist you.

  4. Flutter's Community: If you have questions or face difficulties related to Flutter, you can reach out to Flutter's community. Various channels are available, including a Google group, a subreddit, and a Discord channel.

Remember, there's no need to feel stuck. Appwrite's and Flutter's communities are friendly, informative, and ready to help newcomers to the technology. Happy coding!