Skip to content

Commit 595289c

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

File tree

1 file changed

+52
-39
lines changed

1 file changed

+52
-39
lines changed

lib/src/jwt.dart

Lines changed: 52 additions & 39 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
}
@@ -186,6 +197,7 @@ class JWT {
186197
jwtId: jwtId,
187198
);
188199
} catch (ex) {
200+
print(ex);
189201
return null;
190202
}
191203
}
@@ -194,18 +206,15 @@ class JWT {
194206
///
195207
/// This also sets [JWT.audience], [JWT.subject], [JWT.issuer], and
196208
/// [JWT.jwtId] even though they are not verified. Use with caution.
209+
///
210+
/// This methods only supports map payloads. For `String` payloads use `verify`.
197211
static JWT decode(String token) {
198212
try {
199213
final parts = token.split('.');
200-
var header = jsonBase64.decode(base64Padded(parts[0]));
214+
final header = jsonBase64.decode(base64Padded(parts[0]));
201215

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

210219
final audiance = _parseAud(payload['aud']);
211220
final issuer = payload['iss']?.toString();
@@ -246,10 +255,20 @@ class JWT {
246255
this.issuer,
247256
this.jwtId,
248257
this.header,
249-
});
258+
}) {
259+
if (payload is! String &&
260+
payload is! Map<dynamic, dynamic> &&
261+
payload is! Map<String, dynamic>) {
262+
throw Exception('Unexpected payload type `${payload.runtimeType}`');
263+
}
264+
}
250265

251-
/// Custom claims
252-
dynamic payload;
266+
/// The token's payload, either as a `Map<String, dynamic>`, `Map<dynamic, dynamic>`,
267+
/// or plain `String` (in case it was not a JSON-encoded map).
268+
///
269+
/// If it's a map, it has all claims, containing the utilized registered claims
270+
/// as well custom ones added.
271+
Object payload;
253272

254273
/// Audience claim
255274
Audience? audience;
@@ -281,9 +300,17 @@ class JWT {
281300
bool noIssueAt = false,
282301
}) {
283302
try {
284-
if (payload is Map<String, dynamic> || payload is Map<dynamic, dynamic>) {
285-
try {
286-
payload = Map<String, dynamic>.from(payload);
303+
final tokenHeader = Map.from(header ?? {});
304+
tokenHeader.putIfAbsent('alg', () => algorithm.name);
305+
tokenHeader.putIfAbsent('typ', () => 'JWT');
306+
307+
final b64Header = base64Unpadded(jsonBase64.encode(tokenHeader));
308+
309+
final String b64Payload;
310+
311+
try {
312+
if (payload is Map) {
313+
final payload = <String, dynamic>{...(this.payload as Map)};
287314

288315
if (!noIssueAt) {
289316
payload['iat'] = secondsSinceEpoch(timeNowUTC());
@@ -298,34 +325,20 @@ class JWT {
298325
if (subject != null) payload['sub'] = subject;
299326
if (issuer != null) payload['iss'] = issuer;
300327
if (jwtId != null) payload['jti'] = jwtId;
301-
} catch (ex) {
302-
assert(
303-
payload is Map<String, dynamic>,
304-
'If payload is a Map its must be a Map<String, dynamic>',
305-
);
306-
}
307-
}
308-
309-
final tokenHeader = Map.from(header ?? {});
310-
tokenHeader.putIfAbsent('alg', () => algorithm.name);
311-
tokenHeader.putIfAbsent('typ', () => 'JWT');
312-
313-
final b64Header = base64Unpadded(jsonBase64.encode(tokenHeader));
314328

315-
String b64Payload;
316-
try {
317-
b64Payload = base64Unpadded(
318-
payload is String
319-
? base64Url.encode(utf8.encode(payload))
320-
: jsonBase64.encode(payload),
321-
);
322-
} catch (ex) {
329+
b64Payload = jsonBase64.encode(payload);
330+
} else {
331+
b64Payload = base64Url.encode(utf8.encode(payload as String));
332+
}
333+
} catch (e, s) {
334+
print(e);
335+
print(s);
323336
throw JWTException(
324-
'invalid payload json format (Map keys must be String type)',
337+
'invalid payload, must be String or Map<String, dynamic>, but got ${payload.runtimeType}',
325338
);
326339
}
327340

328-
final body = '$b64Header.$b64Payload';
341+
final body = '$b64Header.${base64Unpadded(b64Payload)}';
329342
final signature = base64Unpadded(
330343
base64Url.encode(
331344
algorithm.sign(

0 commit comments

Comments
 (0)