Newby Coder header banner

Basic Authentication in Flutter Android/iOS application

What is basic authentication

In basic authentication, user credentials are sent to a server in raw form or with basic encoding, typically in the body of a request

Follwing sample code is also basic authentication where request header is used to send credential pair:

Map<String, String> headers = new Map();
var temp = base64Encode('${userData.username}:${userData.password}'.codeUnits);
headers["Authorization"] = 'Basic ${temp}';
http.get('http://192.168.43.34:4500/users/getInfo', headers:headers).then((response){ });

It consists of a prefix Basic (or some other word) and base64 encoding of username and password separated by colon (:)

Provided application requires a backend server which performs the authentication

Check Nodejs Basic 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_basic_auth :

flutter create nc_basic_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,both of which involve sending http requests with user credentials

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:4500/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

This encoding is according to server implementation

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 is passed

User Page

The userData object passed to this widget is used to create a basic authentication string which is the prefix Basic concatenated to the base64 encoded form of username:password

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

@override
void initState() {
    var temp = base64Encode('${userData.username}:${userData.password}'.codeUnits);
    headers["Authorization"] = 'Basic ${temp}';
    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

Future<Map> getUserData() async {
    Map<String, dynamic> responseMap;
    final url = 'http://192.168.43.34:4500/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

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 username = '';
  int id;

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

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:4500/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(
                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() {
    var temp = base64Encode('${userData.username}:${userData.password}'.codeUnits);
    headers["Authorization"] = 'Basic ${temp}';
    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 getUserData 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 contains 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: getUserPosts(), //sets getUserPosts method as the expected Future
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return SingleChildScrollView(
                    child: Container(
                      height: (screenSize.height-60) * 0.65,
                      padding: new EdgeInsets.all(20.0),
                      child: getPosts(snapshot.data),
                    ),
                  );
                }
                else if (snapshot.hasError) {
                  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:4500/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> getUserPosts() async {
    final url = 'http://192.168.43.34:4500/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-auth-basic

iOS

cm-flutter-basic-auth-as1cm-flutter-basic-auth-as2cm-flutter-basic-auth-as3
cm-flutter-basic-auth-as4