Flutter Authentication: Implementing User Signup and Login | LoginRadius Blog

../authentication/auth.dart

class Auth extends StatefulWidget {
@override
_AuthState createState() => _AuthState();
}

class _AuthState extends State<Auth> {
bool showSignUp = true;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Corey's Corner",
),
elevation: 16.0,
actions: [
IconButton(
icon: Icon(Icons.swap_horiz),
onPressed: () {
setState(() {

showSignUp = !showSignUp;
});
})
],
),
// ternary operator
body: Container(child: showSignUp ? SignUp() : SignIn()));
}
}

Adding authentication to a flutter app

In this section, you’ll learn how to secure a Flutter app with Auth0. You’ll take a production-ready Flutter app and add a login screen and logout functionality to it, and you’ll do it with only a fraction of the effort required to implement login and logout yourself!

You’ll be able to follow this tutorial a little more smoothly if you know the basics of Flutter, but it’s not a hard requirement. If you have experience with any modern web framework, you’ll probably be able to understand the code and learn Flutter and Dart (Flutter’s programming language) as you go.

Analyzing an example

I’ve taken an example of a JWT generated by the backend we’ll build as an example in this post. It is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNTgxOTY2MzkxLCJleHAiOjE1ODMyNjIzOTF9.IDXKR0PknG96OyVgRf7NEX1olzhhLAiwE_-v-uMbOK0.

Building the ui

Your Flutter application will consist of four screens, which include:

  • Registration Screen
  • Login Screen
  • Home Screen

Let’s begin by building the Registration Screen.

Choosing how to sign the token

The token can be signed using either a method based on public key cryptography (for example using RSA or ECDSA) or by relying on hashing the concatenation of the secret key and the message (called a payload in JWT terms) with any hashing algorithm (usually sha256).

Похожее:  Управляйте своими финансами с легкостью. Получайте информацию и контролируйте баланс моего личного счета Beeline.

Configure auth0

The next step is to register MJ Coffee as an application in the Auth0 dashboard.

You’ll need an Auth0 account for this step. If you don’t already have one, you can sign up for a free account. The free tier is generous enough for many small applications.

🛠 Log in to into your Auth0 account and follow the steps below to register the application:

  • 🛠 Go to the Applications section of your dashboard:
  • 🛠 Click the Create Application button:
  • 🛠 Enter a name for your application (e.g., “MJ Coffee Flutter Application”) and select the Native application type:
  • 🛠 You’ll see the Quick Start page of your newly-registered application. Go to the Connections page…

Configure the callback url for android

🛠 To configure the Android version of the app, open the /android/app/build.gradle file.

Update the defaultConfig section of the file by adding a new item: manifestPlaceHolders and its value, [‘appAuthRedirectScheme’:’mj.coffee.app’]. The value of appAuthRedirectScheme must be in lower case letters.

🛠 You should set the value for minSdkVersion to at least 18, as it’s a requirement for the flutter_secure_storage package.

The result should look like this:

Configure the callback url for ios

The only change that you need to make in order to configure the iOS version of the app is to add a callback scheme.

🛠 To do this, open the /ios/Runner/Info.plist file. Inside the <dict> tag, add a new key, CFBundleURLTypes so that the start of the <dict> tag looks like this:



...
<dict><key>CFBundleURLTypes</key><array><dict><key>CFBundleTypeRole</key><string>Editor</string><key>CFBundleURLSchemes</key><array><string>mj.coffee.app</string></array></dict></array>
...

Creating a base api class:

Our first step is to build a BaseAPI class to hold all of the URL’s of our API. In my How To Make Flutter API Calls EasyI taught you how to use class inheritance as a means of simplifying and organizing your API calls. This class isn’t to complex it just stores the routes we will be requesting, check out the code below.

Creating a customer api class

Next we’re going to create a class to store all of the API calls for customer authentication.

Creating a customer object

When we create an object we are creating our own data type, we’re creating a blue print that outlines all the properties that each of our customers will have.

Creating a log-in page

The MaterialApp object we’re launching is called MyApp, but we’ll worry about that later, given that it needs to check whether we’re already logged in, and then choose whether to display a log-in page or the home page.

That’s a bit boring to worry about now, let’s build some UI and create a log-in page!

The log-in page itself will be a StatelessWidget called LoginPage:

The nature of the login page and our implementation means we are going to make extensive use of dialogs. This means we’ll create a helper method in order to make everything more succint:

Enabling login on the home screen

