-
-
Save ziqq/aaad16283f1a2ffc09540470d6ebc18d to your computer and use it in GitHub Desktop.
JWT Hmac SHA256 HS256
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:collection'; | |
import 'dart:convert'; | |
import 'dart:typed_data'; | |
import 'package:collection/collection.dart'; | |
import 'package:crypto/crypto.dart'; | |
import 'package:meta/meta.dart'; | |
/// Токен JWT состоит из трех частей: заголовка (header), полезной нагрузки (payload) и подписи или данных шифрования. | |
/// Первые два элемента — это JSON объекты определенной структуры. | |
/// Третий элемент вычисляется на основании первых и зависит от выбранного алгоритма | |
/// (в случае использования неподписанного JWT может быть опущен). | |
/// Токены могут быть перекодированы в компактное представление (JWS/JWE Compact Serialization): | |
/// к заголовку и полезной нагрузке применяется алгоритм кодирования Base64-URL, | |
/// после чего добавляется подпись и все три элемента разделяются точками («.»). | |
@immutable | |
abstract class JWT { | |
/// Decode a string JWT token into a `Map<String, Object>` | |
/// containing the decoded JSON payload. | |
/// | |
/// Note: header and signature are not returned by this method. | |
/// | |
/// Throws [FormatException] if parameter is not a valid JWT token. | |
factory JWT.decode(String token) = _JWTImpl.decode; | |
/// Can return null, static method, not factory | |
// ignore: prefer_constructors_over_static_methods | |
static JWT? tryDecode(String token) { | |
try { | |
return JWT.decode(token); | |
} on Object { | |
return null; | |
} | |
} | |
/// Генерирует JWT | |
/// Реализован только алгоритм HS256 | |
factory JWT.create({ | |
/// Payload / Data | |
required final Map<String, Object?> payload, | |
/// Header, algorithm & token type | |
final Map<String, Object?>? header, | |
/// Secret for creating a signed token | |
final String? secret, | |
}) => | |
_JWTImpl.create( | |
header: header ?? | |
<String, Object?>{ | |
'alg': 'HS256', | |
'typ': 'JWT', | |
}, | |
payload: payload, | |
secret: secret ?? 'secret', | |
); | |
/// Токен подписан, [signature] не пустая | |
bool get isSigned; | |
/// Header, algorithm & token type | |
JWTHeader get header; | |
/// Payload / Data | |
JWTPayload get payload; | |
/// Verify signature | |
String get signature; | |
/// Validates and returns a list of validation errors. | |
/// Empty set indicates there were no validation errors. | |
/// Проверяет заполнение содержимого | |
Set<String> validatePayload({ | |
/// Проверить алгоритм на соответсвие | |
final String? algorithm, | |
/// Текущее время для проверки актуальности токена, по умолчанию - DateTime.now() | |
final DateTime? dateTime, | |
/// Допуск расхождения с текущим временем (с точностью до секунд) | |
final Duration? tolerance, | |
/// Проверить истекание | |
final bool expiresAt = true, | |
/// Проверить выпуск | |
final bool issuedAt = false, | |
/// Проверить время с которого он начинает действовать | |
final bool notBefore = false, | |
/// Проверить идентификатор проекта | |
final String? audience, | |
/// Проверить выпустившего токен | |
final String? issuer, | |
/// Проверить принадлежность пользователю | |
final String? subject, | |
}); | |
/// Получить исходный токен в виде строки | |
@override | |
String toString(); | |
} | |
/// HashMap<String, Object?> | |
@immutable | |
abstract class _ImmutableJSON extends MapBase<String, Object?> { | |
Map<String, Object?> get _source; | |
@override | |
Object? operator [](Object? key) => _source[key]; | |
@override | |
Iterable<String> get keys => _source.keys; | |
@override | |
void operator []=(String key, Object? value) => throw UnsupportedError('Cannot modify unmodifiable map'); | |
@override | |
void clear() => throw UnsupportedError('Cannot modify unmodifiable map'); | |
@override | |
Object? remove(Object? key) => throw UnsupportedError('Cannot modify unmodifiable map'); | |
@override | |
int get hashCode => _source.hashCode; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || (other is _ImmutableJSON && const DeepCollectionEquality.unordered().equals(_source, other._source)); | |
} | |
/// В заголовке указывается необходимая информация для описания самого токена. | |
@immutable | |
abstract class JWTHeader extends _ImmutableJSON { | |
/// List of standard (reserved) claims. | |
static const Set<String> reservedClaims = <String>{'alg', 'kid', 'typ', 'cty'}; | |
/// Algorithm (alg) | |
/// Алгоритм, используемый для подписи/шифрования (в случае неподписанного JWT используется значение «none»). | |
String get algorithm; | |
/// Key ID (kid) | |
String? get keyID; | |
/// Type of token (typ) | |
/// Тип токена (type). Используется в случае, когда токены смешиваются с другими объектами, | |
/// имеющими JOSE заголовки. Должно иметь значение «JWT». | |
String? get tokenType; | |
/// Content type (cty) | |
/// Тип содержимого (content type). | |
/// Если в токене помимо зарегистрированных служебных ключей есть пользовательские, | |
/// то данный ключ не должен присутствовать. В противном случае должно иметь значение «JWT» | |
String? get contentType; | |
} | |
/// В данной секции указывается пользовательская информация (например, имя пользователя и уровень его доступа), | |
/// а также могут быть использованы некоторые служебные ключи. Все они являются необязательными | |
abstract class JWTPayload extends _ImmutableJSON { | |
/// List of standard (reserved) claims / payload. | |
static const Set<String> reservedClaims = <String>{'iss', 'aud', 'iat', 'exp', 'nbf', 'sub', 'jti'}; | |
/// The issuer of this token (iss). | |
/// Чувствительная к регистру строка или URI, | |
/// которая является уникальным идентификатором стороны, генерирующей токен (issuer). | |
String? get issuer; | |
/// The audience of this token (aud). | |
/// Массив чувствительных к регистру строк или URI, являющийся списком получателей данного токена. | |
/// Когда принимающая сторона получает JWT с данным ключом, | |
/// она должна проверить наличие себя в получателях — иначе проигнорировать токен (audience). | |
Object? get audience; | |
/// The time this token was issued (iat). | |
/// Время в формате Unix Time, определяющее момент, когда токен был создан. | |
/// [issuedAt] и [notBefore] могут не совпадать, например, если токен был создан раньше, | |
/// чем время, когда он должен стать валидным | |
int? get issuedAt; | |
/// The expiration time of this token (exp). | |
/// Время в формате Unix Time, определяющее момент, когда токен станет невалидным (expiration). | |
int? get expiresAt; | |
/// The time before which this token must not be accepted (nbf). | |
/// В противоположность ключу [expiresAt], это время в формате Unix Time, определяющее момент, | |
/// когда токен станет валидным (not before). | |
int? get notBefore; | |
/// Identifies the principal that is the subject of this token (sub). | |
/// Чувствительная к регистру строка или URI, которая является уникальным идентификатором стороны, | |
/// о которой содержится информация в данном токене (subject). | |
/// Значения с этим ключом должны быть уникальны в контексте стороны, генерирующей JWT. | |
/// По сути это UID пользователя. | |
String? get subject; | |
/// Unique identifier of this token (jti). | |
/// Строка, определяющая уникальный идентификатор данного токена (JWT ID). | |
String? get jwtID; | |
} | |
class _JWTImpl with _JWTValidationMixin implements JWT { | |
factory _JWTImpl.decode(String token) { | |
final splitToken = token.split('.'); // Split the token by '.' | |
final length = splitToken.length; | |
if (length < 2 || length > 3) { | |
throw const FormatException('Invalid token'); | |
} | |
return _JWTImpl._( | |
header: _JWTHeaderImpl.decode(splitToken[0]), | |
payload: _JWTPayloadImpl.decode(splitToken[1]), | |
signature: length < 3 ? '' : splitToken[2], | |
token: token, | |
); | |
} | |
_JWTImpl._({ | |
required final this.header, | |
required final this.payload, | |
required final this.signature, | |
required final String token, | |
}) : _token = token; | |
factory _JWTImpl.create({ | |
required final Map<String, Object?> header, | |
required final Map<String, Object?> payload, | |
required final String? secret, | |
}) { | |
final createdHeader = _JWTHeaderImpl.create(header); | |
final createdPayload = _JWTPayloadImpl.create(payload); | |
final _jsonToBase64Url = json.fuse(utf8.fuse(base64Url)); | |
final buffer = StringBuffer(); | |
final headerBase64 = _base64Unpadded(_jsonToBase64Url.encode(createdHeader)); | |
buffer.write(headerBase64); | |
final payloadBase64 = _base64Unpadded(_jsonToBase64Url.encode(createdPayload)); | |
buffer | |
..write('.') | |
..write(payloadBase64); | |
if (secret != null) { | |
final algorithm = JWTAlgorithm.fromName(createdHeader.algorithm); | |
final body = Uint8List.fromList(utf8.encode(buffer.toString())); | |
final signature = _base64Unpadded(base64Url.encode(algorithm.sign(secret, body))); | |
buffer | |
..write('.') | |
..write(signature); | |
} | |
final jwt = _JWTImpl.decode(buffer.toString()); | |
buffer.clear(); | |
return jwt; | |
} | |
/// Исходный токен | |
final String _token; | |
@override | |
bool get isSigned => signature.isNotEmpty; | |
@override | |
final JWTHeader header; | |
@override | |
final JWTPayload payload; | |
@override | |
final String signature; | |
@override | |
String toString() => _token; | |
@override | |
int get hashCode => _token.hashCode; | |
@override | |
bool operator ==(Object other) => identical(this, other) || (other is _JWTImpl && _token == other._token); | |
} | |
class _JWTHeaderImpl extends JWTHeader { | |
factory _JWTHeaderImpl.decode(final String header) { | |
try { | |
// Base64 should be multiple of 4. Normalize the payload before decode it | |
final normalizedPayload = base64.normalize(header); | |
// Decode payload, the result is a String | |
final payloadString = utf8.decode(base64.decode(normalizedPayload)); | |
// Parse the String to a Map<String, Object?> | |
final data = jsonDecode(payloadString) as Map<String, Object?>; | |
return _JWTHeaderImpl._(data); | |
} on Object { | |
throw const FormatException('Invalid header'); | |
} | |
} | |
factory _JWTHeaderImpl.create(Map<String, Object?> source) = _JWTHeaderImpl._; | |
_JWTHeaderImpl._(Map<String, Object?> source) | |
: _source = source, | |
algorithm = source['alg']?.toString() ?? '', | |
contentType = source['cty']?.toString(), | |
keyID = source['kid']?.toString(), | |
tokenType = source['typ']?.toString(); | |
@override | |
final Map<String, Object?> _source; | |
@override | |
final String algorithm; | |
@override | |
final String? contentType; | |
@override | |
final String? keyID; | |
@override | |
final String? tokenType; | |
} | |
class _JWTPayloadImpl extends JWTPayload { | |
factory _JWTPayloadImpl.decode(final String payload) { | |
try { | |
// Base64 should be multiple of 4. Normalize the payload before decode it | |
final normalizedPayload = base64.normalize(payload); | |
// Decode payload, the result is a String | |
final payloadString = utf8.decode(base64.decode(normalizedPayload)); | |
// Parse the String to a Map<String, Object?> | |
final data = jsonDecode(payloadString) as Map<String, Object?>; | |
return _JWTPayloadImpl._(data); | |
} on Object { | |
throw const FormatException('Invalid payload'); | |
} | |
} | |
factory _JWTPayloadImpl.create(Map<String, Object?> source) = _JWTPayloadImpl._; | |
_JWTPayloadImpl._(Map<String, Object?> source) | |
: _source = source, | |
audience = source['aud'], | |
expiresAt = _getInt(source['exp']), | |
issuedAt = _getInt(source['iat']), | |
issuer = source['iss']?.toString(), | |
jwtID = source['jti']?.toString(), | |
notBefore = _getInt(source['nbf']), | |
subject = source['sub']?.toString(); | |
@override | |
final Map<String, Object?> _source; | |
@override | |
final Object? audience; | |
@override | |
final int? expiresAt; | |
@override | |
final int? issuedAt; | |
@override | |
final String? issuer; | |
@override | |
final String? jwtID; | |
@override | |
final int? notBefore; | |
@override | |
final String? subject; | |
static int? _getInt(Object? obj) { | |
if (obj is int) { | |
return obj; | |
} else if (obj is String) { | |
return int.tryParse(obj); | |
} else { | |
return null; | |
} | |
} | |
} | |
mixin _JWTValidationMixin implements JWT { | |
@override | |
Set<String> validatePayload({ | |
final String? algorithm, | |
final DateTime? dateTime, | |
final Duration? tolerance, | |
final bool expiresAt = true, | |
final bool issuedAt = true, | |
final bool notBefore = false, | |
final String? audience, | |
final String? issuer, | |
final String? subject, | |
}) { | |
final errors = <String>{}; | |
final currentTimestamp = (dateTime ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000; | |
final toleranceSec = tolerance?.inSeconds ?? 0; | |
if (header.algorithm.isEmpty) { | |
errors.add('Algorithm must be a non-empty string'); | |
} | |
if (algorithm != null && algorithm != header.algorithm) { | |
errors.add('Algorithm is not the same'); | |
} | |
if (expiresAt) { | |
final data = payload.expiresAt; | |
if (data == null || data - currentTimestamp + toleranceSec < 0) { | |
errors.add('The token has expired'); | |
} | |
} | |
if (issuedAt) { | |
final data = payload.issuedAt; | |
if (data == null || currentTimestamp - data + toleranceSec < 0) { | |
errors.add('The token issuedAt time is in future'); | |
} | |
} | |
if (notBefore) { | |
final data = payload.notBefore; | |
if (data == null || currentTimestamp - data + toleranceSec < 0) { | |
errors.add('The token can not be accepted due to notBefore policy'); | |
} | |
} | |
if (audience != null) { | |
if (audience != payload.audience) { | |
errors.add('Audience must be your project ID'); | |
} | |
} | |
if (issuer != null) { | |
if (issuer != payload.issuer) { | |
errors.add('Issuer is not the same'); | |
} | |
} | |
if (subject != null) { | |
if (subject != payload.subject) { | |
errors.add('Subject must be a non-empty string and must be the uid of the user or device'); | |
} | |
} | |
return errors; | |
} | |
} | |
/// Алгоритм взаимодействия с подписью JWT | |
abstract class JWTAlgorithm { | |
/// HMAC using SHA-256 hash algorithm | |
static const JWTAlgorithm hs256 = _HMAC256Algorithm(); | |
/// Return the `JWTAlgorithm` from his string name | |
static JWTAlgorithm fromName(String name) { | |
switch (name) { | |
case 'HS256': | |
return JWTAlgorithm.hs256; | |
default: | |
throw const FormatException('Unknown algorithm'); | |
} | |
} | |
const JWTAlgorithm(); | |
/// `JWTAlgorithm` name | |
String get name; | |
/// Create a signature of the `body` with `key` | |
/// | |
/// return the signature as bytes | |
Uint8List sign(String key, Uint8List body); | |
/// Verify the `signature` of `body` with `key` | |
/// | |
/// return `true` if the signature is correct `false` otherwise | |
bool verify(String key, Uint8List body, Uint8List signature); | |
} | |
class _HMAC256Algorithm extends JWTAlgorithm { | |
const _HMAC256Algorithm(); | |
@override | |
String get name => 'HS256'; | |
@override | |
Uint8List sign(String key, Uint8List body) { | |
final hmac = Hmac(sha256, utf8.encode(key)); | |
return Uint8List.fromList(hmac.convert(body).bytes); | |
} | |
@override | |
bool verify(String key, Uint8List body, Uint8List signature) { | |
try { | |
// Приведем подпись к тому же Base64 виду, что и исходная | |
final actual = Uint8List.fromList(utf8.encode(_base64Unpadded(base64Url.encode(sign(key, body))))); | |
if (actual.length != signature.length) { | |
return false; | |
} | |
for (var i = 0; i < actual.length; i++) { | |
if (actual[i] != signature[i]) return false; | |
} | |
return true; | |
} on Object { | |
return false; | |
} | |
} | |
} | |
String _base64Unpadded(String value) { | |
if (value.endsWith('==')) return value.substring(0, value.length - 2); | |
if (value.endsWith('=')) return value.substring(0, value.length - 1); | |
return value; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment