Securing API Calls with JWT in Flutter

In modern mobile development, securing API calls is a critical aspect of building robust and reliable applications. JSON Web Tokens (JWT) have become a popular standard for securely transmitting information between parties as a JSON object. When combined with Flutter, a cross-platform UI toolkit, JWTs can be used to effectively secure API requests and ensure data integrity.

What is JWT (JSON Web Token)?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. JWTs are commonly used for authentication and authorization purposes, and they provide a stateless, scalable, and secure method for verifying the identity of users or applications.

Key Characteristics of JWT

  • Compact: JWTs are small, which makes them suitable for being transmitted in URLs, POST parameters, or HTTP headers.
  • Self-Contained: JWTs carry all the necessary information about the user or application.
  • Secure: JWTs can be digitally signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Why Use JWT in Flutter Applications?

Integrating JWTs into Flutter applications provides numerous benefits, including:

  • Authentication: Verify the identity of users by validating their JWTs.
  • Authorization: Control access to resources based on the roles and permissions embedded within the JWT.
  • Stateless: JWTs eliminate the need for server-side session management.
  • Scalability: Supports scalable application architectures by offloading authentication and authorization tasks to the client-side.

How to Secure API Calls with JWT in Flutter

Follow these steps to implement JWT-based authentication in your Flutter application:

Step 1: Add Dependencies

First, add the necessary dependencies to your pubspec.yaml file. You’ll need packages for handling HTTP requests and JWT decoding.

dependencies:
  http: ^0.13.0
  flutter_secure_storage: ^9.0.0
  dart_jsonwebtoken: ^2.6.0 # for JWT Encoding/Decoding

Then, run flutter pub get to install the dependencies.

Step 2: Implement User Login and Token Storage

Create a function to handle user login and store the received JWT securely. flutter_secure_storage is used to store the token securely.

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

Future<bool> loginUser(String username, String password) async {
  final url = Uri.parse('https://your-api.com/login');
  final response = await http.post(
    url,
    body: json.encode({
      'username': username,
      'password': password,
    }),
    headers: {'Content-Type': 'application/json'},
  );

  if (response.statusCode == 200) {
    final responseData = json.decode(response.body);
    final token = responseData['token'];

    // Store the token securely
    await storage.write(key: 'jwt', value: token);
    return true; // Login successful
  } else {
    // Handle login failure
    print('Login failed with status: ${response.statusCode}');
    return false; // Login failed
  }
}

Step 3: Retrieve the JWT for API Requests

Create a utility function to retrieve the stored JWT token. This token will be included in the headers of your API requests.

Future<String?> getToken() async {
  return await storage.read(key: 'jwt');
}

Step 4: Include JWT in API Request Headers

Modify your API request function to include the JWT in the Authorization header. A bearer token scheme is often employed (Authorization: Bearer <token>).

Future<dynamic> fetchData(String endpoint) async {
  final token = await getToken();
  final url = Uri.parse('https://your-api.com/$endpoint');

  final response = await http.get(
    url,
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    return json.decode(response.body);
  } else {
    // Handle error
    print('Request failed with status: ${response.statusCode}');
    return null;
  }
}

Step 5: Decode and Validate JWT (Optional)

If you need to access claims stored within the JWT on the client side, you can decode it using the dart_jsonwebtoken package.

import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

Future<Map<String, dynamic>?> decodeJWT() async {
  final token = await getToken();

  if (token == null) {
    return null;
  }

  try {
    final jwt = JWT.decode(token); // Decode only, doesn't verify signature
    return jwt.payload as Map<String, dynamic>;
  } on JWTException {
    print('Invalid JWT');
    return null;
  }
}

Warning Client side validation isn’t secure because the shared key is included in your app.

Step 6: Handling Token Expiry

Implement logic to check if the JWT has expired. You can decode the JWT and check the exp (expiration time) claim.

Future<bool> isTokenExpired() async {
  final token = await getToken();

  if (token == null) {
    return true;
  }

  try {
    final jwt = JWT.decode(token); // Decode the token
    final expiry = jwt.payload['exp'] as int;
    final expiryDate = DateTime.fromMillisecondsSinceEpoch(expiry * 1000); // exp is in seconds

    return expiryDate.isBefore(DateTime.now());
  } catch (e) {
    print('Error checking token expiry: $e');
    return true; // Assume expired if there's an error
  }
}

Step 7: Implement Token Refresh (If Applicable)

If your application requires token refresh functionality, implement a mechanism to request a new token from the server when the current token expires.

Future<bool> refreshToken() async {
  final refreshTokenUrl = Uri.parse('https://your-api.com/refresh');
  final token = await getToken();

  final response = await http.post(
    refreshTokenUrl,
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final responseData = json.decode(response.body);
    final newToken = responseData['token'];

    await storage.write(key: 'jwt', value: newToken);
    return true; // Token refresh successful
  } else {
    // Handle refresh failure
    print('Token refresh failed with status: ${response.statusCode}');
    return false; // Token refresh failed
  }
}

Example Flutter UI

Login page and protected API page


import 'package:flutter/material.dart';

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

class _LoginPageState extends State<LoginPage> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _usernameController,
              decoration: InputDecoration(labelText: 'Username'),
            ),
            TextField(
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            ElevatedButton(
              onPressed: () async {
                bool success = await loginUser(_usernameController.text, _passwordController.text);
                if (success) {
                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(builder: (context) => ProtectedPage()),
                  );
                } else {
                  // Show error message
                }
              },
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

class ProtectedPage extends StatefulWidget {
  @override
  _ProtectedPageState createState() => _ProtectedPageState();
}

class _ProtectedPageState extends State<ProtectedPage> {
  String _data = 'Loading...';

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    final data = await fetchData('protected');
    setState(() {
      _data = data != null ? data.toString() : 'Failed to load data.';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Protected Page')),
      body: Center(
        child: Text(_data),
      ),
    );
  }
}

Conclusion

Securing API calls with JWTs in Flutter involves implementing authentication and authorization mechanisms using tokens. By following the outlined steps—setting up dependencies, storing tokens securely, including JWTs in headers, and handling token expiry—developers can build Flutter applications with enhanced security and scalability. JWT provides a stateless and robust way to protect sensitive data transmitted between the client and server, ensuring a secure and reliable user experience.