Skip to content

Commit

Permalink
Fix JWT and add test
Browse files Browse the repository at this point in the history
  • Loading branch information
PlugFox committed Jul 17, 2024
1 parent 5ade0c2 commit 3c68339
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 11 deletions.
55 changes: 46 additions & 9 deletions lib/src/model/jwt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ sealed class SpinifyJWT {
/// Creates JWT from [secret] (with HMAC-SHA256 algorithm)
/// and current payload.
String encode(String secret);

/// Creates a JSON representation of payload.
Map<String, Object?> toJson();
}

final class _SpinifyJWTImpl extends SpinifyJWT {
Expand All @@ -256,28 +259,30 @@ final class _SpinifyJWTImpl extends SpinifyJWT {
}) : super._();

factory _SpinifyJWTImpl.decode(String jwt, [String? secret]) {
// Разделение токена на составляющие части
// Split token into parts
var parts = jwt.split('.');
if (parts.length != 3) {
// coverage:ignore-line
throw const FormatException(
'Invalid token format, expected 3 parts separated by "."');
}
final <String>[encodedHeader, encodedPayload, encodedSignature] = parts;

if (secret != null) {
// Вычисление подписи
// Compute signature
final key = utf8.encode(secret); // Your 256 bit secret key
final bytes = utf8.encode('$encodedHeader.$encodedPayload');
var hmacSha256 = Hmac(sha256, key); // HMAC-SHA256
var digest = hmacSha256.convert(bytes);

// Кодирование подписи
var computedSignature = base64Url.encode(digest.bytes);
final hmacSha256 = Hmac(sha256, key); // HMAC-SHA256
final digest = hmacSha256.convert(bytes);
final computedSignature = const _UnpaddedBase64Converter()
.convert(base64Url.encode(digest.bytes));

// Сравнение подписи в токене с вычисленной подписью
// Check signature equality
// coverage:ignore-start
if (computedSignature != encodedSignature) {
throw const FormatException('Invalid token signature');
}
// coverage:ignore-end
}

Map<String, Object?> payload;
Expand All @@ -286,10 +291,12 @@ final class _SpinifyJWTImpl extends SpinifyJWT {
.fuse<String>(const Utf8Decoder())
.fuse<Map<String, Object?>>(
const JsonDecoder().cast<String, Map<String, Object?>>())
.convert(encodedPayload);
.convert(const _NormilizeBase64Converter().convert(encodedPayload));
} on Object catch (_, stackTrace) {
// coverage:ignore-start
Error.throwWithStackTrace(
const FormatException('Can\'t decode token payload'), stackTrace);
// coverage:ignore-end
}
try {
return _SpinifyJWTImpl(
Expand All @@ -310,8 +317,10 @@ final class _SpinifyJWTImpl extends SpinifyJWT {
expireAt: payload['expire_at'] as int?,
);
} on Object catch (_, stackTrace) {
// coverage:ignore-start
Error.throwWithStackTrace(
const FormatException('Invalid token payload data'), stackTrace);
// coverage:ignore-end
}
}

Expand Down Expand Up @@ -402,6 +411,23 @@ final class _SpinifyJWTImpl extends SpinifyJWT {
return '$encodedHeader.$encodedPayload.$encodedSignature';
}

@override
Map<String, Object?> toJson() => <String, Object?>{
'sub': sub,
if (channel != null) 'channel': channel,
if (exp != null) 'exp': exp,
if (iat != null) 'iat': iat,
if (jti != null) 'jti': jti,
if (aud != null) 'aud': aud,
if (iss != null) 'iss': iss,
if (info != null) 'info': info,
if (b64info != null) 'b64info': b64info,
if (channels != null) 'channels': channels,
if (subs != null) 'subs': subs,
if (meta != null) 'meta': meta,
if (expireAt != null) 'expireAt': expireAt,
};

@override
String toString() => 'SpinifyJWT{sub: $sub}';
}
Expand All @@ -418,3 +444,14 @@ class _UnpaddedBase64Converter extends Converter<String, String> {
return input;
}
}

