Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active November 21, 2023 14:24
Show Gist options
  • Save PlugFox/4110f47cab8c2029b205ffe2863d6e3f to your computer and use it in GitHub Desktop.
Save PlugFox/4110f47cab8c2029b205ffe2863d6e3f to your computer and use it in GitHub Desktop.

Revisions

  1. PlugFox revised this gist Jul 16, 2023. 1 changed file with 10 additions and 9 deletions.
    19 changes: 10 additions & 9 deletions jwt.dart
    Original file line number Diff line number Diff line change
    @@ -19,11 +19,11 @@ import 'package:meta/meta.dart';
    /// https://centrifugal.dev/docs/server/authentication#connection-jwt-claims
    /// {@endtemplate}
    @immutable
    sealed class JWT {
    sealed class CentrifugoJWT {
    /// {@macro jwt}
    ///
    /// Creates JWT from [secret] (with HMAC-SHA256 algorithm)
    const factory JWT({
    const factory CentrifugoJWT({
    required String sub,
    int? exp,
    int? iat,
    @@ -36,16 +36,17 @@ sealed class JWT {
    Map<String, Object?>? subs,
    Map<String, Object?>? meta,
    int? expireAt,
    }) = _JWTImpl;
    }) = _CentrifugoJWTImpl;

    /// {@macro jwt}
    ///
    /// Parses JWT, if [secret] is provided
    /// then checks signature by HMAC-SHA256 algorithm.
    factory JWT.decode(String jwt, [String? secret]) = _JWTImpl.decode;
    factory CentrifugoJWT.decode(String jwt, [String? secret]) =
    _CentrifugoJWTImpl.decode;

    /// {@nodoc}
    const JWT._();
    const CentrifugoJWT._();

    /// This is a standard JWT claim which must contain
    /// an ID of the current application user (as string).
    @@ -231,8 +232,8 @@ sealed class JWT {
    String encode(String secret);
    }

    final class _JWTImpl extends JWT {
    const _JWTImpl({
    final class _CentrifugoJWTImpl extends CentrifugoJWT {
    const _CentrifugoJWTImpl({
    required this.sub,
    this.exp,
    this.iat,
    @@ -247,7 +248,7 @@ final class _JWTImpl extends JWT {
    this.expireAt,
    }) : super._();

    factory _JWTImpl.decode(String jwt, [String? secret]) {
    factory _CentrifugoJWTImpl.decode(String jwt, [String? secret]) {
    // Разделение токена на составляющие части
    var parts = jwt.split('.');
    if (parts.length != 3) {
    @@ -284,7 +285,7 @@ final class _JWTImpl extends JWT {
    const FormatException('Can\'t decode token payload'), stackTrace);
    }
    try {
    return _JWTImpl(
    return _CentrifugoJWTImpl(
    sub: payload['sub'] as String,
    exp: payload['exp'] as int?,
    iat: payload['iat'] as int?,
  2. PlugFox revised this gist Jul 16, 2023. 1 changed file with 123 additions and 69 deletions.
    192 changes: 123 additions & 69 deletions jwt.dart
    Original file line number Diff line number Diff line change
    @@ -3,20 +3,26 @@ import 'dart:convert';
    import 'package:crypto/crypto.dart';
    import 'package:meta/meta.dart';

    /// A JWT token consists of three parts: the header, the payload, and the signature or encryption data.
    /// {@template jwt}
    /// A JWT token consists of three parts: the header,
    /// the payload, and the signature or encryption data.
    /// The first two elements are JSON objects of a specific structure.
    /// The third element is calculated based on the first two and depends on the chosen algorithm
    /// The third element is calculated based on the first two
    /// and depends on the chosen algorithm
    /// (in the case of using an unsigned JWT, it can be omitted).
    /// Tokens can be re-encoded into a compact representation (JWS/JWE Compact Serialization):
    /// Tokens can be re-encoded into a compact representation
    /// (JWS/JWE Compact Serialization):
    /// the header and payload are subjected to Base64-URL encoding,
    /// after which the signature is added, and all three elements are separated by periods (".").
    /// after which the signature is added,
    /// and all three elements are separated by periods (".").
    ///
    /// https://centrifugal.dev/docs/server/authentication#connection-jwt-claims
    /// {@endtemplate}
    @immutable
    sealed class JWT {
    const JWT._();

    /// Создает JWT.
    /// {@macro jwt}
    ///
    /// Creates JWT from [secret] (with HMAC-SHA256 algorithm)
    const factory JWT({
    required String sub,
    int? exp,
    @@ -32,33 +38,48 @@ sealed class JWT {
    int? expireAt,
    }) = _JWTImpl;

    /// Parses JWT, if [secret] is provided then checks signature by HMAC-SHA256 algorithm.
    /// {@macro jwt}
    ///
    /// Parses JWT, if [secret] is provided
    /// then checks signature by HMAC-SHA256 algorithm.
    factory JWT.decode(String jwt, [String? secret]) = _JWTImpl.decode;

    /// This is a standard JWT claim which must contain an ID of the current application user (as string).
    /// {@nodoc}
    const JWT._();

    /// This is a standard JWT claim which must contain
    /// an ID of the current application user (as string).
    ///
    /// If a user is not currently authenticated in an application,
    /// but you want to let him connect anyway – you can use an empty string as a user ID in sub claim.
    /// but you want to let him connect anyway – you can use
    /// an empty string as a user ID in sub claim.
    /// This is called anonymous access.
    abstract final String sub;

    /// This is a UNIX timestamp seconds when the token will expire.
    /// This is a standard JWT claim - all JWT libraries for different languages provide an API to set it.
    /// This is a standard JWT claim - all JWT libraries
    /// for different languages provide an API to set it.
    ///
    /// If exp claim is not provided then Centrifugo won't expire connection.
    /// When provided special algorithm will find connections with exp in the past
    /// and activate the connection refresh mechanism.
    /// Refresh mechanism allows connection to survive and be prolonged.
    /// In case of refresh failure, the client connection will be eventually closed by Centrifugo
    /// and won't be accepted until new valid and actual credentials are provided in the connection token.
    /// In case of refresh failure, the client connection
    /// will be eventually closed by Centrifugo
    /// and won't be accepted until new valid and actual
    /// credentials are provided in the connection token.
    ///
    /// You can use the connection expiration mechanism in cases when you don't want users of your app
    /// You can use the connection expiration mechanism in
    /// cases when you don't want users of your app
    /// to be subscribed on channels after being banned/deactivated in the application.
    /// Or to protect your users from token leakage (providing a reasonably short time of expiration).
    /// Or to protect your users from token leakage
    /// (providing a reasonably short time of expiration).
    ///
    /// Choose exp value wisely, you don't need small values because the refresh mechanism
    /// Choose exp value wisely, you don't need small
    /// values because the refresh mechanism
    /// will hit your application often with refresh requests.
    /// But setting this value too large can lead to slow user connection deactivation. This is a trade-off.
    /// But setting this value too large can lead
    /// to slow user connection deactivation. This is a trade-off.
    ///
    /// Read more about connection expiration below.
    abstract final int? exp;
    @@ -83,8 +104,10 @@ sealed class JWT {
    /// }
    /// ```
    ///
    /// Setting token_audience will also affect subscription tokens (used for channel token authorization).
    /// If you need to separate connection token configuration and subscription token configuration
    /// Setting token_audience will also affect subscription tokens
    /// (used for channel token authorization).
    /// If you need to separate connection token configuration
    /// and subscription token configuration
    /// check out separate subscription token config feature.
    ///
    /// This claim is optional.
    @@ -101,27 +124,32 @@ sealed class JWT {
    /// }
    /// ```
    ///
    /// Setting token_issuer will also affect subscription tokens (used for channel token authorization).
    /// If you need to separate connection token configuration and subscription token configuration
    /// Setting token_issuer will also affect subscription tokens
    /// (used for channel token authorization).
    /// If you need to separate connection token configuration
    /// and subscription token configuration
    /// check out separate subscription token config feature.
    ///
    /// This claim is optional.
    abstract final String? iss;

    /// This claim is optional - this is additional information about client connection
    /// This claim is optional - this is additional information
    /// about client connection
    /// that can be provided for Centrifugo.
    /// This information will be included in presence information,
    /// join/leave events, and channel publication if it was published from a client-side.
    abstract final Map<String, Object?>? info;

    /// If you are using binary Protobuf protocol you may want info to be custom bytes. Use this field in this case.
    /// If you are using binary Protobuf protocol you may want info
    /// to be custom bytes. Use this field in this case.
    ///
    /// This field contains a `base64` representation of your bytes.
    /// After receiving Centrifugo will decode base64 back to bytes
    /// and will embed the result into various places described above.
    abstract final String? b64info;

    /// An optional array of strings with server-side channels to subscribe a client to.
    /// An optional array of strings with server-side channels
    /// to subscribe a client to.
    /// See more details about [server-side subscriptions](https://centrifugal.dev/docs/server/server_subs).
    abstract final List<String>? channels;

    @@ -131,7 +159,8 @@ sealed class JWT {
    /// can be annotated with info, data, and so on using options.
    /// The claim sub described above is a standart JWT claim to provide a user ID
    /// (it's a shortcut from subject).
    /// While claims have similar names they have different purpose in a connection JWT.
    /// While claims have similar names they have
    /// different purpose in a connection JWT.
    ///
    /// Example:
    /// ```json
    @@ -150,10 +179,10 @@ sealed class JWT {
    ///
    /// Subscribe options:
    /// - (optional) info - JSON object - Custom channel info
    /// - (optional) b64info - string - Custom channel info in Base64 - to pass binary channel info
    /// - (optional) data - JSON object - Custom JSON data to return in subscription context inside Connect reply
    /// - (optional) b64data - string - Same as `data` but in Base64 to send binary data
    /// - (optional) override - Override object - Allows dynamically override some channel options.
    /// - (optional) b64info - string - Custom channel info in Base64
    /// - (optional) data - JSON object - Custom JSON data
    /// - (optional) b64data - string - Same as `data` but in Base64
    /// - (optional) override - Override object - Override some channel options.
    ///
    /// Override object:
    /// - (optional) presence - BoolValue - Override presence
    @@ -169,27 +198,36 @@ sealed class JWT {
    /// ```
    abstract final Map<String, Object?>? subs;

    /// Meta is an additional JSON object (ex. `{"key": "value"}`) that will be attached to a connection.
    /// Unlike `info` it's never exposed to clients inside presence and join/leave payloads
    /// and only accessible on a backend side. It may be included in proxy calls from Centrifugo
    /// Meta is an additional JSON object (ex. `{"key": "value"}`)
    /// that will be attached to a connection.
    /// Unlike `info` it's never exposed to clients inside presence
    /// and join/leave payloads
    /// and only accessible on a backend side. It may be included
    /// in proxy calls from Centrifugo
    /// to the application backend (see `proxy_include_connection_meta` option).
    /// Also, there is a `connections` API method in Centrifugo PRO that returns
    /// this data in the connection description object.
    abstract final Map<String, Object?>? meta;

    /// By default, Centrifugo looks on `exp` claim to configure connection expiration.
    /// In most cases this is fine, but there could be situations where you wish to decouple token expiration
    /// check with connection expiration time. As soon as the `expire_at` claim is provided (set)
    /// By default, Centrifugo looks on `exp` claim
    /// to configure connection expiration.
    /// In most cases this is fine, but there could be situations
    /// where you wish to decouple token expiration
    /// check with connection expiration time.
    /// As soon as the `expire_at` claim is provided (set)
    /// in JWT Centrifugo relies on it for setting
    /// connection expiration time (JWT expiration still checked over `exp` though).
    /// connection expiration time
    /// (JWT expiration still checked over `exp` though).
    ///
    /// `expire_at` is a UNIX timestamp seconds when the connection should expire.
    ///
    /// Set it to the future time for expiring connection at some point
    /// Set it to 0 to disable connection expiration (but still check token exp claim).
    /// Set it to 0 to disable connection expiration
    /// (but still check token exp claim).
    abstract final int? expireAt;

    /// Creates JWT from [secret] (with HMAC-SHA256 algorithm) and current payload.
    /// Creates JWT from [secret] (with HMAC-SHA256 algorithm)
    /// and current payload.
    String encode(String secret);
    }

    @@ -212,7 +250,10 @@ final class _JWTImpl extends JWT {
    factory _JWTImpl.decode(String jwt, [String? secret]) {
    // Разделение токена на составляющие части
    var parts = jwt.split('.');
    if (parts.length != 3) throw FormatException('Invalid token format, expected 3 parts separated by "."');
    if (parts.length != 3) {
    throw const FormatException(
    'Invalid token format, expected 3 parts separated by "."');
    }
    final <String>[encodedHeader, encodedPayload, encodedSignature] = parts;

    if (secret != null) {
    @@ -226,17 +267,21 @@ final class _JWTImpl extends JWT {
    var computedSignature = base64Url.encode(digest.bytes);

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

    Map<String, Object?> payload;
    try {
    payload = const Base64Decoder()
    .fuse<String>(const Utf8Decoder())
    .fuse<Map<String, Object?>>(const JsonDecoder().cast<String, Map<String, Object?>>())
    .fuse<Map<String, Object?>>(
    const JsonDecoder().cast<String, Map<String, Object?>>())
    .convert(encodedPayload);
    } on Object catch (_, stackTrace) {
    Error.throwWithStackTrace(FormatException('Can\'t decode token payload'), stackTrace);
    Error.throwWithStackTrace(
    const FormatException('Can\'t decode token payload'), stackTrace);
    }
    try {
    return _JWTImpl(
    @@ -248,25 +293,29 @@ final class _JWTImpl extends JWT {
    iss: payload['iss'] as String?,
    info: payload['info'] as Map<String, Object?>?,
    b64info: payload['b64info'] as String?,
    channels: (payload['channels'] as Iterable<Object?>?)?.whereType<String>().toList(),
    channels: (payload['channels'] as Iterable<Object?>?)
    ?.whereType<String>()
    .toList(),
    subs: payload['subs'] as Map<String, Object?>?,
    meta: payload['meta'] as Map<String, Object?>?,
    expireAt: payload['expire_at'] as int?,
    );
    } on Object catch (_, stackTrace) {
    Error.throwWithStackTrace(FormatException('Invalid token payload data'), stackTrace);
    Error.throwWithStackTrace(
    const FormatException('Invalid token payload data'), stackTrace);
    }
    }

    static final Converter<Map<String, Object?>, String> _$encoder = const JsonEncoder()
    .cast<Map<String, Object?>, String>()
    .fuse<List<int>>(const Utf8Encoder())
    .fuse<String>(const Base64Encoder.urlSafe())
    .fuse<String>(const _UnpaddedBase64Converter());
    static final Converter<Map<String, Object?>, String> _$encoder =
    const JsonEncoder()
    .cast<Map<String, Object?>, String>()
    .fuse<List<int>>(const Utf8Encoder())
    .fuse<String>(const Base64Encoder.urlSafe())
    .fuse<String>(const _UnpaddedBase64Converter());

    static final String _$headerHmacSha256 = _$encoder.convert(<String, Object?>{
    "alg": "HS256",
    "typ": "JWT",
    'alg': 'HS256',
    'typ': 'JWT',
    });

    @override
    @@ -307,40 +356,45 @@ final class _JWTImpl extends JWT {

    @override
    String encode(String secret) {
    // Кодирование заголовка и полезной нагрузки
    // Encode header and payload
    final encodedHeader = _$headerHmacSha256;
    final encodedPayload = _$encoder.convert(<String, Object?>{
    'sub': this.sub,
    if (exp != null) 'exp': this.exp,
    if (iat != null) 'iat': this.iat,
    if (jti != null) 'jti': this.jti,
    if (aud != null) 'aud': this.aud,
    if (iss != null) 'iss': this.iss,
    if (info != null) 'info': this.info,
    if (b64info != null) 'b64info': this.b64info,
    if (channels != null) 'channels': this.channels,
    if (subs != null) 'subs': this.subs,
    if (meta != null) 'meta': this.meta,
    if (expireAt != null) 'expire_at': this.expireAt,
    'sub': sub,
    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) 'expire_at': expireAt,
    });

    // Создание подписи
    // Payload signature
    final key = utf8.encode(secret); // Your 256 bit secret key
    final bytes = utf8.encode('$encodedHeader.$encodedPayload');

    final hmacSha256 = Hmac(sha256, key); // HMAC-SHA256
    final digest = hmacSha256.convert(bytes);

    // Кодирование подписи
    final encodedSignature =
    const Base64Encoder.urlSafe().fuse<String>(const _UnpaddedBase64Converter()).convert(digest.bytes);
    // Encode signature
    final encodedSignature = const Base64Encoder.urlSafe()
    .fuse<String>(const _UnpaddedBase64Converter())
    .convert(digest.bytes);

    // Создание JWT
    // Return JWT
    return '$encodedHeader.$encodedPayload.$encodedSignature';
    }
    }

    /// A converter that converts Base64-encoded strings
    /// to unpadded Base64-encoded strings.
    /// {@nodoc}
    class _UnpaddedBase64Converter extends Converter<String, String> {
    /// {@nodoc}
    const _UnpaddedBase64Converter();

    @override
  3. PlugFox revised this gist Jul 16, 2023. 1 changed file with 14 additions and 15 deletions.
    29 changes: 14 additions & 15 deletions jwt.dart
    Original file line number Diff line number Diff line change
    @@ -3,13 +3,13 @@ import 'dart:convert';
    import 'package:crypto/crypto.dart';
    import 'package:meta/meta.dart';

    /// Токен JWT состоит из трех частей: заголовка (header), полезной нагрузки (payload) и подписи или данных шифрования.
    /// Первые два элемента — это JSON объекты определенной структуры.
    /// Третий элемент вычисляется на основании первых и зависит от выбранного алгоритма
    /// (в случае использования неподписанного JWT может быть опущен).
    /// Токены могут быть перекодированы в компактное представление (JWS/JWE Compact Serialization):
    /// к заголовку и полезной нагрузке применяется алгоритм кодирования Base64-URL,
    /// после чего добавляется подпись и все три элемента разделяются точками («.»).
    /// A JWT token consists of three parts: the header, the payload, and the signature or encryption data.
    /// The first two elements are JSON objects of a specific structure.
    /// The third element is calculated based on the first two and depends on the chosen algorithm
    /// (in the case of using an unsigned JWT, it can be omitted).
    /// Tokens can be re-encoded into a compact representation (JWS/JWE Compact Serialization):
    /// the header and payload are subjected to Base64-URL encoding,
    /// after which the signature is added, and all three elements are separated by periods (".").
    ///
    /// https://centrifugal.dev/docs/server/authentication#connection-jwt-claims
    @immutable
    @@ -32,8 +32,7 @@ sealed class JWT {
    int? expireAt,
    }) = _JWTImpl;

    /// Разбирает JWT.
    /// Если [secret] указан, то проверяет подпись с использованием алгоритма HMAC-SHA256.
    /// Parses JWT, if [secret] is provided then checks signature by HMAC-SHA256 algorithm.
    factory JWT.decode(String jwt, [String? secret]) = _JWTImpl.decode;

    /// This is a standard JWT claim which must contain an ID of the current application user (as string).
    @@ -190,7 +189,7 @@ sealed class JWT {
    /// Set it to 0 to disable connection expiration (but still check token exp claim).
    abstract final int? expireAt;

    /// Генерирует JWT с HMAC-SHA256 подписью.
    /// Creates JWT from [secret] (with HMAC-SHA256 algorithm) and current payload.
    String encode(String secret);
    }

    @@ -326,14 +325,14 @@ final class _JWTImpl extends JWT {
    });

    // Создание подписи
    var key = utf8.encode(secret); // Your 256 bit secret key
    var bytes = utf8.encode('$encodedHeader.$encodedPayload');
    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);
    final hmacSha256 = Hmac(sha256, key); // HMAC-SHA256
    final digest = hmacSha256.convert(bytes);

    // Кодирование подписи
    var encodedSignature =
    final encodedSignature =
    const Base64Encoder.urlSafe().fuse<String>(const _UnpaddedBase64Converter()).convert(digest.bytes);

    // Создание JWT
  4. PlugFox created this gist Jul 16, 2023.
    353 changes: 353 additions & 0 deletions jwt.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,353 @@
    import 'dart:convert';

    import 'package:crypto/crypto.dart';
    import 'package:meta/meta.dart';

    /// Токен JWT состоит из трех частей: заголовка (header), полезной нагрузки (payload) и подписи или данных шифрования.
    /// Первые два элемента — это JSON объекты определенной структуры.
    /// Третий элемент вычисляется на основании первых и зависит от выбранного алгоритма
    /// (в случае использования неподписанного JWT может быть опущен).
    /// Токены могут быть перекодированы в компактное представление (JWS/JWE Compact Serialization):
    /// к заголовку и полезной нагрузке применяется алгоритм кодирования Base64-URL,
    /// после чего добавляется подпись и все три элемента разделяются точками («.»).
    ///
    /// https://centrifugal.dev/docs/server/authentication#connection-jwt-claims
    @immutable
    sealed class JWT {
    const JWT._();

    /// Создает JWT.
    const factory JWT({
    required String sub,
    int? exp,
    int? iat,
    String? jti,
    String? aud,
    String? iss,
    Map<String, Object?>? info,
    String? b64info,
    List<String>? channels,
    Map<String, Object?>? subs,
    Map<String, Object?>? meta,
    int? expireAt,
    }) = _JWTImpl;

    /// Разбирает JWT.
    /// Если [secret] указан, то проверяет подпись с использованием алгоритма HMAC-SHA256.
    factory JWT.decode(String jwt, [String? secret]) = _JWTImpl.decode;

    /// This is a standard JWT claim which must contain an ID of the current application user (as string).
    ///
    /// If a user is not currently authenticated in an application,
    /// but you want to let him connect anyway – you can use an empty string as a user ID in sub claim.
    /// This is called anonymous access.
    abstract final String sub;

    /// This is a UNIX timestamp seconds when the token will expire.
    /// This is a standard JWT claim - all JWT libraries for different languages provide an API to set it.
    ///
    /// If exp claim is not provided then Centrifugo won't expire connection.
    /// When provided special algorithm will find connections with exp in the past
    /// and activate the connection refresh mechanism.
    /// Refresh mechanism allows connection to survive and be prolonged.
    /// In case of refresh failure, the client connection will be eventually closed by Centrifugo
    /// and won't be accepted until new valid and actual credentials are provided in the connection token.
    ///
    /// You can use the connection expiration mechanism in cases when you don't want users of your app
    /// to be subscribed on channels after being banned/deactivated in the application.
    /// Or to protect your users from token leakage (providing a reasonably short time of expiration).
    ///
    /// Choose exp value wisely, you don't need small values because the refresh mechanism
    /// will hit your application often with refresh requests.
    /// But setting this value too large can lead to slow user connection deactivation. This is a trade-off.
    ///
    /// Read more about connection expiration below.
    abstract final int? exp;

    /// This is a UNIX time when token was issued (seconds).
    /// See [definition in RFC](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6).
    /// This claim is optional
    abstract final int? iat;

    /// This is a token unique ID. See [definition in RFC](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7).
    /// This claim is optional.
    abstract final String? jti;

    /// Audience.
    /// [rfc7519 aud claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3)
    /// By default, Centrifugo does not check JWT audience.
    ///
    /// But you can force this check by setting token_audience string option:
    /// ```json
    /// {
    /// "token_audience": "centrifugo"
    /// }
    /// ```
    ///
    /// Setting token_audience will also affect subscription tokens (used for channel token authorization).
    /// If you need to separate connection token configuration and subscription token configuration
    /// check out separate subscription token config feature.
    ///
    /// This claim is optional.
    abstract final String? aud;

    /// Issuer.
    /// The "iss" (issuer) claim identifies the principal that issued the JWT.
    /// [rfc7519 iss claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)
    /// By default, Centrifugo does not check JWT issuer (rfc7519 iss claim).
    /// But you can force this check by setting token_issuer string option:
    /// ```json
    /// {
    /// "token_issuer": "my_app"
    /// }
    /// ```
    ///
    /// Setting token_issuer will also affect subscription tokens (used for channel token authorization).
    /// If you need to separate connection token configuration and subscription token configuration
    /// check out separate subscription token config feature.
    ///
    /// This claim is optional.
    abstract final String? iss;

    /// This claim is optional - this is additional information about client connection
    /// that can be provided for Centrifugo.
    /// This information will be included in presence information,
    /// join/leave events, and channel publication if it was published from a client-side.
    abstract final Map<String, Object?>? info;

    /// If you are using binary Protobuf protocol you may want info to be custom bytes. Use this field in this case.
    ///
    /// This field contains a `base64` representation of your bytes.
    /// After receiving Centrifugo will decode base64 back to bytes
    /// and will embed the result into various places described above.
    abstract final String? b64info;

    /// An optional array of strings with server-side channels to subscribe a client to.
    /// See more details about [server-side subscriptions](https://centrifugal.dev/docs/server/server_subs).
    abstract final List<String>? channels;

    /// Subscriptions
    /// An optional map of channels with options. This is like a channels claim
    /// but allows more control over server-side subscription since every channel
    /// can be annotated with info, data, and so on using options.
    /// The claim sub described above is a standart JWT claim to provide a user ID
    /// (it's a shortcut from subject).
    /// While claims have similar names they have different purpose in a connection JWT.
    ///
    /// Example:
    /// ```json
    /// {
    /// ...
    /// "subs": {
    /// "channel1": {
    /// "data": {"welcome": "welcome to channel1"}
    /// },
    /// "channel2": {
    /// "data": {"welcome": "welcome to channel2"}
    /// }
    /// }
    /// }
    /// ```
    ///
    /// Subscribe options:
    /// - (optional) info - JSON object - Custom channel info
    /// - (optional) b64info - string - Custom channel info in Base64 - to pass binary channel info
    /// - (optional) data - JSON object - Custom JSON data to return in subscription context inside Connect reply
    /// - (optional) b64data - string - Same as `data` but in Base64 to send binary data
    /// - (optional) override - Override object - Allows dynamically override some channel options.
    ///
    /// Override object:
    /// - (optional) presence - BoolValue - Override presence
    /// - (optional) join_leave - BoolValue - Override join_leave
    /// - (optional) position - BoolValue - Override position
    /// - (optional) recover - BoolValue - Override recover
    ///
    /// BoolValue is an object like this:
    /// ```json
    /// {
    /// "value": true
    /// }
    /// ```
    abstract final Map<String, Object?>? subs;

    /// Meta is an additional JSON object (ex. `{"key": "value"}`) that will be attached to a connection.
    /// Unlike `info` it's never exposed to clients inside presence and join/leave payloads
    /// and only accessible on a backend side. It may be included in proxy calls from Centrifugo
    /// to the application backend (see `proxy_include_connection_meta` option).
    /// Also, there is a `connections` API method in Centrifugo PRO that returns
    /// this data in the connection description object.
    abstract final Map<String, Object?>? meta;

    /// By default, Centrifugo looks on `exp` claim to configure connection expiration.
    /// In most cases this is fine, but there could be situations where you wish to decouple token expiration
    /// check with connection expiration time. As soon as the `expire_at` claim is provided (set)
    /// in JWT Centrifugo relies on it for setting
    /// connection expiration time (JWT expiration still checked over `exp` though).
    ///
    /// `expire_at` is a UNIX timestamp seconds when the connection should expire.
    ///
    /// Set it to the future time for expiring connection at some point
    /// Set it to 0 to disable connection expiration (but still check token exp claim).
    abstract final int? expireAt;

    /// Генерирует JWT с HMAC-SHA256 подписью.
    String encode(String secret);
    }

    final class _JWTImpl extends JWT {
    const _JWTImpl({
    required this.sub,
    this.exp,
    this.iat,
    this.jti,
    this.aud,
    this.iss,
    this.info,
    this.b64info,
    this.channels,
    this.subs,
    this.meta,
    this.expireAt,
    }) : super._();

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

    if (secret != null) {
    // Вычисление подписи
    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);

    // Сравнение подписи в токене с вычисленной подписью
    if (computedSignature != encodedSignature) throw FormatException('Invalid token signature');
    }

    Map<String, Object?> payload;
    try {
    payload = const Base64Decoder()
    .fuse<String>(const Utf8Decoder())
    .fuse<Map<String, Object?>>(const JsonDecoder().cast<String, Map<String, Object?>>())
    .convert(encodedPayload);
    } on Object catch (_, stackTrace) {
    Error.throwWithStackTrace(FormatException('Can\'t decode token payload'), stackTrace);
    }
    try {
    return _JWTImpl(
    sub: payload['sub'] as String,
    exp: payload['exp'] as int?,
    iat: payload['iat'] as int?,
    jti: payload['jti'] as String?,
    aud: payload['aud'] as String?,
    iss: payload['iss'] as String?,
    info: payload['info'] as Map<String, Object?>?,
    b64info: payload['b64info'] as String?,
    channels: (payload['channels'] as Iterable<Object?>?)?.whereType<String>().toList(),
    subs: payload['subs'] as Map<String, Object?>?,
    meta: payload['meta'] as Map<String, Object?>?,
    expireAt: payload['expire_at'] as int?,
    );
    } on Object catch (_, stackTrace) {
    Error.throwWithStackTrace(FormatException('Invalid token payload data'), stackTrace);
    }
    }

    static final Converter<Map<String, Object?>, String> _$encoder = const JsonEncoder()
    .cast<Map<String, Object?>, String>()
    .fuse<List<int>>(const Utf8Encoder())
    .fuse<String>(const Base64Encoder.urlSafe())
    .fuse<String>(const _UnpaddedBase64Converter());

    static final String _$headerHmacSha256 = _$encoder.convert(<String, Object?>{
    "alg": "HS256",
    "typ": "JWT",
    });

    @override
    final String sub;

    @override
    final int? exp;

    @override
    final int? iat;

    @override
    final String? jti;

    @override
    final String? aud;

    @override
    final String? iss;

    @override
    final Map<String, Object?>? info;

    @override
    final String? b64info;

    @override
    final List<String>? channels;

    @override
    final Map<String, Object?>? subs;

    @override
    final Map<String, Object?>? meta;

    @override
    final int? expireAt;

    @override
    String encode(String secret) {
    // Кодирование заголовка и полезной нагрузки
    final encodedHeader = _$headerHmacSha256;
    final encodedPayload = _$encoder.convert(<String, Object?>{
    'sub': this.sub,
    if (exp != null) 'exp': this.exp,
    if (iat != null) 'iat': this.iat,
    if (jti != null) 'jti': this.jti,
    if (aud != null) 'aud': this.aud,
    if (iss != null) 'iss': this.iss,
    if (info != null) 'info': this.info,
    if (b64info != null) 'b64info': this.b64info,
    if (channels != null) 'channels': this.channels,
    if (subs != null) 'subs': this.subs,
    if (meta != null) 'meta': this.meta,
    if (expireAt != null) 'expire_at': this.expireAt,
    });

    // Создание подписи
    var key = utf8.encode(secret); // Your 256 bit secret key
    var bytes = utf8.encode('$encodedHeader.$encodedPayload');

    var hmacSha256 = Hmac(sha256, key); // HMAC-SHA256
    var digest = hmacSha256.convert(bytes);

    // Кодирование подписи
    var encodedSignature =
    const Base64Encoder.urlSafe().fuse<String>(const _UnpaddedBase64Converter()).convert(digest.bytes);

    // Создание JWT
    return '$encodedHeader.$encodedPayload.$encodedSignature';
    }
    }

    class _UnpaddedBase64Converter extends Converter<String, String> {
    const _UnpaddedBase64Converter();

    @override
    String convert(String input) {
    final padding = input.indexOf('=', input.length - 2);
    if (padding != -1) return input.substring(0, padding);
    return input;
    }
    }