From 14d40d0548eb392ccaa3cf77f7065278c2d6f4d0 Mon Sep 17 00:00:00 2001 From: Satyam Mishra Date: Wed, 12 Mar 2025 04:21:13 +0530 Subject: [PATCH] Added JWT Auth feature --- lib/utils/api_client.dart | 52 +++++++++++++ lib/utils/auth_service.dart | 50 +++++++++++++ lib/utils/http_utils.dart | 18 ++++- lib/utils/jwt_utils.dart | 61 ++++++++++++++++ pubspec.lock | 130 +++++++++++++++++++++++++++------ pubspec.yaml | 6 +- test/utils/jwt_utils_test.dart | 34 +++++++++ 7 files changed, 327 insertions(+), 24 deletions(-) create mode 100644 lib/utils/api_client.dart create mode 100644 lib/utils/auth_service.dart create mode 100644 lib/utils/jwt_utils.dart create mode 100644 test/utils/jwt_utils_test.dart diff --git a/lib/utils/api_client.dart b/lib/utils/api_client.dart new file mode 100644 index 000000000..da5addad5 --- /dev/null +++ b/lib/utils/api_client.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'jwt_utils.dart'; + +class ApiClient { + final String baseUrl; + + ApiClient({required this.baseUrl}); + + // Get request headers with JWT authentication + Future> _getAuthHeaders() async { + final token = await JwtUtils.getToken(); + return { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + // GET request with authentication + Future get(String endpoint) async { + return http.get( + Uri.parse('$baseUrl/$endpoint'), + headers: await _getAuthHeaders(), + ); + } + + // POST request with authentication + Future post(String endpoint, Map body) async { + return http.post( + Uri.parse('$baseUrl/$endpoint'), + headers: await _getAuthHeaders(), + body: jsonEncode(body), + ); + } + + // PUT request with authentication + Future put(String endpoint, Map body) async { + return http.put( + Uri.parse('$baseUrl/$endpoint'), + headers: await _getAuthHeaders(), + body: jsonEncode(body), + ); + } + + // DELETE request with authentication + Future delete(String endpoint) async { + return http.delete( + Uri.parse('$baseUrl/$endpoint'), + headers: await _getAuthHeaders(), + ); + } +} diff --git a/lib/utils/auth_service.dart b/lib/utils/auth_service.dart new file mode 100644 index 000000000..a75c51d1d --- /dev/null +++ b/lib/utils/auth_service.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../utils/jwt_utils.dart'; + +class AuthService { + final String baseUrl; + + AuthService({required this.baseUrl}); + + // Login and store token + Future login(String username, String password) async { + try { + final response = await http.post( + Uri.parse('$baseUrl/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'username': username, + 'password': password, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final token = data['token']; + if (token != null) { + await JwtUtils.saveToken(token); + return true; + } + } + } catch (e) { + print("Login error: $e"); + } + return false; + } + + // Logout (delete token) + Future logout() async { + await JwtUtils.deleteToken(); + } + + // Check if the user is authenticated + Future isAuthenticated() async { + return await JwtUtils.isAuthenticated(); + } + + // Retrieve user details from the stored JWT + Future> getUserInfo() async { + return await JwtUtils.getPayload(); + } +} diff --git a/lib/utils/http_utils.dart b/lib/utils/http_utils.dart index 379a3bf97..a1038e663 100644 --- a/lib/utils/http_utils.dart +++ b/lib/utils/http_utils.dart @@ -1,13 +1,14 @@ import 'package:apidash_core/apidash_core.dart'; import '../consts.dart'; +import '../utils/jwt_utils.dart'; // Import JWT utilities String getRequestTitleFromUrl(String? url) { - if (url == null || url.trim() == "") { + if (url == null || url.trim().isEmpty) { return kUntitled; } if (url.contains("://")) { String rem = url.split("://")[1]; - if (rem.trim() == "") { + if (rem.trim().isEmpty) { return kUntitled; } return rem; @@ -48,3 +49,16 @@ String getRequestTitleFromUrl(String? url) { } return (kNoBodyViewOptions, null); } + +// Utility function to get authorization header for API requests +Future> getAuthHeaders() async { + final token = await JwtUtils.getToken(); + final headers = { + 'Content-Type': 'application/json', + }; + + if (token != null) { + headers['Authorization'] = 'Bearer $token'; + } + return headers; +} diff --git a/lib/utils/jwt_utils.dart b/lib/utils/jwt_utils.dart new file mode 100644 index 000000000..e834c415b --- /dev/null +++ b/lib/utils/jwt_utils.dart @@ -0,0 +1,61 @@ +import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; + +class JwtUtils { + static const String _tokenKey = 'jwt_token'; + + // Determine if the platform is mobile (Android/iOS) + static bool _isMobile() { + return defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS; + } + + // Save JWT token securely + static Future saveToken(String token) async { + if (kIsWeb || !_isMobile()) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_tokenKey, token); + } else { + const storage = FlutterSecureStorage(); + await storage.write(key: _tokenKey, value: token); + } + } + + // Retrieve stored token + static Future getToken() async { + if (kIsWeb || !_isMobile()) { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_tokenKey); + } else { + const storage = FlutterSecureStorage(); + return await storage.read(key: _tokenKey); + } + } + + // Delete token when logging out + static Future deleteToken() async { + if (kIsWeb || !_isMobile()) { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); + } else { + const storage = FlutterSecureStorage(); + await storage.delete(key: _tokenKey); + } + } + + // Check if a stored token is valid (not expired) + static Future isAuthenticated() async { + final token = await getToken(); + if (token == null) return false; + return !JwtDecoder.isExpired(token); + } + + // Extract payload data from the token + static Future> getPayload() async { + final token = await getToken(); + if (token == null) return {}; + return JwtDecoder.decode(token); + } +} diff --git a/pubspec.lock b/pubspec.lock index b16f69dd4..b36b8aca0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "80.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" analyzer: dependency: transitive description: @@ -51,10 +59,10 @@ packages: dependency: transitive description: name: archive - sha256: "528579c7e4579719f04b21eeeeddfd73a18b31dabc22766893b7d1be7f49b967" + sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" args: dependency: transitive description: @@ -171,10 +179,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.9.5" characters: dependency: transitive description: @@ -318,6 +326,14 @@ packages: relative: true source: path version: "0.1.2" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: "00a0812d2aeaeb0d30bcbc4dd3cee57971dbc0ab2216adf4f0247f37793f15ef" + url: "https://pub.dev" + source: hosted + version: "2.17.0" dart_style: dependency: "direct main" description: @@ -350,6 +366,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" equatable: dependency: transitive description: @@ -434,10 +458,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc" + sha256: f3a3d48a36d1640b4dca22a086f26b426c246925a80eddc2953120775fbcf86a url: "https://pub.dev" source: hosted - version: "0.5.1+12" + version: "0.5.1+13" file_selector_ios: dependency: transitive description: @@ -632,6 +656,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + url: "https://pub.dev" + source: hosted + version: "2.1.1" flutter_svg: dependency: "direct main" description: @@ -776,7 +848,7 @@ packages: source: hosted version: "2.0.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f @@ -930,6 +1002,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker: dependency: transitive description: @@ -1101,10 +1181,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -1149,10 +1229,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.16" path_provider_foundation: dependency: transitive description: @@ -1257,6 +1337,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.2+1" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" pool: dependency: transitive description: @@ -1316,10 +1404,10 @@ packages: dependency: "direct main" description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pub_updater: dependency: transitive description: @@ -1444,10 +1532,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.8" shared_preferences_foundation: dependency: transitive description: @@ -1737,10 +1825,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.15" url_launcher_ios: dependency: transitive description: @@ -1841,10 +1929,10 @@ packages: dependency: transitive description: name: video_player_android - sha256: "7018dbcb395e2bca0b9a898e73989e67c0c4a5db269528e1b036ca38bcca0d0b" + sha256: ae7d4f1b41e3ac6d24dd9b9d5d6831b52d74a61bdd90a7a6262a33d8bb97c29a url: "https://pub.dev" source: hosted - version: "2.7.17" + version: "2.8.2" video_player_avfoundation: dependency: transitive description: @@ -1929,10 +2017,10 @@ packages: dependency: transitive description: name: win32 - sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted - version: "5.11.0" + version: "5.12.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index af67db989..95d49c1e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,11 @@ dependencies: data_table_2: 2.5.16 dart_style: ^3.0.1 desktop_drop: ^0.5.0 + dart_jsonwebtoken: ^2.12.2 extended_text_field: ^16.0.0 + flutter_secure_storage: ^8.0.0 + shared_preferences: ^2.2.0 + jwt_decoder: ^2.0.1 file_selector: ^1.0.3 flex_color_scheme: ^8.1.1 flutter_highlighter: ^0.1.0 @@ -58,7 +62,7 @@ dependencies: riverpod: ^2.5.1 scrollable_positioned_list: ^0.3.8 share_plus: ^10.1.4 - shared_preferences: ^2.5.2 + http: ^1.1.0 url_launcher: ^6.2.5 uuid: ^4.5.0 vector_graphics_compiler: ^1.1.9+1 diff --git a/test/utils/jwt_utils_test.dart b/test/utils/jwt_utils_test.dart new file mode 100644 index 000000000..91a67550e --- /dev/null +++ b/test/utils/jwt_utils_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/utils/jwt_utils.dart'; + +void main() { + group('JWT Utils Tests', () { + const testToken = + "your_valid_jwt_token_here"; // Replace with a real valid token + const expiredToken = + "your_expired_jwt_token_here"; // Replace with an expired token + + test('Save and retrieve token', () async { + await JwtUtils.saveToken(testToken); + final retrievedToken = await JwtUtils.getToken(); + expect(retrievedToken, equals(testToken)); + }); + + test('Check if token is valid', () { + final isValid = JwtUtils.isTokenValid(testToken); + expect(isValid, isTrue); + }); + + test('Check if expired token is invalid', () { + final isExpired = JwtUtils.isTokenValid(expiredToken); + expect(isExpired, isFalse); + }); + + test('Extract payload from token', () { + final payload = JwtUtils.getPayload(testToken); + expect(payload, isA>()); + expect(payload.containsKey("sub"), isTrue); // Adjust according to your JWT structure + }); + + test('Delete token', () async { + await JwtUtils.