Newby Coder header banner

Flutter Jwt Authentication

Flutter Application with Jwt Authentication

JWT is short for Json Web Token, which is a quite popular implementation of authentication

A Json Web Token is a Json string sent from a server to a client(such as mobile app) typically after user login

The client then uses this token for subsequent requests to the server, to indicate the identity of a user, so that credentials of the user are not required after login

A jwt can be associated with an expiry period and additional session data, which is implemented on the server side

Provided application requires a server which performs the authentication and returns a Jwt

Check Nodejs Jwt Authentication Server to implement such a server in Nodejs

Creating new Flutter App

Check Flutter installation to setup Flutter

Use flutter create command to create a Flutter project (here nc_jwt_auth :

flutter create nc_jwt_auth

Dependency

Implementation

Imports
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';

json.decode() requires dart:convert

dart:async for async and await keywords, http.dart for http.post() method


Example app provided below uses two StatefulWidget widgets LoginPage and UserPage,such that LoginPage sends user credentials to get jwt from server, and requests in UserPage uses the jwt in its requests

Login Page

Login page renders two TextFormField widgets to take username and password from user and a button to initiate login

In login() function, http.post() method is used to send request with username and password as body of the request

void login() async {
    final url = 'http://192.168.43.34:4000/users/authenticate';
    await http.post(url, body: {'username': userData.email, 'password': base64Encode(userData.password.codeUnits)})
    .then((response) {
      Map<String, dynamic> responseMap = json.decode(response.body);
      if(response.statusCode == 200) {
        userData.addData(responseMap);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => UserPage(userData),),
        );
      }
      else {
        if(responseMap.containsKey("message"))
          showDialog(context: context, builder: (BuildContext context) =>
            getAlertDialog("Login failed", '${responseMap["message"]}', context));
      }
    }).catchError((err) {
      showDialog(context: context, builder: (BuildContext context) =>
        getAlertDialog("Login failed", '${err.toString()}', context));
    });
}

The password is encoded to its base64 form using base64Encode() method which takes a list of UTF-16 code units as input

With basic base64 encoding the password can be easily decoded if request is sniffed by an attacker

Something like AES encryption is recommended when sending user credentials, to enhance security

The body of response is decoded to json format and assigned to a map

If response of server has status code 200 then app is navigated to UserPage to which the map (which presumable contains jwt as value of field token) is passed

User Page

The userData object passed to this widget is used to an authentication string which is the prefix Bearer concatenated to the jwt

This format can be considered as a convention and can be manipulated as per server implementation

@override
void initState() {
    headers["Authorization"] = 'Bearer ${userData.token}';
    // headers["x-access-token"] = '${userData.username}';
    super.initState();
}

This string is assigned to the header Authorization(for a map denoting request headers) which is added to requests sent to the server

x-access-token (or some random name) can also be used, which is extracted in server

Future<Map> getUserData() async {
    Map<String, dynamic> responseMap;
    final url = 'http://192.168.43.34:4000/users/getInfo';
    await http.get(url, headers: headers)
      .then((response) {
        responseMap = json.decode(response.body);
        if(response.statusCode == 200) {
          responseMap = responseMap["userdata"];
        }
        else {
          if(responseMap.containsKey("message"))
            throw(Exception(responseMap["message"]));
        }
      })
      .timeout(Duration(seconds:40),onTimeout: () {
        throw(new TimeoutException("fetch from server timed out"));
      })
      .catchError((err) {
        throw(err);
      });
      return responseMap;
}

Valid responses, based on status code of response, are rendered using Widgets

App Code

import 'package:flutter/material.dart';
import 'package:validate/validate.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';


void main() => runApp(new MaterialApp(
  title: 'Flutter Authentication App',
  home: new LoginPage(),
));

AlertDialog getAlertDialog(title, content, context) {
  return AlertDialog(
    title: Text("Login failed"),
    content: Text('${content}'),
    actions: <Widget>[
      FlatButton(
        child: Text('Close'),
        onPressed: () {
          Navigator.of(context).pop();
        },
      ),
    ],
  );
}

class LoginPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new _LoginPageState();
}

class _LoginData {
  String email = '';
  String password = '';
}

class UserData extends _LoginData {
  String token = '';
  String username = '';
  int id;

  void addData (Map<String, dynamic> responseMap) {
    this.id = responseMap["id"];
    this.username = responseMap["username"];
    this.token = responseMap["token"];
  }
}

class _LoginPageState extends State<LoginPage> {
  final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
  UserData userData = new UserData();

  void submit() {
    if (this._formKey.currentState.validate()) {
      _formKey.currentState.save();
      login();
    }
  }

  void login() async {
    final url = 'http://192.168.43.34:4000/users/authenticate';
    await http.post(url, body: {'username': userData.email, 'password': base64Encode(userData.password.codeUnits)})
    .then((response) {
      Map<String, dynamic> responseMap = json.decode(response.body);
      if(response.statusCode == 200) {
        userData.addData(responseMap);
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => UserPage(userData),),
        );
      }
      else {
        if(responseMap.containsKey("message"))
          showDialog(context: context, builder: (BuildContext context) =>
            getAlertDialog("Login failed", '${responseMap["message"]}', context));
      }
    }).catchError((err) {
      showDialog(context: context, builder: (BuildContext context) =>
        getAlertDialog("Login failed", '${err.toString()}', context));
    });
  }

  @override
  Widget build(BuildContext context) {
    final Size screenSize = MediaQuery.of(context).size;
    return new Scaffold(
      appBar: new AppBar(title: new Text('Login'),),
      body: new Container(
        padding: new EdgeInsets.all(20.0),
        child: new Form(
          key: this._formKey,
          child: new ListView(
            children: <Widget>[
              new TextFormField(
                keyboardType: TextInputType.emailAddress, // Use email input type for emails.
                decoration: new InputDecoration(hintText: 'ign', labelText: 'Username'),
                onSaved: (String value) { this.userData.email = value; }
              ),
              new TextFormField(
                obscureText: true, // To display typed char with *
                decoration: new InputDecoration(
                  hintText: 'Password',
                  labelText: 'Enter your password'
                ),
                onSaved: (String value) { this.userData.password = value; }
              ),
              new Container(
                width: screenSize.width,
                child: new RaisedButton(
                  child: new Text('Login', style: new TextStyle(color: Colors.white),),
                  onPressed: this.submit,
                  color: Colors.blue,
                ),
                margin: new EdgeInsets.only(top: 20.0),
              ),
            ],
          ),
        )
      ),
    );
  }
}

class UserPage extends StatefulWidget {
  UserData userData;
  UserPage(@required this.userData) : super(key: key);
  @override
  State<StatefulWidget> createState() => new _UserPageState(userData);
}

class _UserPageState extends State<UserPage> {
  UserData userData;
  Map<String, String> headers = new Map();
  List<Widget> posts = new List();

  _UserPageState(this.userData);

