From c8c30d3838902e714b9b4fd498d9b1690d6eff14 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Tue, 1 Apr 2025 11:52:46 +0200 Subject: [PATCH] v1.4.0 --- CHANGELOG.md | 38 ++++ README.md | 31 ++- lib/arcane_helper_utils.dart | 2 +- .../extensions/{string_jwt.dart => jwt.dart} | 197 +++++++++++------- pubspec.yaml | 2 +- test/extensions/jwt_test.dart | 82 ++++++++ test/extensions/string_jwt_test.dart | 40 ---- 7 files changed, 270 insertions(+), 122 deletions(-) rename lib/src/extensions/{string_jwt.dart => jwt.dart} (60%) create mode 100644 test/extensions/jwt_test.dart delete mode 100644 test/extensions/string_jwt_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d7b1e..0fd7571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +## 1.4.0 + +- [BREAKING] JWT-related extensions have been reworked. + +Old: + +```dart +String token = "your.jwt.token"; +DateTime? expiry = token.jwtExpiryTime(); +String? userId = token.jwtUserId(); +String? email = token.jwtEmail(); +``` + +New: + +```dart +String token = "your.jwt.token"; +DateTime? expiry = token.jwt.expiryTime; +String? userId = token.jwt.userId; +String? email = token.jwt.email; + +// Added: +JwtPayload? token.jwt; +String? givenName = token.jwt.givenName; +String? familyName = token.jwt.familyName; +``` + +Additionally, the exceptions thrown when parsing an invalid JWT have been +updated. + +```dart + String email = "invalid".jwtEmail() // Previously threw Exception("invalid token") + String email = "invalid".jwt.email // Now throws InvalidTokenException() + + String email = "".jwtEmail() // Previously threw Exception("invalid payload") + String email = "".jwt.email // Now throws InvalidPayloadException() +``` + ## 1.3.2 - Added `isEmptyOrNull` and `isNotEmptyOrNull` extensions for `List` and `String` objects. These extensions are identical to `isNullOrEmpty` and `isNotNullOrEmpty`, respectively. diff --git a/README.md b/README.md index 8a2a129..44cf311 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ providing utility functions and extensions that simplify common tasks. - **DateTime Extensions**: Adds additional functionality to the `DateTime` class, making it easier to format dates and calculate differences. - **String Extensions**: Enhances the `String` class by adding new methods for - common transformations and checks, including JWT parsing. + common transformations and checks. +- **JWT Utilities**: Provides getters to parse a JWT token from a `String`, then + get common properties from it. - **List Extensions**: Adds a new `unique` operator for filtering `List` items. ## Getting Started @@ -260,11 +262,18 @@ making it easier to handle JSON Web Tokens directly as `String` objects. Here are some examples of how these methods can be utilized: +- Parse a JWT string + + ```dart + String token = "your.jwt.token"; + final JwtPayload? payload = token.jwt; // Returns the JWT's payload + ``` + - Extracting the email address (`jwt["sub"]`) ```dart String jwt = "your.jwt.token"; - final String? email = jwt.jwtEmail(); // Returns the email address in the JWT + final String? email = jwt.jwt.email; // Returns the email address in the JWT ``` - Extracting the token expiration time (`jwt["exp"]`) @@ -272,14 +281,28 @@ Here are some examples of how these methods can be utilized: ```dart String jwt = "your.jwt.token"; // Returns a `DateTime?` when the token expires - final DateTime? email = jwt.jwtExpiryTime(); + final DateTime? email = jwt.jwt.expiryTime; ``` - Extracting the user ID (`jwt["uid"]`) ```dart String jwt = "your.jwt.token"; - final String? uid = jwt.jwtUserId(); // Returns the UID value from the token + final String? uid = jwt.jwt.userId; // Returns the UID value from the token + ``` + +- Extracting the given name (`jwt["given_name"]`) + + ```dart + String jwt = "your.jwt.token"; + final String? uid = jwt.jwt.givenName; // Returns the given name from the token + ``` + +- Extracting the family name (`jwt["family_name"]`) + + ```dart + String jwt = "your.jwt.token"; + final String? uid = jwt.jwt.familyName; // Returns the family name from the token ``` ### String Utilities diff --git a/lib/arcane_helper_utils.dart b/lib/arcane_helper_utils.dart index 1ea013b..b71c2ba 100644 --- a/lib/arcane_helper_utils.dart +++ b/lib/arcane_helper_utils.dart @@ -2,8 +2,8 @@ library arcane_helper_utils; export "package:arcane_helper_utils/src/extensions/date_time.dart"; export "package:arcane_helper_utils/src/extensions/dynamic.dart"; +export "package:arcane_helper_utils/src/extensions/jwt.dart"; export "package:arcane_helper_utils/src/extensions/list.dart"; export "package:arcane_helper_utils/src/extensions/string.dart"; -export "package:arcane_helper_utils/src/extensions/string_jwt.dart"; export "package:arcane_helper_utils/src/utils/json_converter.dart"; export "package:arcane_helper_utils/src/utils/ticker.dart"; diff --git a/lib/src/extensions/string_jwt.dart b/lib/src/extensions/jwt.dart similarity index 60% rename from lib/src/extensions/string_jwt.dart rename to lib/src/extensions/jwt.dart index f57826c..1e63abb 100644 --- a/lib/src/extensions/string_jwt.dart +++ b/lib/src/extensions/jwt.dart @@ -1,79 +1,12 @@ import "dart:convert"; +typedef JwtPayload = Map; + /// An extension on `String` to extract useful information from JSON Web Tokens (JWT). /// /// This extension provides methods to decode a JWT string and retrieve common /// fields like email, expiration time, and user ID. extension JWTUtility on String { - /// Extracts the email from the JWT payload. - /// - /// This method attempts to parse the JWT and retrieve the `sub` field, which is - /// typically used to store the email of the token owner. - /// - /// Returns the email as a `String` if present, or `null` if the parsing fails or the - /// email is not found. - /// - /// Example: - /// ```dart - /// String token = "your.jwt.token"; - /// String? email = token.jwtEmail(); - /// ``` - String? jwtEmail() { - try { - final payload = _parseJwt(this); - final String email = payload["sub"] as String; - return email; - } catch (_) { - return null; - } - } - - /// Extracts the expiration time from the JWT payload. - /// - /// This method retrieves the `exp` field from the JWT, which represents the expiration - /// time as a Unix timestamp. The timestamp is converted to a `DateTime` object. - /// - /// Returns the `DateTime` representing the expiration time, or `null` if the parsing - /// fails or the expiration time is not found. - /// - /// Example: - /// ```dart - /// String token = "your.jwt.token"; - /// DateTime? expiry = token.jwtExpiryTime(); - /// ``` - DateTime? jwtExpiryTime() { - try { - final payload = _parseJwt(this); - final expiry = payload["exp"] as int; - final date = DateTime.fromMillisecondsSinceEpoch(expiry * 1000); - return date; - } catch (_) { - return null; - } - } - - /// Extracts the user ID from the JWT payload. - /// - /// This method retrieves the `uid` field, which is typically the unique user identifier. - /// This ID can be used in analytics or for tracking purposes. - /// - /// Returns the user ID as a `String`, or `null` if the parsing fails or the ID is not found. - /// - /// Example: - /// ```dart - /// String token = "your.jwt.token"; - /// String? userId = token.jwtUserId(); - /// ``` - String? jwtUserId() { - try { - final payload = _parseJwt(this); - final String id = payload["uid"] as String; - return id; - } catch (_) { - return null; - } - } - /// Decodes a base64 URL-encoded string. /// /// This method replaces the URL-safe characters in the base64 string (`-` and `_`) @@ -111,20 +44,132 @@ extension JWTUtility on String { /// /// Example: /// ```dart - /// Map payload = _parseJwt("your.jwt.token"); + /// Map payload = parseJwt("your.jwt.token"); /// ``` - Map _parseJwt(String token) { - final parts = token.split("."); + JwtPayload get jwt { + final parts = this.split("."); if (parts.length != 3) { - throw Exception("invalid token"); + throw InvalidTokenException(); } final payload = _decodeBase64(parts[1]); - final dynamic payloadMap = json.decode(payload); - if (payloadMap is! Map) { - throw Exception("invalid payload"); + final payloadMap = json.decode(payload) as JwtPayload?; + if (payloadMap is! JwtPayload) { + throw InvalidPayloadException(); } return payloadMap; } } + +extension JWTMapUtility on JwtPayload { + /// Extracts the email from the JWT payload. + /// + /// This method attempts to parse the JWT and retrieve the `sub` field, which is + /// typically used to store the email of the token owner. + /// + /// Returns the email as a `String` if present, or `null` if the parsing fails or the + /// email is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// String? email = token.jwt.email; + /// ``` + String? get email { + try { + return this["sub"] as String; + } catch (_) { + return null; + } + } + + /// Extracts the given (first) name from the JWT payload. + /// + /// This method attempts to parse the JWT and retrieve the `given_name` field, + /// which is typically used to store the given (first) name of the token owner. + /// + /// Returns the given name as a `String` if present, or `null` if the parsing + /// fails or the given name is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// String? givenName = token.jwt.givenName; + /// ``` + String? get givenName { + try { + return this["given_name"] as String; + } catch (_) { + return null; + } + } + + /// Extracts the family (last) name from the JWT payload. + /// + /// This method attempts to parse the JWT and retrieve the `family_name` field, + /// which is typically used to store the family (last) name of the token owner. + /// + /// Returns the family name as a `String` if present, or `null` if the parsing + /// fails or the family name is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// String? familyName = token.jwt.familyName; + /// ``` + String? get familyName { + try { + return this["family_name"] as String; + } catch (_) { + return null; + } + } + + /// Extracts the expiration time from the JWT payload. + /// + /// This method retrieves the `exp` field from the JWT, which represents the expiration + /// time as a Unix timestamp. The timestamp is converted to a `DateTime` object. + /// + /// Returns the `DateTime` representing the expiration time, or `null` if the parsing + /// fails or the expiration time is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// DateTime? expiry = token.jwt.expiryTime; + /// ``` + DateTime? get expiryTime { + try { + final expiry = this["exp"] as int; + final date = DateTime.fromMillisecondsSinceEpoch(expiry * 1000); + return date; + } catch (_) { + return null; + } + } + + /// Extracts the user ID from the JWT payload. + /// + /// This method retrieves the `uid` field, which is typically the unique user identifier. + /// This ID can be used in analytics or for tracking purposes. + /// + /// Returns the user ID as a `String`, or `null` if the parsing fails or the ID is not found. + /// + /// Example: + /// ```dart + /// String token = "your.jwt.token"; + /// String? userId = token.jwt.userId; + /// ``` + String? get userId { + try { + return this["uid"] as String; + } catch (_) { + return null; + } + } +} + +class InvalidTokenException implements Exception {} + +class InvalidPayloadException implements Exception {} diff --git a/pubspec.yaml b/pubspec.yaml index 03bb249..1fb8857 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: arcane_helper_utils description: Provides a variety of helpful utilities and extensions for Flutter and Dart. -version: 1.3.2 +version: 1.4.0 repository: https://github.com/hanskokx/arcane_helper_utils issue_tracker: https://github.com/hanskokx/arcane_helper_utils/issues diff --git a/test/extensions/jwt_test.dart b/test/extensions/jwt_test.dart new file mode 100644 index 0000000..2577f2b --- /dev/null +++ b/test/extensions/jwt_test.dart @@ -0,0 +1,82 @@ +import "package:arcane_helper_utils/arcane_helper_utils.dart"; +import "package:test/test.dart"; + +void main() { + // Example JWT token for testing + const String validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJ1aWQiOiIxMjM0NTYiLCJnaXZlbl9uYW1lIjoiZ2l2ZW4iLCJmYW1pbHlfbmFtZSI6ImZhbWlseSJ9." + "Ki_D9fOhv7bLe86j6fdZ1guNGI7ldKSeeANUfinwNtc"; + + group("JWTUtility", () { + test("jwt getter parses a valid JWT", () { + expect(validToken.jwt, isNotNull); + }); + + test("jwt getter throws an exception for invalid token", () { + expect(() => "invalid.token".jwt, throwsException); + expect(() => "".jwt, throwsException); + }); + + test("jwt.email extracts email correctly", () { + expect(validToken.jwt.email, "test@example.com"); + }); + + test("jwt.email throws an exception for invalid token", () { + expect( + () => "invalid.token".jwt.email, + throwsA(isA()), + ); + expect(() => "".jwt.email, throwsA(isA())); + }); + + test("jwt.givenName extracts given name correctly", () { + expect(validToken.jwt.givenName, "given"); + }); + + test("jwt.givenName throws an exception for invalid token", () { + expect( + () => "invalid.token".jwt.givenName, + throwsA(isA()), + ); + expect(() => "".jwt.givenName, throwsA(isA())); + }); + + test("jwt.familyName extracts family name correctly", () { + expect(validToken.jwt.familyName, "family"); + }); + + test("jwt.familyName returns null for invalid token", () { + expect( + () => "invalid.token".jwt.familyName, + throwsA(isA()), + ); + expect(() => "".jwt.familyName, throwsA(isA())); + }); + + test("jwt.expiryTime extracts expiry time correctly", () { + final DateTime? expiry = validToken.jwt.expiryTime; + expect(expiry, isNotNull); + expect(expiry?.year, 2025); // Based on the exp value in the test token + }); + + test("jwt.expiryTime throws an exception for invalid token", () { + expect( + () => "invalid.token".jwt.expiryTime, + throwsA(isA()), + ); + expect(() => "".jwt.expiryTime, throwsA(isA())); + }); + + test("jwt.userId extracts user ID correctly", () { + expect(validToken.jwt.userId, "123456"); + }); + + test("jwt.userId throws an exception for invalid token", () { + expect( + () => "invalid.token".jwt.userId, + throwsA(isA()), + ); + expect(() => "".jwt.userId, throwsA(isA())); + }); + }); +} diff --git a/test/extensions/string_jwt_test.dart b/test/extensions/string_jwt_test.dart deleted file mode 100644 index 752520f..0000000 --- a/test/extensions/string_jwt_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "package:arcane_helper_utils/arcane_helper_utils.dart"; -import "package:test/test.dart"; - -void main() { - // Example JWT token for testing - const String validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." - "eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzM1Njg5NjAwLCJ1aWQiOiIxMjM0NTYifQ." - "signature"; - - group("JWTUtility", () { - test("jwtEmail extracts email correctly", () { - expect(validToken.jwtEmail(), "test@example.com"); - }); - - test("jwtEmail returns null for invalid token", () { - expect("invalid.token".jwtEmail(), null); - expect("".jwtEmail(), null); - }); - - test("jwtExpiryTime extracts expiry time correctly", () { - final DateTime? expiry = validToken.jwtExpiryTime(); - expect(expiry, isNotNull); - expect(expiry?.year, 2025); // Based on the exp value in the test token - }); - - test("jwtExpiryTime returns null for invalid token", () { - expect("invalid.token".jwtExpiryTime(), null); - expect("".jwtExpiryTime(), null); - }); - - test("jwtUserId extracts user ID correctly", () { - expect(validToken.jwtUserId(), "123456"); - }); - - test("jwtUserId returns null for invalid token", () { - expect("invalid.token".jwtUserId(), null); - expect("".jwtUserId(), null); - }); - }); -}