/// A converter thats normalizes Base64-encoded strings
class _NormilizeBase64Converter extends Converter<String, String> {
const _NormilizeBase64Converter();

@override
String convert(String input) {
final padding = (4 - input.length % 4) % 4;
return input + '=' * padding;
}
}
1 change: 0 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ dependencies:
fixnum: ^1.1.0
stack_trace: ^1.11.0


dev_dependencies:
build_runner: ^2.4.6
pubspec_generator: ^4.0.0
Expand Down
55 changes: 55 additions & 0 deletions test/unit/jwt_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// ignore_for_file: prefer_const_constructors

import 'package:spinify/spinify.dart';
import 'package:test/test.dart';

void main() {
group('JWT', () {
test('Create', () {
expect(() => SpinifyJWT(sub: 'sub'), returnsNormally);
});

test('Encode_and_decode', () {
final jwt = SpinifyJWT(
sub: 'sub',
channel: 'channel',
iat: 1234567890,
exp: 1234567890,
iss: 'iss',
aud: 'aud',
jti: 'jti',
expireAt: 1234567890,
b64info: 'b64info',
channels: const <String>[
'channel1',
'channel2',
],
subs: const <String, Object?>{},
info: const <String, Object?>{},
meta: const <String, Object?>{},
);
expect(jwt, isA<SpinifyJWT>());
expect(jwt.toString(), equals('SpinifyJWT{sub: ${jwt.sub}}'));
final encoded = jwt.encode('secret');
expect(encoded, isA<String>());
final decoded = SpinifyJWT.decode(encoded, 'secret');
expect(
decoded,
isA<SpinifyJWT>()
.having((e) => e.sub, 'sub', jwt.sub)
.having((e) => e.channel, 'channel', jwt.channel)
.having((e) => e.iat, 'iat', jwt.iat)
.having((e) => e.exp, 'exp', jwt.exp)
.having((e) => e.iss, 'iss', jwt.iss)
.having((e) => e.aud, 'aud', jwt.aud)
.having((e) => e.jti, 'jti', jwt.jti)
.having((e) => e.expireAt, 'expireAt', jwt.expireAt)
.having((e) => e.b64info, 'b64info', jwt.b64info)
.having((e) => e.channels, 'channels', jwt.channels)
.having((e) => e.subs, 'subs', jwt.subs),
);
expect(decoded.toJson(), equals(jwt.toJson()));
expect(decoded.toString(), equals(jwt.toString()));
});
});
}
10 changes: 9 additions & 1 deletion test/unit/spinify_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ void main() {
greaterThan(Int64.ZERO),
),
]));
client.close();
client
..newSubscription('channel')
..close();
async.elapse(client.config.timeout);
expect(
client.metrics,
Expand All @@ -261,8 +263,14 @@ void main() {
),
]));
expect(() => client.metrics.toString(), returnsNormally);
expect(client.metrics.toString(), equals('SpinifyMetrics{}'));
expect(() => client.metrics.toJson(), returnsNormally);
expect(client.metrics.toJson(), isA<Map<String, Object?>>());
expect(client.metrics.channels, hasLength(2));
expect(
client.metrics.channels['channel'],
isA<SpinifyMetrics$Channel>().having((c) => c.toString(),
'subscriptions', equals(r'SpinifyMetrics$Channel{}')));
}));
});
}
2 changes: 2 additions & 0 deletions test/unit_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:test/test.dart';

import 'unit/config_test.dart' as config_test;
import 'unit/jwt_test.dart' as jwt_test;
import 'unit/logs_test.dart' as logs_test;
import 'unit/server_subscription_test.dart' as server_subscription_test;
import 'unit/spinify_test.dart' as spinify_test;
Expand All @@ -11,5 +12,6 @@ void main() {
config_test.main();
spinify_test.main();
server_subscription_test.main();
jwt_test.main();
});
}

0 comments on commit 3c68339

Please sign in to comment.