├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── corsac_jwt.dart └── src │ ├── es256.dart │ ├── hs256.dart │ ├── rs256.dart │ ├── signer.dart │ └── utils.dart ├── pubspec.yaml └── test ├── es256_test.dart ├── jwt_test.dart ├── resources ├── ec_private_key.pem ├── ec_private_key_pkcs8.pem ├── private.pem └── public.pem └── rs256_test.dart /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [c] 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: dart-lang/setup-dart@v1 15 | - run: dart pub get 16 | # - run: dart format --output=none --set-exit-if-changed . 17 | - run: dart analyze 18 | - run: dart test 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/publish.yml 2 | name: Publish to pub.dev 3 | 4 | on: 5 | push: 6 | tags: 7 | # must align with the tag-pattern configured on pub.dev, often just replace 8 | # {{version}} with [0-9]+.[0-9]+.[0-9]+ 9 | - 'v[0-9]+.[0-9]+.[0-9]+' # tag-pattern on pub.dev: 'v{{version}}' 10 | # If you prefer tags like '1.2.3', without the 'v' prefix, then use: 11 | # - '[0-9]+.[0-9]+.[0-9]+' # tag-pattern on pub.dev: '{{version}}' 12 | # If your repository contains multiple packages consider a pattern like: 13 | # - 'my_package_name-v[0-9]+.[0-9]+.[0-9]+' 14 | 15 | # Publish using the reusable workflow from dart-lang. 16 | jobs: 17 | publish: 18 | permissions: 19 | id-token: write # Required for authentication using OIDC 20 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pubspec.lock 2 | .pub 3 | packages 4 | .packages 5 | doc/api 6 | bin/ 7 | .dart_tool/ 8 | 9 | coverage/ 10 | .test_coverage.dart 11 | 12 | # IDEs 13 | .idea 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: dart 4 | 5 | dart: 6 | - stable 7 | - dev 8 | 9 | script: 10 | - pub get 11 | - dartfmt -n --set-exit-if-changed lib/ 12 | - dart analyze --fatal-infos --fatal-warnings . 13 | - pub run test -r expanded --coverage coverage/ 14 | - pub run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.1 2 | 3 | * Added .fromJWK constructors to ES256 and RS256 signers. 4 | 5 | ## 2.0.0+1 6 | 7 | * Minor readme updates. 8 | 9 | ## 2.0.0 10 | 11 | - Switched to package:jose for key parsing, signing and verification logic 12 | - Added ES256 signer 13 | - Removed no longer used dependencies 14 | - Breaking change: the RS256 signer constructor, changed: 15 | ``` 16 | // from: 17 | JWTRsaSha256Signer({String? privateKey, String? publicKey, String? password, String? kid}) 18 | // to: 19 | JWTRsaSha256Signer({required String pem, String? kid}) 20 | ``` 21 | The `password` argument was never implemented by the underlying package so removed to avoid confusion. 22 | The single `pem` param now can be used to represent either the private key or public key. 23 | 24 | ## 1.0.1 25 | 26 | - Internal changes. 27 | 28 | ## 1.0.0 29 | 30 | - Allow setting `kid` header from signers. 31 | - Allow setting standard `typ` header from builder. 32 | 33 | ## 1.0.0-nullsafety.1 34 | 35 | - Support null safety (#21) 36 | 37 | ## 0.4.0 38 | 39 | - Allow setting custom headers. See `JWTBuilder.setHeader` for details. 40 | 41 | ## 0.3.0 42 | 43 | - Upgraded dependencies. 44 | 45 | ## 0.2.2 46 | 47 | - Upgraded dependencies which fixes an issue with validating Firebase id tokens. 48 | 49 | ## 0.2.1 50 | 51 | - Added getter to access headers map (read-only). 52 | 53 | ## 0.2.0 54 | 55 | - Added back RS256 signer (#11) 56 | 57 | ## 0.1.2 58 | 59 | - Exposed complete `claims` Map (read-only). 60 | 61 | ## 0.1.1 62 | 63 | - Relaxed SDK constraint to allow 2.0.0-dev versions. 64 | 65 | ## 0.1.0 66 | 67 | - Initial release. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Anatoly Pulyaevskiy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 22 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lightweight implementation of JSON Web Tokens (JWT). 2 | 3 | ## Usage 4 | 5 | ```dart 6 | import 'package:corsac_jwt/corsac_jwt.dart'; 7 | 8 | void main() { 9 | var builder = new JWTBuilder(); 10 | var token = builder 11 | ..issuer = 'https://api.foobar.com' 12 | ..expiresAt = new DateTime.now().add(new Duration(minutes: 3)) 13 | ..setClaim('data', {'userId': 836}) 14 | ..getToken(); // returns token without signature 15 | 16 | var signer = new JWTHmacSha256Signer('sharedSecret'); 17 | var signedToken = builder.getSignedToken(signer); 18 | print(signedToken); // prints encoded JWT 19 | var stringToken = signedToken.toString(); 20 | 21 | var decodedToken = new JWT.parse(stringToken); 22 | // Verify signature: 23 | print(decodedToken.verify(signer)); // true 24 | 25 | // Validate claims: 26 | var validator = new JWTValidator() // uses DateTime.now() by default 27 | ..issuer = 'https://api.foobar.com'; // set claims you wish to validate 28 | Set errors = validator.validate(decodedToken); 29 | print(errors); // (empty list) 30 | } 31 | ``` 32 | 33 | Supported signers: 34 | 35 | * HS256 (`JWTHmacSha256Signer`). 36 | * RS256 (`JWTRsaSha256Signer`) 37 | * ES256 (`JWTEcdsaSha256Signer`) 38 | 39 | Refer to documentation for more details. 40 | 41 | ## License 42 | 43 | BSD-2 44 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | 7 | linter: 8 | rules: 9 | - avoid_bool_literals_in_conditional_expressions 10 | - avoid_catching_errors 11 | - avoid_classes_with_only_static_members 12 | - avoid_function_literals_in_foreach_calls 13 | - avoid_private_typedef_functions 14 | - avoid_redundant_argument_values 15 | - avoid_renaming_method_parameters 16 | - avoid_returning_null_for_void 17 | - avoid_returning_this 18 | - avoid_single_cascade_in_expression_statements 19 | - avoid_unused_constructor_parameters 20 | - avoid_void_async 21 | - await_only_futures 22 | - camel_case_types 23 | - cancel_subscriptions 24 | - cascade_invocations 25 | - comment_references 26 | - constant_identifier_names 27 | - control_flow_in_finally 28 | - directives_ordering 29 | - empty_statements 30 | - file_names 31 | - hash_and_equals 32 | - implementation_imports 33 | - join_return_with_assignment 34 | - literal_only_boolean_expressions 35 | - missing_whitespace_between_adjacent_strings 36 | - no_adjacent_strings_in_list 37 | - no_runtimeType_toString 38 | - non_constant_identifier_names 39 | - only_throw_errors 40 | - overridden_fields 41 | - package_names 42 | - package_prefixed_library_names 43 | - prefer_asserts_in_initializer_lists 44 | - prefer_const_declarations 45 | - prefer_final_locals 46 | - prefer_function_declarations_over_variables 47 | - prefer_initializing_formals 48 | - prefer_inlined_adds 49 | - prefer_interpolation_to_compose_strings 50 | - prefer_is_not_operator 51 | - prefer_null_aware_operators 52 | - prefer_relative_imports 53 | - prefer_typing_uninitialized_variables 54 | - prefer_void_to_null 55 | - provide_deprecation_message 56 | - test_types_in_equals 57 | - throw_in_finally 58 | - type_annotate_public_apis 59 | - unnecessary_await_in_return 60 | - unnecessary_brace_in_string_interps 61 | - unnecessary_getters_setters 62 | - unnecessary_lambdas 63 | - unnecessary_null_aware_assignments 64 | - unnecessary_overrides 65 | - unnecessary_parenthesis 66 | - unnecessary_statements 67 | - unnecessary_string_interpolations 68 | - use_is_even_rather_than_modulo 69 | - use_string_buffers 70 | - void_checks 71 | -------------------------------------------------------------------------------- /lib/corsac_jwt.dart: -------------------------------------------------------------------------------- 1 | /// Lightweight JSON Web Token (JWT) implementation. 2 | /// 3 | /// ## Usage 4 | /// 5 | /// void main() { 6 | /// var builder = new JWTBuilder(); 7 | /// var token = builder 8 | /// ..issuer = 'https://api.foobar.com' 9 | /// ..expiresAt = new DateTime.now().add(new Duration(minutes: 3)) 10 | /// ..setClaim('data', {'userId': 836}) 11 | /// ..getToken(); // returns token without signature 12 | /// 13 | /// var signer = new JWTHmacSha256Signer(); 14 | /// var signedToken = builder.getSignedToken(signer, 'sharedSecret'); 15 | /// print(signedToken); // prints encoded JWT 16 | /// var stringToken = signedToken.toString(); 17 | /// 18 | /// var decodedToken = new JWT.parse(stringToken); 19 | /// // Verify signature: 20 | /// print(decodedToken.verify(signer, 'sharedSecret')); // true 21 | /// 22 | /// // Validate claims: 23 | /// var validator = new JWTValidator() // uses DateTime.now() by default 24 | /// ..issuer = 'https://api.foobar.com'; // set claims you wish to validate 25 | /// Set errors = validator.validate(decodedToken); 26 | /// print(errors); // (empty list) 27 | /// } 28 | /// 29 | library corsac_jwt; 30 | 31 | import 'dart:convert'; 32 | 33 | import 'src/signer.dart'; 34 | import 'src/utils.dart'; 35 | 36 | export 'src/es256.dart'; 37 | export 'src/hs256.dart'; 38 | export 'src/rs256.dart'; 39 | export 'src/signer.dart'; 40 | 41 | Map _decode(String input) { 42 | try { 43 | return Map.from( 44 | _jsonToBase64Url.decode(_base64Padded(input)) as Map, 45 | ); 46 | } catch (e) { 47 | throw JWTError('Could not decode token string. Error: $e.'); 48 | } 49 | } 50 | 51 | final _jsonToBase64Url = json.fuse(utf8.fuse(base64Url)); 52 | 53 | String _base64Padded(String value) { 54 | final mod = value.length % 4; 55 | if (mod == 0) { 56 | return value; 57 | } else if (mod == 3) { 58 | return value.padRight(value.length + 1, '='); 59 | } else if (mod == 2) { 60 | return value.padRight(value.length + 2, '='); 61 | } else { 62 | return value; // let it fail when decoding 63 | } 64 | } 65 | 66 | String _base64Unpadded(String value) { 67 | if (value.endsWith('==')) return value.substring(0, value.length - 2); 68 | if (value.endsWith('=')) return value.substring(0, value.length - 1); 69 | return value; 70 | } 71 | 72 | /// Error thrown by `JWT` when parsing tokens from string. 73 | class JWTError implements Exception { 74 | final String message; 75 | 76 | JWTError(this.message); 77 | 78 | @override 79 | String toString() => 'JWTError: $message'; 80 | } 81 | 82 | /// JSON Web Token. 83 | class JWT { 84 | /// List of standard (reserved) claims. 85 | static const reservedClaims = ['iss', 'aud', 'iat', 'exp', 'nbf', 'sub', 'jti']; 86 | 87 | /// List of reserved headers. 88 | static const reservedHeaders = ['alg', 'kid']; 89 | 90 | /// Allows access to the full headers map. 91 | /// 92 | /// Returned map is read-only. 93 | final Map headers; 94 | 95 | /// Allows access to the full claims map. 96 | /// 97 | /// Returns modifiable copy of internal map object. 98 | Map get claims => Map.from(_claims); 99 | final Map _claims; 100 | 101 | /// Contains original Base64 encoded token header. 102 | final String encodedHeader; 103 | 104 | /// Contains original Base64 encoded token payload (claims). 105 | final String encodedPayload; 106 | 107 | /// Contains original Base64 encoded token signature, or `null` 108 | /// if token is unsigned. 109 | final String? signature; 110 | 111 | JWT._(this.encodedHeader, this.encodedPayload, this.signature) 112 | : headers = Map.unmodifiable(_decode(encodedHeader)), 113 | _claims = _decode(encodedPayload); 114 | 115 | /// Parses [token] string and creates new instance of [JWT]. 116 | /// Throws [JWTError] if parsing fails. 117 | factory JWT.parse(String token) { 118 | final parts = token.split('.'); 119 | if (parts.length == 2) { 120 | return JWT._(parts.first, parts.last, null); 121 | } else if (parts.length == 3) { 122 | return JWT._(parts[0], parts[1], parts[2]); 123 | } else { 124 | throw JWTError('Invalid token string format for JWT.'); 125 | } 126 | } 127 | 128 | /// Algorithm used to sign this token. The value `none` means this token 129 | /// is not signed. 130 | /// 131 | /// One should not rely on this value to determine the algorithm used to sign 132 | /// this token. 133 | String? get algorithm => headers['alg']; 134 | 135 | /// Id of the key used to sign this token. 136 | String? get keyId => headers['kid']; 137 | 138 | /// The issuer of this token (value of standard `iss` claim). 139 | String? get issuer => _claims['iss'] as String?; 140 | 141 | /// The audience of this token (value of standard `aud` claim). 142 | String? get audience => _claims['aud'] as String?; 143 | 144 | /// The time this token was issued (value of standard `iat` claim). 145 | int? get issuedAt => _claims['iat'] as int?; 146 | 147 | /// The expiration time of this token (value of standard `exp` claim). 148 | int? get expiresAt => _claims['exp'] as int?; 149 | 150 | /// The time before which this token must not be accepted (value of standard 151 | /// `nbf` claim). 152 | int? get notBefore => _claims['nbf'] as int?; 153 | 154 | /// Identifies the principal that is the subject of this token (value of 155 | /// standard `sub` claim). 156 | String? get subject => _claims['sub'] as String?; 157 | 158 | /// Unique identifier of this token (value of standard `jti` claim). 159 | String? get id => _claims['jti'] as String?; 160 | 161 | @override 162 | String toString() { 163 | final buffer = StringBuffer()..writeAll([encodedHeader, '.', encodedPayload]); 164 | if (signature is String) { 165 | buffer.writeAll(['.', signature]); 166 | } 167 | return buffer.toString(); 168 | } 169 | 170 | /// Verifies this token's signature using [signer]. 171 | /// 172 | /// Returns `true` if token is signed and signature is valid, and `false` 173 | /// otherwise. 174 | bool verify(JWTSigner signer) { 175 | if (signature == null) { 176 | return false; 177 | } 178 | 179 | if (keyId != signer.kid) { 180 | return false; // key ids don't match 181 | } 182 | 183 | final body = utf8.encode('$encodedHeader.$encodedPayload'); 184 | final sign = base64Url.decode(_base64Padded(signature!)); 185 | return signer.verify(body, sign); 186 | } 187 | 188 | /// Returns value associated with claim specified by [key]. 189 | dynamic getClaim(String key) => _claims[key]; 190 | } 191 | 192 | /// Builder for JSON Web Tokens. 193 | class JWTBuilder { 194 | final Map _claims = {}; 195 | final Map _headers = {'typ': 'JWT', 'alg': 'none'}; 196 | 197 | /// Token issuer (standard `iss` claim). 198 | set issuer(String issuer) { 199 | _claims['iss'] = issuer; 200 | } 201 | 202 | /// Token audience (standard `aud` claim). 203 | set audience(String audience) { 204 | _claims['aud'] = audience; 205 | } 206 | 207 | /// Token issued at timestamp in seconds (standard `iat` claim). 208 | set issuedAt(DateTime issuedAt) { 209 | _claims['iat'] = secondsSinceEpoch(issuedAt); 210 | } 211 | 212 | /// Token expires timestamp in seconds (standard `exp` claim). 213 | set expiresAt(DateTime expiresAt) { 214 | _claims['exp'] = secondsSinceEpoch(expiresAt); 215 | } 216 | 217 | /// Sets value for standard `nbf` claim. 218 | set notBefore(DateTime notBefore) { 219 | _claims['nbf'] = secondsSinceEpoch(notBefore); 220 | } 221 | 222 | /// Sets standard `sub` claim value. 223 | set subject(String subject) { 224 | _claims['sub'] = subject; 225 | } 226 | 227 | /// Sets standard `jti` claim value. 228 | set id(String id) { 229 | _claims['jti'] = id; 230 | } 231 | 232 | /// Sets standard `typ` header. 233 | set typ(String typ) { 234 | setHeader('typ', typ); 235 | } 236 | 237 | /// Sets value of private (custom) claim. 238 | /// 239 | /// This method cannot be used to 240 | /// set values of standard (reserved) claims. 241 | void setClaim(String name, Object value) { 242 | if (JWT.reservedClaims.contains(name.toLowerCase())) { 243 | throw ArgumentError.value( 244 | name, 245 | 'name', 246 | 'Only custom claims can be set with setClaim.', 247 | ); 248 | } 249 | _claims[name] = value; 250 | } 251 | 252 | /// Sets value of a private (custom) header. 253 | /// 254 | /// This method cannot be used to update standard (reserved) headers. 255 | void setHeader(String name, Object value) { 256 | if (JWT.reservedHeaders.contains(name.toLowerCase())) { 257 | throw ArgumentError.value( 258 | name, 259 | 'name', 260 | 'Only custom headers can be set with setHeader.', 261 | ); 262 | } 263 | _headers[name] = value; 264 | } 265 | 266 | /// Builds and returns JWT. The token will not be signed. 267 | /// 268 | /// To create signed token use [getSignedToken] instead. 269 | JWT getToken() { 270 | final encodedHeader = _base64Unpadded(_jsonToBase64Url.encode(_headers)); 271 | final encodedPayload = _base64Unpadded(_jsonToBase64Url.encode(_claims)); 272 | return JWT._(encodedHeader, encodedPayload, null); 273 | } 274 | 275 | /// Builds and returns signed JWT. 276 | /// 277 | /// The token is signed with provided [signer]. 278 | /// 279 | /// To create unsigned token use [getToken]. 280 | JWT getSignedToken(JWTSigner signer) { 281 | // Set the algorithm and optionally kid headers for the resulting token. 282 | _headers['alg'] = signer.algorithm; 283 | if (signer.kid != null) { 284 | _headers['kid'] = signer.kid; 285 | } 286 | final encodedHeader = _base64Unpadded(_jsonToBase64Url.encode(_headers)); 287 | final encodedPayload = _base64Unpadded(_jsonToBase64Url.encode(_claims)); 288 | final body = '$encodedHeader.$encodedPayload'; 289 | final signature = _base64Unpadded(base64Url.encode(signer.sign(utf8.encode(body)))); 290 | return JWT._(encodedHeader, encodedPayload, signature); 291 | } 292 | } 293 | 294 | /// Validator for JSON Web Tokens. 295 | /// 296 | /// One must configure validator and provide values for claims that should be 297 | /// validated, except for `iat`, `exp` and `nbf` claims - these are always 298 | /// validated based on the value of [currentTime]. 299 | class JWTValidator { 300 | /// Current time used to validate token's `iat`, `exp` and `nbf` claims. 301 | final DateTime currentTime; 302 | String? issuer; 303 | String? audience; 304 | String? subject; 305 | String? id; 306 | 307 | /// Creates new validator. One can supply custom value for [currentTime] 308 | /// parameter, if not [DateTime.now] is used by default. 309 | JWTValidator({DateTime? currentTime}) : currentTime = currentTime ?? DateTime.now(); 310 | 311 | /// Validates provided [token] and returns a list of validation errors. 312 | /// Empty list indicates there were no validation errors. 313 | /// 314 | /// If [signer] parameter is provided then token signature 315 | /// will also be verified. Otherwise signature must be verified manually using 316 | /// [JWT.verify] method. 317 | 318 | Set validate(JWT token, {JWTSigner? signer}) { 319 | final errors = {}; 320 | final currentTimestamp = secondsSinceEpoch(currentTime); 321 | 322 | if (token.expiresAt != null && currentTimestamp >= token.expiresAt!) { 323 | errors.add('The token has expired.'); 324 | } 325 | 326 | if (token.issuedAt != null && currentTimestamp < token.issuedAt!) { 327 | errors.add('The token issuedAt time is in future.'); 328 | } 329 | 330 | if (token.notBefore != null && currentTimestamp < token.notBefore!) { 331 | errors.add('The token can not be accepted due to notBefore policy.'); 332 | } 333 | 334 | if (issuer != null && issuer != token.issuer) { 335 | errors.add('The token issuer is invalid.'); 336 | } 337 | 338 | if (audience != null && audience != token.audience) { 339 | errors.add('The token audience is invalid.'); 340 | } 341 | 342 | if (subject != null && subject != token.subject) { 343 | errors.add('The token subject is invalid.'); 344 | } 345 | 346 | if (id != null && id != token.id) { 347 | errors.add('The token unique identifier is invalid.'); 348 | } 349 | 350 | if (signer != null && !token.verify(signer)) { 351 | errors.add('The token signature is invalid.'); 352 | } 353 | 354 | return errors; 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /lib/src/es256.dart: -------------------------------------------------------------------------------- 1 | import 'package:jose/jose.dart'; 2 | 3 | import 'signer.dart'; 4 | 5 | /// The ES256 signer for JWTs. 6 | class JWTEcdsaSha256Signer implements JWTSigner { 7 | final JsonWebKey _jwk; 8 | 9 | @override 10 | String get algorithm => 'ES256'; 11 | 12 | @override 13 | final String? kid; 14 | 15 | JWTEcdsaSha256Signer.fromJWK(JsonWebKey jwk, this.kid) : _jwk = jwk; 16 | 17 | factory JWTEcdsaSha256Signer({required String pem, String? kid}) { 18 | return JWTEcdsaSha256Signer.fromJWK(JsonWebKey.fromPem(pem, keyId: kid), kid); 19 | } 20 | 21 | @override 22 | List sign(List body) { 23 | return _jwk.sign(body, algorithm: algorithm); 24 | } 25 | 26 | @override 27 | bool verify(List body, List signature) { 28 | return _jwk.verify(body, signature, algorithm: algorithm); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/hs256.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:crypto/crypto.dart'; 4 | 5 | import 'signer.dart'; 6 | 7 | /// Signer implementing HMAC encryption using SHA256 hashing. 8 | class JWTHmacSha256Signer implements JWTSigner { 9 | final List secret; 10 | @override 11 | final String? kid; 12 | 13 | JWTHmacSha256Signer(String secret, {this.kid}) : secret = utf8.encode(secret); 14 | 15 | @override 16 | String get algorithm => 'HS256'; 17 | 18 | @override 19 | List sign(List body) { 20 | final hmac = Hmac(sha256, secret); 21 | return hmac.convert(body).bytes; 22 | } 23 | 24 | @override 25 | bool verify(List body, List signature) { 26 | final actual = sign(body); 27 | if (actual.length == signature.length) { 28 | // constant-time comparison 29 | var isEqual = true; 30 | for (var i = 0; i < actual.length; i++) { 31 | if (actual[i] != signature[i]) isEqual = false; 32 | } 33 | return isEqual; 34 | } else { 35 | return false; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/rs256.dart: -------------------------------------------------------------------------------- 1 | import 'package:jose/jose.dart'; 2 | 3 | import 'signer.dart'; 4 | 5 | class JWTRsaSha256Signer implements JWTSigner { 6 | final JsonWebKey _jwk; 7 | 8 | @override 9 | final String? kid; 10 | 11 | JWTRsaSha256Signer.fromJWK(JsonWebKey jwk, this.kid) : _jwk = jwk; 12 | 13 | /// Creates new signer from specified PEM string 14 | factory JWTRsaSha256Signer({required String pem, String? kid}) { 15 | return JWTRsaSha256Signer.fromJWK(JsonWebKey.fromPem(pem, keyId: kid), kid); 16 | } 17 | 18 | @override 19 | String get algorithm => 'RS256'; 20 | 21 | @override 22 | List sign(List body) { 23 | return _jwk.sign(body, algorithm: algorithm); 24 | } 25 | 26 | @override 27 | bool verify(List body, List signature) { 28 | return _jwk.verify(body, signature, algorithm: algorithm); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/signer.dart: -------------------------------------------------------------------------------- 1 | /// Signer interface for JWT. 2 | abstract class JWTSigner { 3 | /// The algorithm of this signer. 4 | String get algorithm; 5 | 6 | /// Optinal `kid` header to set in the signed token. 7 | String? get kid; 8 | 9 | List sign(List body); 10 | 11 | bool verify(List body, List signature); 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | int secondsSinceEpoch(DateTime dateTime) => 2 | (dateTime.millisecondsSinceEpoch / 1000).floor(); 3 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: corsac_jwt 2 | version: 2.0.1 3 | description: Lightweight implementation of JSON Web Tokens (JWT). 4 | homepage: https://github.com/corsac-dart/jwt 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | jose: ^0.3.4 11 | crypto: ^3.0.0 12 | logging: ^1.0.0 13 | pointycastle: ^3.9.1 14 | 15 | dev_dependencies: 16 | coverage: ^1.0.0 17 | lints: ^5.0.0 18 | test: ^1.17.5 19 | 20 | false_secrets: 21 | - /test/resources/*.pem 22 | -------------------------------------------------------------------------------- /test/es256_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:corsac_jwt/corsac_jwt.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('ES256: ', () { 8 | final pem = File('test/resources/ec_private_key.pem').readAsStringSync(); 9 | final signer = JWTEcdsaSha256Signer(pem: pem); 10 | 11 | test('it can sign and verify JWT with ES256', () { 12 | final builder = JWTBuilder() 13 | ..issuer = 'abc.com' 14 | ..expiresAt = DateTime.now().add(Duration(minutes: 3)); 15 | final token = builder.getSignedToken(signer); 16 | 17 | expect(token.algorithm, 'ES256'); 18 | expect(token.verify(signer), isTrue); 19 | }); 20 | 21 | test('it supports PKCS8 pem format', () { 22 | final pem = File('test/resources/ec_private_key_pkcs8.pem').readAsStringSync(); 23 | final signer = JWTEcdsaSha256Signer(pem: pem); 24 | 25 | final builder = JWTBuilder() 26 | ..issuer = 'abc.com' 27 | ..expiresAt = DateTime.now().add(Duration(minutes: 3)); 28 | final token = builder.getSignedToken(signer); 29 | 30 | expect(token.algorithm, 'ES256'); 31 | expect(token.verify(signer), isTrue); 32 | }); 33 | 34 | test('it handles exceptions on verification', () { 35 | const jwtString = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.' 36 | 'eyJpZCI6ImY0MTJiMDViZTRhYTIwZmJlNmEwMDUyZjA5YjdlMzhjOTlmYjdhNjEiLCJq' 37 | 'dGkiOiJmNDEyYjA1YmU0YWEyMGZiZTZhMDA1MmYwOWI3ZTM4Yzk5ZmI3YTYxIiwiaXNz' 38 | 'IjoiaHR0cDovL2FwaS5mb29iYXIuY29tIn0.' 39 | 'B2vu-KIEFvY3T_EPAYFw48OS7Q7kKVbbOSMIhptyIHZximJ6hkFCBTr2Czz5ArbEYJfA' 40 | 'L8_3ZtV3Il6YxE5XQF5hVFNet-Ypt-RzRXPtKMAqt_iiu4C4qg7qes9penNHgu2hvZbQ' 41 | '2FpSPGKrt_ozNehy52YysAKmzKj2ZSelru81ap80pgkYC6Eql8DGIqgz6OVHj_9NRQHq' 42 | 'J2OHDi_nLjYSQW6BtKSA-nmaySr_wn2rMe2xSaf2iA3mPiheCN6yL8yvwcGziNX3wtya' 43 | 'huL1vxg_wJ-sD-py9X7bLu9OmoWds76gxAQQh0Wi694FXQ5p5e4ub0BDlJ9Pv2vr2uPz' 44 | 'KL6OQpY4wYBYNhe4UF2QxfFjwnYWITo6O6_tiQtH7Q6WNqF27OfriGYNQbiOgD0icpRr' 45 | 'L9w3JI907G4bO3bm0mCIimBbLgr0B_pM4Pr5wcbhXC71yZ0j3ODlfXJ9qnO9G5aAGC9w' 46 | 'wFsGT0jgv0ydReDwMgasMN4lYl_iUkxckhHR6ys3wg8FG6SG818CvG-jOkrJMIjNXD9n' 47 | 'ZMVXPH-tqP-8_60SN8G5vPVdB0nxwL6FprWIc6jC-eXPsATN4E4YJnu5Wnsd4VEPKZVX' 48 | '4Q1AFVOO6dgDxZ7jGStHx50Q1zh1GuNIeEmSnWsVsgtkhTymyZNQSvoIiZnq-wcNtB-Y' 49 | 'FvY'; 50 | final token = JWT.parse(jwtString); 51 | expect(token.verify(signer), isFalse); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/jwt_test.dart: -------------------------------------------------------------------------------- 1 | library corsac_jwt.test; 2 | 3 | import 'package:corsac_jwt/corsac_jwt.dart'; 4 | import 'package:corsac_jwt/src/utils.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('$JWT', () { 9 | final now = DateTime.now(); 10 | late JWTBuilder builder; 11 | 12 | setUp(() { 13 | builder = JWTBuilder() 14 | ..issuer = 'https://mycompany.com' 15 | ..audience = 'people' 16 | ..issuedAt = now 17 | ..expiresAt = now.add(Duration(seconds: 10)) 18 | ..notBefore = now.add(Duration(seconds: 5)) 19 | ..id = 'identifier' 20 | ..subject = 'subj'; 21 | }); 22 | 23 | test('JWTError toString', () { 24 | final error = JWTError('failed'); 25 | expect(error.toString(), 'JWTError: failed'); 26 | }); 27 | 28 | test('JWTBuilder can build unsigned token', () { 29 | final token = builder.getToken(); 30 | expect(token, const TypeMatcher()); 31 | expect(token.issuer, equals('https://mycompany.com')); 32 | }); 33 | 34 | test('JWTBuilder can build signed token', () { 35 | final signer = JWTHmacSha256Signer('secret1'); 36 | final token = builder.getSignedToken(signer); 37 | 38 | expect(token, const TypeMatcher()); 39 | expect(token.issuer, equals('https://mycompany.com')); 40 | expect(token.verify(signer), isTrue); 41 | expect(token.verify(JWTHmacSha256Signer('invalid')), isFalse); 42 | }); 43 | 44 | test('it parses string token', () { 45 | final signer = JWTHmacSha256Signer('secret1'); 46 | const stringToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL215Y29tcGF' 47 | 'ueS5jb20ifQ.R7OVbiAKtvSkE-qF0fCkZP_m2JGrHobbRayHhEsKuKU'; 48 | final token = JWT.parse(stringToken); 49 | 50 | expect(token, const TypeMatcher()); 51 | expect(token.issuer, equals('https://mycompany.com')); 52 | expect(token.verify(signer), isTrue); 53 | expect(token.verify(JWTHmacSha256Signer('invalid')), isFalse); 54 | }); 55 | 56 | test('it parses another token', () { 57 | final signer = JWTHmacSha256Signer('secret'); 58 | const stringToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL215Zm9vYmF' 59 | 'yLmNvbSIsImlhdCI6MTQ1NTIzMjI2NywiZXhwIjoxNDU1MjM0MDY3LCJuYmYiOjE0NTU' 60 | 'yMzIyMzcsImJvZHkiOnsiYmxhaCI6ImJvb2EifX0.PXbDbE7YapU-6WvRqbdQ2OC1N2D' 61 | 'ScadvuQUqTHXopNc'; 62 | 63 | final token = JWT.parse(stringToken); 64 | 65 | expect(token, const TypeMatcher()); 66 | expect(token.issuer, equals('https://myfoobar.com')); 67 | expect(token.verify(signer), isTrue); 68 | expect(token.verify(JWTHmacSha256Signer('invalid')), isFalse); 69 | expect(token.toString(), stringToken); 70 | }); 71 | 72 | test('it throws JWTError if token is invalid', () { 73 | const badToken1 = 'invalid.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL21' 74 | '5Y29tcGFueS5jb20ifQ.R7OVbiAKtvSkE-qF0fCkZP_m2JGrHobbRayHhEsKuKU'; 75 | const badToken2 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.invalid'; 76 | expect(() => JWT.parse(badToken1), throwsA(const TypeMatcher())); 77 | expect(() => JWT.parse(badToken2), throwsA(const TypeMatcher())); 78 | }); 79 | 80 | test('it supports all standard claims', () { 81 | final token = builder.getToken(); 82 | expect(token, const TypeMatcher()); 83 | expect(token.issuer, equals('https://mycompany.com')); 84 | expect(token.audience, equals('people')); 85 | expect(token.issuedAt, equals(secondsSinceEpoch(now))); 86 | expect(token.expiresAt, equals(secondsSinceEpoch(now) + 10)); 87 | expect(token.notBefore, equals(secondsSinceEpoch(now) + 5)); 88 | expect(token.id, equals('identifier')); 89 | expect(token.subject, equals('subj')); 90 | expect(token.algorithm, equals('none')); 91 | }); 92 | 93 | test('it prevents setting standard claims using setClaim', () { 94 | expect(() => builder.setClaim('iss', 'bad'), throwsArgumentError); 95 | }); 96 | 97 | test('it supports custom (private) claims', () { 98 | builder 99 | ..issuer = 'https://foobar.com' 100 | ..setClaim('pld', 'payload') 101 | ..setClaim('map', {'key': 'value'}); 102 | final token = builder.getToken(); 103 | expect(token.issuer, equals('https://foobar.com')); 104 | expect(token.getClaim('pld'), equals('payload')); 105 | expect(token.getClaim('map'), equals({'key': 'value'})); 106 | 107 | final stringToken = token.toString(); 108 | final parsedToken = JWT.parse(stringToken); 109 | 110 | expect(parsedToken.issuer, equals('https://foobar.com')); 111 | expect(parsedToken.getClaim('pld'), equals('payload')); 112 | expect(parsedToken.getClaim('map'), equals({'key': 'value'})); 113 | 114 | final claims = parsedToken.claims; 115 | 116 | expect(claims['pld'], equals('payload')); 117 | expect(claims['map'], equals({'key': 'value'})); 118 | 119 | claims['pld'] = 'good times!'; 120 | expect(claims['pld'], equals('good times!')); 121 | expect(parsedToken.getClaim('pld'), equals('payload')); 122 | }); 123 | 124 | test('it supports custom headers', () { 125 | builder 126 | ..issuer = 'https://foobar.com' 127 | ..setHeader('x5t', 'payload'); 128 | final token = builder.getToken(); 129 | expect(token.issuer, equals('https://foobar.com')); 130 | expect(token.headers['x5t'], equals('payload')); 131 | 132 | final stringToken = token.toString(); 133 | final parsedToken = JWT.parse(stringToken); 134 | 135 | expect(parsedToken.issuer, equals('https://foobar.com')); 136 | expect(parsedToken.headers['x5t'], equals('payload')); 137 | }); 138 | 139 | test('it throws error for updating reserved headers', () { 140 | expect(() { 141 | builder.setHeader('alg', 'error'); 142 | }, throwsArgumentError); 143 | }); 144 | 145 | test('validator uses current time by default', () { 146 | final validator = JWTValidator(); 147 | expect(validator.currentTime, isNotNull); 148 | }); 149 | 150 | test('iss claim is validated', () { 151 | final token = builder.getToken(); 152 | final time = DateTime.now().add(Duration(seconds: 6)); 153 | final validator = JWTValidator(currentTime: time)..issuer = 'wrong'; 154 | var errors = validator.validate(token); 155 | expect(errors, isNotEmpty); 156 | expect(errors, contains('The token issuer is invalid.')); 157 | 158 | validator.issuer = 'https://mycompany.com'; 159 | errors = validator.validate(token); 160 | expect(errors, isEmpty); 161 | }); 162 | 163 | test('exp claim is validated', () { 164 | final token = builder.getToken(); 165 | var validator = JWTValidator(currentTime: DateTime.now().add(Duration(seconds: 20))); 166 | var errors = validator.validate(token); 167 | expect(errors, isNotEmpty); 168 | expect(errors, contains('The token has expired.')); 169 | 170 | validator = JWTValidator(currentTime: DateTime.now().add(Duration(seconds: 5))); 171 | errors = validator.validate(token); 172 | expect(errors, isEmpty); 173 | }); 174 | 175 | test('iat claim is validated', () { 176 | final token = builder.getToken(); 177 | 178 | var validator = JWTValidator(currentTime: DateTime.now().subtract(Duration(seconds: 1))); 179 | var errors = validator.validate(token); 180 | expect(errors, isNotEmpty); 181 | expect(errors, contains('The token issuedAt time is in future.')); 182 | 183 | final time = DateTime.now().add(Duration(seconds: 6)); 184 | validator = JWTValidator(currentTime: time); 185 | errors = validator.validate(token); 186 | expect(errors, isEmpty); 187 | }); 188 | 189 | test('nbf claim is validated', () { 190 | final token = builder.getToken(); 191 | var validator = JWTValidator(currentTime: DateTime.now().add(Duration(seconds: 1))); 192 | var errors = validator.validate(token); 193 | expect(errors, isNotEmpty); 194 | expect(errors, contains('The token can not be accepted due to notBefore policy.')); 195 | 196 | final time = DateTime.now().add(Duration(seconds: 6)); 197 | validator = JWTValidator(currentTime: time); 198 | errors = validator.validate(token); 199 | expect(errors, isEmpty); 200 | }); 201 | 202 | test('aud claim is validated', () { 203 | final token = builder.getToken(); 204 | final time = DateTime.now().add(Duration(seconds: 6)); 205 | final validator = JWTValidator(currentTime: time)..audience = 'wrong'; 206 | var errors = validator.validate(token); 207 | expect(errors, isNotEmpty); 208 | expect(errors, contains('The token audience is invalid.')); 209 | 210 | validator.audience = 'people'; 211 | errors = validator.validate(token); 212 | expect(errors, isEmpty); 213 | }); 214 | 215 | test('sub claim is validated', () { 216 | final token = builder.getToken(); 217 | final time = DateTime.now().add(Duration(seconds: 6)); 218 | final validator = JWTValidator(currentTime: time)..subject = 'wrong'; 219 | var errors = validator.validate(token); 220 | expect(errors, isNotEmpty); 221 | expect(errors, contains('The token subject is invalid.')); 222 | 223 | validator.subject = 'subj'; 224 | errors = validator.validate(token); 225 | expect(errors, isEmpty); 226 | }); 227 | 228 | test('jti claim is validated', () { 229 | final token = builder.getToken(); 230 | final time = DateTime.now().add(Duration(seconds: 6)); 231 | final validator = JWTValidator(currentTime: time)..id = 'wrong'; 232 | var errors = validator.validate(token); 233 | expect(errors, isNotEmpty); 234 | expect(errors, contains('The token unique identifier is invalid.')); 235 | 236 | validator.id = 'identifier'; 237 | errors = validator.validate(token); 238 | expect(errors, isEmpty); 239 | }); 240 | 241 | test('signature is validated', () { 242 | final signer = JWTHmacSha256Signer('secret'); 243 | 244 | final token = builder.getSignedToken(signer); 245 | final time = DateTime.now().add(Duration(seconds: 6)); 246 | final validator = JWTValidator(currentTime: time); 247 | var errors = validator.validate(token, signer: JWTHmacSha256Signer('invalid')); 248 | expect(errors, isNotEmpty); 249 | expect(errors, contains('The token signature is invalid.')); 250 | 251 | errors = validator.validate(token, signer: signer); 252 | expect(errors, isEmpty); 253 | }); 254 | 255 | test('it provides read-only access to headers', () { 256 | builder.issuer = 'https://foobar.com'; 257 | final token = builder.getToken(); 258 | 259 | final headers = token.headers; 260 | expect(headers['typ'], 'JWT'); 261 | expect(headers['alg'], 'none'); 262 | expect(() { 263 | headers['kid'] = 'boom'; 264 | }, throwsUnsupportedError); 265 | }); 266 | 267 | test('it verifies kid header', () { 268 | final signer1 = JWTHmacSha256Signer('secret', kid: 'key1'); 269 | final signer2 = JWTHmacSha256Signer('secret', kid: 'key2'); 270 | final signer3 = JWTHmacSha256Signer('secret'); 271 | 272 | final token = builder.getSignedToken(signer1); 273 | expect(token.verify(signer1), isTrue); 274 | expect(token.verify(signer2), isFalse); 275 | expect(token.verify(signer3), isFalse); 276 | }); 277 | }); 278 | } 279 | -------------------------------------------------------------------------------- /test/resources/ec_private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIKY2Qr6Sy56ADtJ+S+FogyHEDYSsa0m3zZ3DiG9/bpKXoAoGCCqGSM49 3 | AwEHoUQDQgAEj468ClNGPZhBeecp7qrYs2a1Cp4JPuXrqaa9FLSzuWYX4bISGV8U 4 | KAwPvYYaJKSVZdv6dP1hcC4zxys18B5GhQ== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/resources/ec_private_key_pkcs8.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgpjZCvpLLnoAO0n5L 3 | 4WiDIcQNhKxrSbfNncOIb39ukpehRANCAASPjrwKU0Y9mEF55ynuqtizZrUKngk+ 4 | 5euppr0UtLO5ZhfhshIZXxQoDA+9hhokpJVl2/p0/WFwLjPHKzXwHkaF 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/resources/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAstsFOYl5GzHkkxNqFhDMpTRbeTcESFqoBxzxeq2dzjZYhZEK 3 | HmCDMLS/9uNvkAltYy6NU6fB4A6+yxGdzMLa6/Kwx53hcH/Rsbi0JVmBbOSi76on 4 | b8w0Cigr2saAvxIhD/v/nC8Rds0KiJw7NJsIjwRPG/S3/EwdgNqNIiCrpf3ySvHI 5 | bd7Py73lIId1fbSUEBulg2hTZ3dNNERloTqHLceFupAi66PeyP3adcGCndXO1UT3 6 | qk0GInbc8MMdoggc/YXLsMbgXNXhQ+lXd9wXJJY2jm2lbpJKBQ87To4CDDJd/m7C 7 | rJ/XK2nl7A11/C8zONfITSNfyqJWBlFMmBr2nwIDAQABAoIBAC31KXqHYJ8kSt+f 8 | 9XqGBo/MzFRxVqBg6xwoy8QCKLJ8NHLdugPkONGjIFCIUHpqcbkylQ4R5Td1koEL 9 | ncinqSlHyzT/1JXbwj4wf9m5DhC1D0kWJ9wKVLTnbZ1htGNkfSpmTmALmCk2tqYz 10 | vuLdk21sZgeA8mXaCRq7DdjfHDwifs7ZcpW10WnqTZxIOWM0xzhQKXAfquTkS8pu 11 | kim+4FGJ0YN4K7e3po3EajqgVmBZfjfpQWrRYbOCtu6ClenCGUmfZ4gCi+M+q8xS 12 | OROY3V2kv8I123BTBR1wtQUwkW52XGHRu5px+l7p8TIEbxf99xqtSVF931jHUPKz 13 | /MFl9gECgYEA2BfeZMxCq/0RQJ+yFLfXH2HAnBbTG2qA9prfhdPmeF6XpYY6unaP 14 | LZowlFjIzm3uIikwUzzT+ItTNaXvGtRcFc2GtZOVtJq+ErpueM0p5PbgLCSe7ISx 15 | he6PLWc3jY98IUOHZGpBhTgE6L9A2ZRvnQ/2nHJpTqp4L6BYPOaaiaECgYEA0+Kv 16 | KA15+rSXnEFC/R4vgligVLMaV2rLCGuCW1u5VPmZJe5hGG+ttdfsaE02tl2iYCem 17 | US5P4OIb1T6Fn574uJFHL8ijTvBEk2L1QRYspdt3keiRGWHQYwguxyfUl0ip6S1+ 18 | dBYjJFTdQ8Xy5yGfAeandwf64AN72WKdQAOsGD8CgYEAspVfPKw1+U8GQAL6N/cK 19 | eKvfct/GDWVCOQsa6M2LLTT3XFsHE+xBPW2s8hxBr5/X6jFh95hQkZoK5U5BwUl4 20 | 5KfayRqz4PL1XCLogzsCgW+pKbIGCO9MiqPxfZNMrNfEvPTC4rCRf1ghbnwISwhK 21 | CWIU64v+DX4CH1IDOilV06ECgYEAlg2XFbpVhCKYq+Pb2P2jj5/MC6+7G+VZW+En 22 | NCPFIFSTDLXAtVmBn6IGnebwtD2jXI03z44Iq631IBNi9iPS6IKZ81EXtjOZnPcb 23 | 0LgvblX6W65j86G9viRxXEDs4SZojeXWA8gZowUmnXR2DRFWVjZOqpFQLYzKDK01 24 | x7vSCGkCgYBqMxOupv6xBXNIi6T90odiI1CmO9q0dZ3zddplTx1k8cBHMY4iro2a 25 | EUSLDusaW1OacKOz9oddOavn0KIDBarAoY+8pD1NScrFU2xT8Z4mZ8UTmMIqKzD5 26 | 8VcLWnhAWnrg/Oyhyb/4kW3L7e7SIWi2Sg2tQuyLikYOoC0YNW7QuA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/resources/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAstsFOYl5GzHkkxNqFhDM 3 | pTRbeTcESFqoBxzxeq2dzjZYhZEKHmCDMLS/9uNvkAltYy6NU6fB4A6+yxGdzMLa 4 | 6/Kwx53hcH/Rsbi0JVmBbOSi76onb8w0Cigr2saAvxIhD/v/nC8Rds0KiJw7NJsI 5 | jwRPG/S3/EwdgNqNIiCrpf3ySvHIbd7Py73lIId1fbSUEBulg2hTZ3dNNERloTqH 6 | LceFupAi66PeyP3adcGCndXO1UT3qk0GInbc8MMdoggc/YXLsMbgXNXhQ+lXd9wX 7 | JJY2jm2lbpJKBQ87To4CDDJd/m7CrJ/XK2nl7A11/C8zONfITSNfyqJWBlFMmBr2 8 | nwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/rs256_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:corsac_jwt/corsac_jwt.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('RS256: ', () { 8 | final priv = File('test/resources/private.pem').readAsStringSync(); 9 | final signer = JWTRsaSha256Signer(pem: priv); 10 | 11 | test('it can sign and verify JWT with RS256', () { 12 | final builder = JWTBuilder() 13 | ..issuer = 'abc.com' 14 | ..expiresAt = DateTime.now().add(Duration(minutes: 3)); 15 | final token = builder.getSignedToken(signer); 16 | 17 | expect(token.algorithm, 'RS256'); 18 | expect(token.verify(signer), isTrue); 19 | }); 20 | 21 | test('it can verify with only public key pem', () { 22 | final builder = JWTBuilder() 23 | ..issuer = 'abc.com' 24 | ..expiresAt = DateTime.now().add(Duration(minutes: 3)); 25 | final token = builder.getSignedToken(signer); 26 | 27 | final pub = File('test/resources/public.pem').readAsStringSync(); 28 | final verifier = JWTRsaSha256Signer(pem: pub); 29 | 30 | expect(token.algorithm, 'RS256'); 31 | expect(token.verify(verifier), isTrue); 32 | 33 | expect(() { 34 | builder.getSignedToken(verifier); 35 | }, throwsStateError); 36 | }); 37 | 38 | test('it handles exceptions on verification', () { 39 | const jwtString = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.' 40 | 'eyJpZCI6ImY0MTJiMDViZTRhYTIwZmJlNmEwMDUyZjA5YjdlMzhjOTlmYjdhNjEiLCJq' 41 | 'dGkiOiJmNDEyYjA1YmU0YWEyMGZiZTZhMDA1MmYwOWI3ZTM4Yzk5ZmI3YTYxIiwiaXNz' 42 | 'IjoiaHR0cDovL2FwaS5mb29iYXIuY29tIn0.' 43 | 'B2vu-KIEFvY3T_EPAYFw48OS7Q7kKVbbOSMIhptyIHZximJ6hkFCBTr2Czz5ArbEYJfA' 44 | 'L8_3ZtV3Il6YxE5XQF5hVFNet-Ypt-RzRXPtKMAqt_iiu4C4qg7qes9penNHgu2hvZbQ' 45 | '2FpSPGKrt_ozNehy52YysAKmzKj2ZSelru81ap80pgkYC6Eql8DGIqgz6OVHj_9NRQHq' 46 | 'J2OHDi_nLjYSQW6BtKSA-nmaySr_wn2rMe2xSaf2iA3mPiheCN6yL8yvwcGziNX3wtya' 47 | 'huL1vxg_wJ-sD-py9X7bLu9OmoWds76gxAQQh0Wi694FXQ5p5e4ub0BDlJ9Pv2vr2uPz' 48 | 'KL6OQpY4wYBYNhe4UF2QxfFjwnYWITo6O6_tiQtH7Q6WNqF27OfriGYNQbiOgD0icpRr' 49 | 'L9w3JI907G4bO3bm0mCIimBbLgr0B_pM4Pr5wcbhXC71yZ0j3ODlfXJ9qnO9G5aAGC9w' 50 | 'wFsGT0jgv0ydReDwMgasMN4lYl_iUkxckhHR6ys3wg8FG6SG818CvG-jOkrJMIjNXD9n' 51 | 'ZMVXPH-tqP-8_60SN8G5vPVdB0nxwL6FprWIc6jC-eXPsATN4E4YJnu5Wnsd4VEPKZVX' 52 | '4Q1AFVOO6dgDxZ7jGStHx50Q1zh1GuNIeEmSnWsVsgtkhTymyZNQSvoIiZnq-wcNtB-Y' 53 | 'FvY'; 54 | final token = JWT.parse(jwtString); 55 | expect(token.verify(signer), isFalse); 56 | }); 57 | }); 58 | } 59 | --------------------------------------------------------------------------------