Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 44 additions & 15 deletions lib/src/jwt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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<String, dynamic>) {
Expand All @@ -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<String, dynamic>;
} catch (ex) {
payload = utf8.decode(base64Url.decode(base64Padded(parts[1])));
}
Expand Down Expand Up @@ -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<String, dynamic>);

final audiance = _parseAud(payload['aud']);
final issuer = payload['iss']?.toString();
Copy link
Contributor Author

@tp tp Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, any non-Map payload would've failed anyway.

Expand Down Expand Up @@ -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<String, dynamic>` 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<String, dynamic>.from(value);
} else {
throw Exception(
'Unexpected `payload` type `${value.runtimeType}`, must be either `String` or `Map<String, *>`',
);
}
}

/// Audience claim
Audience? audience;
Expand Down Expand Up @@ -281,7 +309,8 @@ class JWT {
bool noIssueAt = false,
}) {
try {
if (payload is Map<String, dynamic> || payload is Map<dynamic, dynamic>) {
var payload = this.payload;
if (payload is Map<String, dynamic>) {
try {
payload = Map<String, dynamic>.from(payload);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it made sense that this was taking a copy of the map (as it might've been immutable or used again in the previous variant which was not holding it's own copy), but I wonder if the "writing back" to payload was intentional?

Now we don't write back to the JWT object (this) (and the local reference is also needed to get the type checking work properly, as it's now based on Object instead of the "always working" dynamic).


Expand Down
86 changes: 86 additions & 0 deletions test/create_test.dart
Original file line number Diff line number Diff line change
@@ -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<String, String>', () {
test('Works as a payload, but gets converted', () {
final payload = <String, String>{
'foo': 'bar',
};

final jwt = JWT(payload);

expect(jwt.payload, isA<Map<String, dynamic>>());
expect(jwt.payload, payload);
});
});

group('Map<String, dynamic>', () {
test('Works as a payload', () {
final payload = <String, dynamic>{
'foo': 'bar',
'iat': 1234,
};

final jwt = JWT(payload);

expect(jwt.payload, isA<Map<String, dynamic>>());
expect(jwt.payload, payload);
});

test('Gets copied internally', () {
final payload = <String, dynamic>{
'foo': 'bar',
'iat': 1234,
};

final jwt = JWT(payload);

expect(jwt.payload, isA<Map<String, dynamic>>());
expect(jwt.payload, payload);
expect(identical(jwt.payload, payload), isFalse);

payload['new_key'] = true;

expect(jwt.payload, hasLength(2));
});
});

group('Map<int, dynamic>', () {
test('Does not work as a payload', () {
final payload = <int, dynamic>{
123: 'bar',
};

expect(
() => JWT(payload),
throwsA(isA<TypeError>()),
);
});
});

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<Exception>()),
);
});
});
});
});
}