import 'dart:collection'; import 'package:angel3_framework/angel3_framework.dart'; import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:logging/logging.dart'; /// Calls [BASE64URL], but also works for strings with lengths /// that are *not* multiples of 4. String decodeBase64(String str) { var output = str.replaceAll('-', '+').replaceAll('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw 'Illegal base64url string!"'; } return utf8.decode(base64Url.decode(output)); } class AuthToken { static final _log = Logger('AuthToken'); final SplayTreeMap _header = SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'}); String? ipAddress; late DateTime issuedAt; num lifeSpan; var userId; Map payload = {}; AuthToken( {this.ipAddress, this.lifeSpan = -1, this.userId, DateTime? issuedAt, Map payload = const {}}) { this.issuedAt = issuedAt ?? DateTime.now(); this.payload.addAll(payload.keys .fold({}, ((out, k) => out?..[k.toString()] = payload[k])) ?? {}); /* this.payload.addAll(payload.keys.fold( {}, ((out, k) => out..[k.toString()] = payload[k]) as Map? Function( Map?, dynamic)) ?? {}); */ } factory AuthToken.fromJson(String jsons) => AuthToken.fromMap(json.decode(jsons) as Map); factory AuthToken.fromMap(Map data) { return AuthToken( ipAddress: data['aud'].toString(), lifeSpan: data['exp'] as num, issuedAt: DateTime.parse(data['iat'].toString()), userId: data['sub'], payload: data['pld'] as Map); } factory AuthToken.parse(String jwt) { var split = jwt.split('.'); if (split.length != 3) { _log.severe('Invalid JWT'); throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.'); } var payloadString = decodeBase64(split[1]); return AuthToken.fromMap(json.decode(payloadString) as Map); } factory AuthToken.validate(String jwt, Hmac hmac) { var split = jwt.split('.'); if (split.length != 3) { _log.severe('Invalid JWT'); throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.'); } // var headerString = decodeBase64(split[0]); var payloadString = decodeBase64(split[1]); var data = split[0] + '.' + split[1]; var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes); if (signature != split[2]) { _log.severe('JWT payload does not match hashed version'); throw AngelHttpException.notAuthenticated( message: 'JWT payload does not match hashed version.'); } return AuthToken.fromMap(json.decode(payloadString) as Map); } String serialize(Hmac hmac) { var headerString = base64Url.encode(json.encode(_header).codeUnits); var payloadString = base64Url.encode(json.encode(toJson()).codeUnits); var data = headerString + '.' + payloadString; var signature = hmac.convert(data.codeUnits).bytes; return data + '.' + base64Url.encode(signature); } Map toJson() { return _splayify({ 'iss': 'angel_auth', 'aud': ipAddress, 'exp': lifeSpan, 'iat': issuedAt.toIso8601String(), 'sub': userId, 'pld': _splayify(payload) }); } } SplayTreeMap _splayify(Map map) { var data = {}; map.forEach((k, v) { data[k] = _splay(v); }); return SplayTreeMap.from(data); } dynamic _splay(value) { if (value is Iterable) { return value.map(_splay).toList(); } else if (value is Map) { return _splayify(value); } else { return value; } }