  @override
  void initState() {
    headers["Authorization"] = 'Bearer ${userData.token}';
    // headers["x-access-token"] = '${userData.username}';
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final Size screenSize = MediaQuery.of(context).size;
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('User page'),
      ),
      body: new SingleChildScrollView(
        child: new Column(
          children: [
            FutureBuilder<Map>(
              future: getUserData(), //sets getServerData method as the expected Future
              builder: (context, snapshot) {
                List<Widget> widgetList = List();
                if (snapshot.hasData) { //checks if response returned valid data
                  widgetList = getUserInfo(snapshot.data);
                }
                else if (snapshot.hasError) { //checks if the response threw error
                  widgetList.add(Text("${snapshot.error}"));
                }
                else {
                  widgetList.add(getRowWithText("Id", "${userData.id}"));
                  widgetList.add(getRowWithText("Username", userData.username));
                  widgetList.add(CircularProgressIndicator());
                }
                return Container(
                  height: (screenSize.height-60) * 0.26,
                  color: Colors.blue[500],
                  padding: new EdgeInsets.all(10.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children:widgetList
                  ),
                );
              },
            ),
            FutureBuilder<List>(
              future: getServerData(), //sets getServerData method as the expected Future
              builder: (context, snapshot) {
                if (snapshot.hasData) { //checks if response returned valid data
                  return SingleChildScrollView(
                    child: Container(
                      height: (screenSize.height-60) * 0.65,
                      padding: new EdgeInsets.all(20.0),
                      child: getPosts(snapshot.data),
                    ),
                  );
                }
                else if (snapshot.hasError) { //checks if the response threw error
                  return Text("${snapshot.error}");
                }
                return CircularProgressIndicator();
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget getPosts(List<dynamic> _posts) {
    for(int i=0;i<_posts.length;i++) {
      posts.add(getPostCard(_posts[i]));
    }
    return ListView.separated(
      shrinkWrap:true,
      physics: const AlwaysScrollableScrollPhysics(),
      itemCount: posts.length,
      itemBuilder: (BuildContext context, int index) {
        return posts[index];
      },
      separatorBuilder: (BuildContext context, int index) => const Divider(),
    );
  }

  Widget getPostCard(post) {
    return Card(
      color: Colors.teal[300],
      child: ListTile(
        subtitle: Text(post),
      ),
    );
  }

  Widget getTextContainer(text) {
    return Container(
     padding: EdgeInsets.only(left:5, right:5),
     child: Text(text),
   );
  }

  Widget getRowWithText(label, value) {
    return Row(
      children: <Widget>[
        getTextContainer(label),
        getTextContainer(value),
      ],
    );
  }

  List<Widget> getUserInfo(map) {
    return <Widget>[
      Row(
        children: <Widget>[
          Column(
            children: <Widget>[
              Container(
                width:100,
                height:100,
                child: Image.network(map["picture"]["medium"], fit: BoxFit.cover),
              ),
            ],
          ),
          Expanded( child: Container(
            height:100,
            padding: EdgeInsets.only(left: 10),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                getRowWithText("Id", '${map["id"]}'),
                getRowWithText("Username", map["username"]),
                getRowWithText("First name", map["name"]["first"]),
                getRowWithText("Last name", map["name"]["last"]),
              ],
            ),
          ),),
        ],
      ),
      getRowWithText("Phone Number", '${map["phone"]}'),
      getRowWithText("Email", map["email"]),
    ];
  }

  Future<Map> getUserData() async {
    Map<String, dynamic> responseMap;
    final url = 'http://192.168.43.34:4000/users/getInfo';
    await http.get(url, headers: headers)
      .then((response) {
        responseMap = json.decode(response.body);
        if(response.statusCode == 200) {
          responseMap = responseMap["userdata"];
        }
        else {
          if(responseMap.containsKey("message"))
            throw(Exception(responseMap["message"]));
        }
      })
      .timeout(Duration(seconds:40),onTimeout: () {
        throw(new TimeoutException("fetch from server timed out"));
      })
      .catchError((err) {
        throw(err);
      });
      return responseMap;
  }

  Future<List> getServerData() async {
    final url = 'http://192.168.43.34:4000/users/initialPosts';
    Map<String, dynamic> responseMap;
    await http.get(url, headers: headers)
    .then((response) {
      responseMap = json.decode(response.body);
      if(response.statusCode == 200) {
        if(!responseMap.containsKey("posts"))
          throw(Exception('error while server fetch'));
      }
      else {
        if(responseMap.containsKey("message"))
          throw(Exception('${responseMap["message"]}'));
        else
          throw(Exception('error while server fetch'));
      }
    })
    .timeout(Duration(seconds:40),onTimeout: () {
      throw(new TimeoutException("fetch from server timed out"));
    })
    .catchError((err) {
      throw(err);
    });
    return responseMap["posts"];
  }
}

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


Screenshot/image

Android

cl-flutter-jwt-auth

iOS

cm-flutter-jwt-authentication-as1cm-flutter-jwt-authentication-as2cm-flutter-jwt-authentication-as3