Skip to content

Commit 16c496e

Browse files
committed
Document and enforce JWT payload type to be either String or Map<String, dynamic>
1 parent ca7b853 commit 16c496e

File tree

2 files changed

+130
-15
lines changed

2 files changed

+130
-15
lines changed

lib/src/jwt.dart

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class JWT {
2020
/// value is a timestamp (number of seconds since epoch) in UTC if
2121
/// [issueAtUtc] is true, it is compared to the value of the 'iat' claim.
2222
/// Verification fails if the 'iat' claim is before [issueAt].
23+
///
24+
/// If the embedded `payload` is not a JSON map (but rather just a plain string),
25+
/// none of the verifications are executed. In that case only the signature is verified.
2326
static JWT verify(
2427
String token,
2528
JWTKey key, {
@@ -35,6 +38,13 @@ class JWT {
3538
}) {
3639
try {
3740
final parts = token.split('.');
41+
42+
if (parts.length != 3) {
43+
throw JWTInvalidException(
44+
'token does not use JWS Compact Serialization',
45+
);
46+
}
47+
3848
final header = jsonBase64.decode(base64Padded(parts[0]));
3949

4050
if (header == null || header is! Map<String, dynamic>) {
@@ -54,10 +64,11 @@ class JWT {
5464
throw JWTInvalidException('invalid signature');
5565
}
5666

57-
dynamic payload;
67+
Object payload;
5868

5969
try {
60-
payload = jsonBase64.decode(base64Padded(parts[1]));
70+
payload =
71+
jsonBase64.decode(base64Padded(parts[1])) as Map<String, dynamic>;
6172
} catch (ex) {
6273
payload = utf8.decode(base64Url.decode(base64Padded(parts[1])));
6374
}
@@ -194,18 +205,15 @@ class JWT {
194205
///
195206
/// This also sets [JWT.audience], [JWT.subject], [JWT.issuer], and
196207
/// [JWT.jwtId] even though they are not verified. Use with caution.
208+
///
209+
/// This methods only supports map payloads. For `String` payloads use `verify`.
197210
static JWT decode(String token) {
198211
try {
199212
final parts = token.split('.');
200-
var header = jsonBase64.decode(base64Padded(parts[0]));
201-
202-
dynamic payload;
213+
final header = jsonBase64.decode(base64Padded(parts[0]));
203214

204-
try {
205-
payload = jsonBase64.decode(base64Padded(parts[1]));
206-
} catch (ex) {
207-
payload = utf8.decode(base64Url.decode(base64Padded(parts[1])));
208-
}
215+
final payload =
216+
(jsonBase64.decode(base64Padded(parts[1])) as Map<String, dynamic>);
209217

210218
final audiance = _parseAud(payload['aud']);
211219
final issuer = payload['iss']?.toString();
@@ -240,16 +248,36 @@ class JWT {
240248

241249
/// JSON Web Token
242250
JWT(
243-
this.payload, {
251+
Object payload, {
244252
this.audience,
245253
this.subject,
246254
this.issuer,
247255
this.jwtId,
248256
this.header,
249-
});
257+
}) {
258+
this.payload = payload;
259+
}
260+
261+
late Object _payload;
250262

251-
/// Custom claims
252-
dynamic payload;
263+
/// The token's payload, either as a `Map<String, dynamic>` or plain `String`
264+
/// (in case it was not a JSON-encoded map).
265+
///
266+
/// If it's a map, it has all claims, containing the utilized registered claims
267+
/// as well custom ones added.
268+
Object get payload => _payload;
269+
270+
void set payload(Object value) {
271+
if (value is String) {
272+
_payload = value;
273+
} else if (value is Map) {
274+
_payload = Map<String, dynamic>.from(value);
275+
} else {
276+
throw Exception(
277+
'Unexpected `payload` type `${value.runtimeType}`, must be either `String` or `Map<String, *>`',
278+
);
279+
}
280+
}
253281

254282
/// Audience claim
255283
Audience? audience;
@@ -281,7 +309,8 @@ class JWT {
281309
bool noIssueAt = false,
282310
}) {
283311
try {
284-
if (payload is Map<String, dynamic> || payload is Map<dynamic, dynamic>) {
312+
var payload = this.payload;
313+
if (payload is Map<String, dynamic>) {
285314
try {
286315
payload = Map<String, dynamic>.from(payload);
287316

test/create_test.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group('Create a JWT', () {
6+
group('Payload', () {
7+
group('Map<String, String>', () {
8+
test('Works as a payload, but gets converted', () {
9+
final payload = <String, String>{
10+
'foo': 'bar',
11+
};
12+
13+
final jwt = JWT(payload);
14+
15+
expect(jwt.payload, isA<Map<String, dynamic>>());
16+
expect(jwt.payload, payload);
17+
});
18+
});
19+
20+
group('Map<String, dynamic>', () {
21+
test('Works as a payload', () {
22+
final payload = <String, dynamic>{
23+
'foo': 'bar',
24+
'iat': 1234,
25+
};
26+
27+
final jwt = JWT(payload);
28+
29+
expect(jwt.payload, isA<Map<String, dynamic>>());
30+
expect(jwt.payload, payload);
31+
});
32+
33+
test('Gets copied internally', () {
34+
final payload = <String, dynamic>{
35+
'foo': 'bar',
36+
'iat': 1234,
37+
};
38+
39+
final jwt = JWT(payload);
40+
41+
expect(jwt.payload, isA<Map<String, dynamic>>());
42+
expect(jwt.payload, payload);
43+
expect(identical(jwt.payload, payload), isFalse);
44+
45+
payload['new_key'] = true;
46+
47+
expect(jwt.payload, hasLength(2));
48+
});
49+
});
50+
51+
group('Map<int, dynamic>', () {
52+
test('Does not work as a payload', () {
53+
final payload = <int, dynamic>{
54+
123: 'bar',
55+
};
56+
57+
expect(
58+
() => JWT(payload),
59+
throwsA(isA<TypeError>()),
60+
);
61+
});
62+
});
63+
64+
group('String', () {
65+
test('Works as a payload', () {
66+
final payload = 'asdf123';
67+
68+
final jwt = JWT(payload);
69+
70+
expect(jwt.payload, payload);
71+
});
72+
});
73+
74+
group('int', () {
75+
test('Does not work as a payload', () {
76+
final payload = 1234;
77+
78+
expect(
79+
() => JWT(payload),
80+
throwsA(isA<Exception>()),
81+
);
82+
});
83+
});
84+
});
85+
});
86+
}

0 commit comments

Comments
 (0)