Now that you have the underlying methods for login and initial setup, it’s time to implement similar methods for the app’s screens, whose code is in the /lib/screens/ directory.

🛠 The app’s home screen is implemented in the HomeScreen class, located in /lib/screens/home.dart.

import'package:mjcoffee/services/auth_service.dart';

Now scroll past the HomeScreen class to the _HomeScreenState class. You’ll need to make some changes to this class.

🛠 The first set of changes is to the instance variables at the start of _HomeScreenState. Change them to the following:



    bool isProgressing =false;
    bool isLoggedIn =false;
    String errorMessage ='';
    String? name;

🛠 The initState() method is just below those variables. Right now, the only thing it does is call its counterpart in the superclass.

Replace the implement init action comments with a call to initAction(). The method should look like this:

voidinitState(){initAction();super.initState();}

You’ll implement initAction() shortly.

Get the project, configure it, and run it

🛠 Open the repository for the MJ Coffee app and download the source from the main branch. This contains a fully functioning app that is ready for you to add Auth0 authentication/authorization and chat.

🛠 If you want to build the app for iOS, you’ll need to specify your own development team for the build process. Open the /ios/Runner.xcworkspace/ file with Xcode, select the Runner project, then the Runner target, open the Signing & Capabilities tab, and select your team in the Team drop-down menu:

🛠 Confirm that the app works by running it. Open a command-line interface, navigate to the project’s root directory, and enter flutter run.

Flutter will compile the project and run it on any mobile device connected to your computer or any mobile device emulator running on it. If it can’t find any of those, it will run a mobile device emulation in a browser window.

You will see the app’s home screen:

Implementation

The Flutter app doesn’t need to be particularly complicated to be able to work with JWT: it’s mostly about writing an authentication flow, storing the JWT token and sending it with each request.

Integrating auth0 with flutter

Since Auth0 is a standard OAuth 2.0 authorization server, you can utilize any standard OpenID Connect SDK to authenticate against Auth0. One of them is flutter_appauth, a wrapper around the AppAuth SDK for native applications. You will need to integrate it into your application.

🛠 Open the /lib/services/auth_service.dart file and update it to import the necessary libraries as well as instantiate FlutterAppAuth and FlutterSecureStorage:

Layers of sessions

Every login requires logout! It’s more complicated than it looks since there are typically three-session layers you need to consider:

Logging in

🛠 If you’ve made it this far, you’ve done well, and it’s now time to see what you’ve achieved so far. Make sure your emulators or devices are active and stop any earlier versions of this app. Once you’ve done that, run the app using this command:

flutter run -d all --dart-define=AUTH0_DOMAIN=[YOUR DOMAIN] --dart-define=AUTH0_CLIENT_ID=[YOUR CLIENT ID]

Once the app is loaded, tap on the “Login | Register” button.

On iOS, when you run the app for the first time, you will see a prompt like this:

Our api interface

Our backend is going to have three routes:

Prerequisites

If you wish to follow along with this tutorial, you must have the following set up:

This tutorial is verified with Flutter v2.5.1 and Android Studio v3.5.

Project structure

The project is being structured in this order:

  • Core (contains a class that handles network requests)
  • Screens (the UI of our app)
  • Utils (helper components)

Provide the domain and client id to the app

You will need to use the domain and client ID that you copied from the Settings page in your Flutter application. You can either store these values in constant variables in the app’s code, or you can pass these values to the app by providing them as –dart-define arguments when you run it.

Rather than store this sensitive information in your code (which is a big security risk), I suggest that you supply the app with these values as –dart-define arguments when you run it.

🛠 To do this in Terminal or PowerShell, use this command:

flutter run -d all --dart-define=AUTH0_DOMAIN={YOUR DOMAIN} --dart-define=AUTH0_CLIENT_ID={YOUR CLIENT ID}

You can optionally have your editor of choice provide these values. For example, you can have Visual Studio Code pass these additional –dart-define values by adding them to the args field of your launch configuration file (/.vscode/launch.json):

Query parameters

The queries can contain parameters, which can optionally be passed as an array, so they can replace ? characters found in the query string. This will sanitize the strings before substituting them, not making you vulnerable to SQL injection. For example, the following two calls to db.each are equivalent:

Registration screen

