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
Check Flutter installation to setup Flutter
Use flutter create
command to create a Flutter project (here nc_jwt_auth :
flutter create nc_jwt_auth
Add validate
and http
packages to pubspec.yaml
dependencies:
validate:
http: "0.12.1"
flutter:
sdk: flutter
Run following command to add dependency
$ flutter pub get
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 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
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
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"];
}
}
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