diff --git a/lib/src/jwt.dart b/lib/src/jwt.dart index b8b937e..183f6ec 100644 --- a/lib/src/jwt.dart +++ b/lib/src/jwt.dart @@ -20,6 +20,9 @@ class JWT { /// value is a timestamp (number of seconds since epoch) in UTC if /// [issueAtUtc] is true, it is compared to the value of the 'iat' claim. /// Verification fails if the 'iat' claim is before [issueAt]. + /// + /// If the embedded `payload` is not a JSON map (but rather just a plain string), + /// none of the verifications are executed. In that case only the signature is verified. static JWT verify( String token, JWTKey key, { @@ -35,6 +38,13 @@ class JWT { }) { try { final parts = token.split('.'); + + if (parts.length != 3) { + throw JWTInvalidException( + 'token does not use JWS Compact Serialization', + ); + } + final header = jsonBase64.decode(base64Padded(parts[0])); if (header == null || header is! Map) { @@ -54,10 +64,11 @@ class JWT { throw JWTInvalidException('invalid signature'); } - dynamic payload; + Object payload; try { - payload = jsonBase64.decode(base64Padded(parts[1])); + payload = + jsonBase64.decode(base64Padded(parts[1])) as Map; } catch (ex) { payload = utf8.decode(base64Url.decode(base64Padded(parts[1]))); } @@ -194,18 +205,15 @@ class JWT { /// /// This also sets [JWT.audience], [JWT.subject], [JWT.issuer], and /// [JWT.jwtId] even though they are not verified. Use with caution. + /// + /// This methods only supports map payloads. For `String` payloads use `verify`. static JWT decode(String token) { try { final parts = token.split('.'); - var header = jsonBase64.decode(base64Padded(parts[0])); - - dynamic payload; + final header = jsonBase64.decode(base64Padded(parts[0])); - try { - payload = jsonBase64.decode(base64Padded(parts[1])); - } catch (ex) { - payload = utf8.decode(base64Url.decode(base64Padded(parts[1]))); - } + final payload = + (jsonBase64.decode(base64Padded(parts[1])) as Map); final audiance = _parseAud(payload['aud']); final issuer = payload['iss']?.toString(); @@ -240,16 +248,36 @@ class JWT { /// JSON Web Token JWT( - this.payload, { + Object payload, { this.audience, this.subject, this.issuer, this.jwtId, this.header, - }); + }) { + this.payload = payload; + } + + late Object _payload; - /// Custom claims - dynamic payload; + /// The token's payload, either as a `Map` or plain `String` + /// (in case it was not a JSON-encoded map). + /// + /// If it's a map, it has all claims, containing the utilized registered claims + /// as well custom ones added. + Object get payload => _payload; + + void set payload(Object value) { + if (value is String) { + _payload = value; + } else if (value is Map) { + _payload = Map.from(value); + } else { + throw Exception( + 'Unexpected `payload` type `${value.runtimeType}`, must be either `String` or `Map`', + ); + } + } /// Audience claim Audience? audience; @@ -281,7 +309,8 @@ class JWT { bool noIssueAt = false, }) { try { - if (payload is Map || payload is Map) { + var payload = this.payload; + if (payload is Map) { try { payload = Map.from(payload); diff --git a/test/create_test.dart b/test/create_test.dart new file mode 100644 index 0000000..5ad2538 --- /dev/null +++ b/test/create_test.dart @@ -0,0 +1,86 @@ +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:test/test.dart'; + +void main() { + group('Create a JWT', () { + group('Payload', () { + group('Map', () { + test('Works as a payload, but gets converted', () { + final payload = { + 'foo': 'bar', + }; + + final jwt = JWT(payload); + + expect(jwt.payload, isA>()); + expect(jwt.payload, payload); + }); + }); + + group('Map', () { + test('Works as a payload', () { + final payload = { + 'foo': 'bar', + 'iat': 1234, + }; + + final jwt = JWT(payload); + + expect(jwt.payload, isA>()); + expect(jwt.payload, payload); + }); + + test('Gets copied internally', () { + final payload = { + 'foo': 'bar', + 'iat': 1234, + }; + + final jwt = JWT(payload); + + expect(jwt.payload, isA>()); + expect(jwt.payload, payload); + expect(identical(jwt.payload, payload), isFalse); + + payload['new_key'] = true; + + expect(jwt.payload, hasLength(2)); + }); + }); + + group('Map', () { + test('Does not work as a payload', () { + final payload = { + 123: 'bar', + }; + + expect( + () => JWT(payload), + throwsA(isA()), + ); + }); + }); + + group('String', () { + test('Works as a payload', () { + final payload = 'asdf123'; + + final jwt = JWT(payload); + + expect(jwt.payload, payload); + }); + }); + + group('int', () { + test('Does not work as a payload', () { + final payload = 1234; + + expect( + () => JWT(payload), + throwsA(isA()), + ); + }); + }); + }); + }); +}