The RegistrationScreen has two TextFormField widgets that serve as our email and password fields, as well as an ElevatedButton to handle event submission, as shown in the code snippet below from the register.dart file.

 //...    @override    Widget build(BuildContext context) {      Scaffold(          backgroundColor: Colors.blueGrey[200],          body: Form(            key: _formKey,            child: SizedBox(              width: size.width,              height: size.height,              child: Align(                alignment: Alignment.center,                child: Container(                  width: size.width * 0.85,                  padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),                  decoration: BoxDecoration(                    color: Colors.white,                    borderRadius: BorderRadius.circular(20),                  ),                  child: SingleChildScrollView(                    child: Column(                      crossAxisAlignment: CrossAxisAlignment.start,                      children: <Widget>[                        const Center(                          child: Text(                            "Register",                            style: TextStyle(                              fontSize: 30,                              fontWeight: FontWeight.bold,                            ),                          ),                        ),                        SizedBox(height: size.height * 0.05),                        TextFormField(                          validator: (value) =>                              Validator.validateEmail(value ?? ""),                          controller: emailController,                          keyboardType: TextInputType.emailAddress,                          decoration: InputDecoration(                            hintText: "Email",                            isDense: true,                            border: OutlineInputBorder(                              borderRadius: BorderRadius.circular(10),                            ),                          ),                        ),                        SizedBox(height: size.height * 0.03),                        TextFormField(                          obscureText: _showPassword,                          validator: (value) =>                              Validator.validatePassword(value ?? ""),                          controller: passwordController,                          keyboardType: TextInputType.visiblePassword,                          decoration: InputDecoration(                            hintText: "Password",                            isDense: true,                            border: OutlineInputBorder(                              borderRadius: BorderRadius.circular(10),                            ),                          ),                        ),                        SizedBox(height: size.height * 0.06),                        Row(                          mainAxisAlignment: MainAxisAlignment.center,                          children: [                            Expanded(                              child: ElevatedButton(                                onPressed: _handleRegister,                                style: ElevatedButton.styleFrom(                                    primary: Colors.indigo,                                    shape: RoundedRectangleBorder(                                        borderRadius: BorderRadius.circular(10)),                                    padding: const EdgeInsets.symmetric(                                        horizontal: 40, vertical: 15)),                                child: const Text(                                  "Register",                                  style: TextStyle(                                    fontSize: 20,                                    fontWeight: FontWeight.bold,                                  ),                                ),                              ),                            ),                          ],                        ),                      ],                    ),                  ),                ),              ),            ),          ),        );    }    //...

In the onPressed callback of the ElevatedButton widget, you’ll handle the validation of your form data.

Run instructions

Ensure a supported device is connected or emulator/simulator is started

Go to project directory

Use flutter run command to run

flutter run

It builds and runs app on an available android/ios device

Step 1: create a new flutter project

Create a new Flutter project and navigate to the folder of the application by running the following commands in your terminal:

flutter create loginradius_examplecd loginradius_example

Step 3: setting up the api client class

Create a new dart file named api_client.dart and import the dio package into the file, as follows:

import 'package:dio/dio.dart';

Take a quick tour of the app

🛠 Tap the Login | Register button. Right now, there is no login functionality, so the app immediately takes you to the Menu screen:

What you’ll learn and build

While you could create a new Flutter project and implement everything you will learn in this tutorial, adding authentication to an existing production-ready app is pretty common. I’ll provide a production-ready app, MJ Coffee, which you’ll secure by adding authentication.

Why use loginradius?

LoginRadius offers:

So, with everything out of the way, let’s get started.

Небольшая заметка

У новичков могут возникнуть проблемы даже с готовым кодом. И это не издевательство, такое бывает.

Поэтому для 100%-ной работы коды постарайтесь использовать схожие версии Flutter и Dart с моими:

Также в комментах я обратил внимание на null safety. Это очень важно, я позабыл об этом и это мой косяк.

Я уже добавил в приложение поддержку null safety. Вы наверно обратили внимание на восклицательный знак:

// ! указывает на то, что мы 100% уверены
// что currentState не содержит null значение
_formKey.currentState!.validate()

О null safety и о её поддержи в Dart можно сделать целый цикл статей, а возможно и написать целую книгу.

Мы задерживаться не будем и переходим к созданию POST запроса.

Создание формы: добавление поста

Для начала добавим на нашу страницу HomePage кнопку по которой мы будем добавлять новый пост:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Post List Page"),
    ),
    body: _buildContent(),
    // в первой части мы уже рассматривали FloatingActionButton
    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.add),
      onPressed: () {

      },
    ),
  );
}

Далее создадим новую страницу в файле post_add_page.dart:


