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 :
Dependency
Implementation
Imports
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
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
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
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,
decoration: new InputDecoration(hintText: 'ign', labelText: 'Username'),
onSaved: (String value) { this.userData.email = value; }
),
new TextFormField(
obscureText: true,
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}';
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(),
builder: (context, snapshot) {
List<Widget> widgetList = List();
if (snapshot.hasData) {
widgetList = getUserInfo(snapshot.data);
}
else if (snapshot.hasError) {
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(),
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: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
It builds and runs app on an available android/ios device
Screenshot/image
Android
iOS