import 'package:flutter/material.dart';

class PostDetailPage extends StatefulWidget {
  @override
  _PostDetailPageState createState() => _PostDetailPageState();
}

class _PostDetailPageState extends State<PostDetailPage> {
  
  // TextEditingController'ы позволят нам получить текст из полей формы
  final TextEditingController titleController = TextEditingController();
  final TextEditingController contentController = TextEditingController();
  
  // _formKey пригодится нам для валидации
  final _formKey = GlobalKey<FormState>();
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Post Add Page"),
        actions: [
          // пункт меню в AppBar
          IconButton(
            icon: Icon(Icons.check),
            onPressed: () {
              // сначала запускаем валидацию формы
              if (_formKey.currentState!.validate()) {
                // здесь мы будем делать запроc на сервер
              }
            },
          )
        ],
      ),
      body: Padding(
        padding: EdgeInsets.all(15),
        child: _buildContent(),
      ),
    );
  }
  Widget _buildContent() {
    // построение формы
    return Form(
      key: _formKey,
      // у нас будет два поля
      child: Column(
        children: [
          // поля для ввода заголовка
          TextFormField(
            // указываем для поля границу,
            // иконку и подсказку (hint)
            decoration: InputDecoration(
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.face),
                hintText: "Заголовок"
            ),
            // не забываем указать TextEditingController
            controller: titleController,
            // параметр validator - функция которая,
            // должна возвращать null при успешной проверки
            // или строку при неудачной
            validator: (value) {
              // здесь мы для наглядности добавили 2 проверки
              if (value == null || value.isEmpty) {
                return "Заголовок пустой";
              }
              if (value.length < 3) {
                return "Заголовок должен быть не короче 3 символов";
              }
              return null;
            },
          ),
          // небольшой отступ между полями
          SizedBox(height: 10),
          // Expanded означает, что мы должны
          // расширить наше поле на все доступное пространство
          Expanded(
            child: TextFormField(
              // maxLines: null и expands: true 
              // указаны для расширения поля на все доступное пространство
              maxLines: null,
              expands: true,
              textAlignVertical: TextAlignVertical.top,
              decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Содержание",
              ),
              // не забываем указать TextEditingController
              controller: contentController,
              // также добавляем проверку поля
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return "Содержание пустое";
                }
                return null;
              },
            ),
          )
        ],
      ),
    );
  }
}

Не забудьте добавить переход на страницу формы:

floatingActionButton: FloatingActionButton(
   child: Icon(Icons.add),
   onPressed: () {
      Navigator.push(context, MaterialPageRoute(
         builder: (context) => PostDetailPage()
      ));
   },
),

Запускаем и нажимаем на кнопку:

Вуаля! Форма работает.

Заключение

Я надеюсь, что убедил вас в том, что работа с формами на Flutter очень проста и не требует почти никаких усилий.

Большая часть кода – это создание POST запроса на сервер и обработка ошибок.

Полезные ссылки

Всем хорошего кода)

Installing dependencies

As always, create a Node project with

$ npm init

and install the packages I listed above with

$ npm install --save express jsonwebtoken sqlite3

Building the back-end

These are the libraries we are going to use:

Install flutter dependencies

The first step is to import the required libraries. You’ll do that by specifying three new dependencies:

You’ll install them by adding entries to the project’s /pubspec.yaml file (located in the root directory), where dependencies are specified.

🛠 Add the following lines to the /pubspec.yaml’s dependencies: section, just after the line that starts with json_annotation:

Handling the app’s initial state

The only thing missing is handling the authentication state when the app is launched. You might want to be able to silently login and retrieve a new Access Token if a Refresh Token is available.

🛠 Let’s add a new method, init(), to deal with the app’s initial state. Implement this method by adding the following to AuthService:



  Future<bool>init()async{final storedRefreshToken =await secureStorage.read(key: REFRESH_TOKEN_KEY);if(storedRefreshToken ==null){returnfalse;}try{final TokenResponse? result =await appAuth.token(TokenRequest(
          AUTH0_CLIENT_ID,
          AUTH0_REDIRECT_URI,
          issuer: AUTH0_ISSUER,
          refreshToken: storedRefreshToken,),);final String setResult =await_setLocalVariables(result);return setResult =='Success';}catch(e, s){print('error on Refresh Token: $e - stack: $s');returnfalse;}}

Implementing the back-end with node

You can find the code for this Node backend on GitHub by clicking here.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *