2016-09-21 23:09:23 +00:00
|
|
|
import 'dart:collection';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2018-09-11 22:14:33 +00:00
|
|
|
import 'dart:convert';
|
2016-09-21 23:09:23 +00:00
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
|
2017-01-20 23:15:21 +00:00
|
|
|
/// 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!"';
|
|
|
|
}
|
|
|
|
|
2018-06-27 16:36:31 +00:00
|
|
|
return utf8.decode(base64Url.decode(output));
|
2017-01-20 23:15:21 +00:00
|
|
|
}
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
class AuthToken {
|
|
|
|
final SplayTreeMap<String, String> _header =
|
2021-03-20 23:51:20 +00:00
|
|
|
SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
String? ipAddress;
|
|
|
|
late DateTime issuedAt;
|
|
|
|
num? lifeSpan;
|
2016-09-21 23:09:23 +00:00
|
|
|
var userId;
|
2017-01-13 15:50:38 +00:00
|
|
|
Map<String, dynamic> payload = {};
|
2016-09-21 23:09:23 +00:00
|
|
|
|
|
|
|
AuthToken(
|
2017-01-13 15:50:38 +00:00
|
|
|
{this.ipAddress,
|
2019-01-05 23:54:48 +00:00
|
|
|
this.lifeSpan = -1,
|
2017-01-13 15:50:38 +00:00
|
|
|
this.userId,
|
2021-03-20 23:51:20 +00:00
|
|
|
DateTime? issuedAt,
|
2019-01-05 23:54:48 +00:00
|
|
|
Map payload = const {}}) {
|
2019-04-19 07:50:04 +00:00
|
|
|
this.issuedAt = issuedAt ?? DateTime.now();
|
2021-03-20 23:51:20 +00:00
|
|
|
this.payload.addAll(payload?.keys?.fold(
|
|
|
|
{},
|
|
|
|
((out, k) => out..[k.toString()] = payload[k])
|
|
|
|
as Map<String, dynamic>? Function(
|
|
|
|
Map<String, dynamic>?, dynamic)) ??
|
|
|
|
{});
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2018-06-27 16:36:31 +00:00
|
|
|
factory AuthToken.fromJson(String jsons) =>
|
2019-04-19 07:50:04 +00:00
|
|
|
AuthToken.fromMap(json.decode(jsons) as Map);
|
2016-09-21 23:09:23 +00:00
|
|
|
|
|
|
|
factory AuthToken.fromMap(Map data) {
|
2019-04-19 07:50:04 +00:00
|
|
|
return AuthToken(
|
2021-03-20 23:51:20 +00:00
|
|
|
ipAddress: data['aud'].toString(),
|
|
|
|
lifeSpan: data['exp'] as num?,
|
|
|
|
issuedAt: DateTime.parse(data['iat'].toString()),
|
|
|
|
userId: data['sub'],
|
|
|
|
payload: data['pld'] as Map? ?? {});
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2017-01-20 23:15:21 +00:00
|
|
|
factory AuthToken.parse(String jwt) {
|
2021-03-20 23:51:20 +00:00
|
|
|
var split = jwt.split('.');
|
2017-01-20 23:15:21 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
if (split.length != 3) {
|
|
|
|
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
|
|
|
|
}
|
2017-01-20 23:15:21 +00:00
|
|
|
|
|
|
|
var payloadString = decodeBase64(split[1]);
|
2019-04-19 07:50:04 +00:00
|
|
|
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
2017-01-20 23:15:21 +00:00
|
|
|
}
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
factory AuthToken.validate(String jwt, Hmac hmac) {
|
2021-03-20 23:51:20 +00:00
|
|
|
var split = jwt.split('.');
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
if (split.length != 3) {
|
|
|
|
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
|
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2017-01-20 23:15:21 +00:00
|
|
|
// var headerString = decodeBase64(split[0]);
|
|
|
|
var payloadString = decodeBase64(split[1]);
|
2021-03-20 23:51:20 +00:00
|
|
|
var data = split[0] + '.' + split[1];
|
2018-06-27 16:36:31 +00:00
|
|
|
var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes);
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
if (signature != split[2]) {
|
2019-04-19 07:50:04 +00:00
|
|
|
throw AngelHttpException.notAuthenticated(
|
2021-03-20 23:51:20 +00:00
|
|
|
message: 'JWT payload does not match hashed version.');
|
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2019-04-19 07:50:04 +00:00
|
|
|
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
String serialize(Hmac hmac) {
|
2018-06-27 16:36:31 +00:00
|
|
|
var headerString = base64Url.encode(json.encode(_header).codeUnits);
|
|
|
|
var payloadString = base64Url.encode(json.encode(toJson()).codeUnits);
|
2021-03-20 23:51:20 +00:00
|
|
|
var data = headerString + '.' + payloadString;
|
2016-09-21 23:09:23 +00:00
|
|
|
var signature = hmac.convert(data.codeUnits).bytes;
|
2021-03-20 23:51:20 +00:00
|
|
|
return data + '.' + base64Url.encode(signature);
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Map toJson() {
|
2017-01-13 15:50:38 +00:00
|
|
|
return _splayify({
|
2016-09-21 23:09:23 +00:00
|
|
|
"iss": "angel_auth",
|
|
|
|
"aud": ipAddress,
|
|
|
|
"exp": lifeSpan,
|
|
|
|
"iat": issuedAt.toIso8601String(),
|
2017-01-13 15:50:38 +00:00
|
|
|
"sub": userId,
|
|
|
|
"pld": _splayify(payload)
|
2016-09-21 23:09:23 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2017-01-13 15:50:38 +00:00
|
|
|
|
|
|
|
SplayTreeMap _splayify(Map map) {
|
|
|
|
var data = {};
|
|
|
|
map.forEach((k, v) {
|
|
|
|
data[k] = _splay(v);
|
|
|
|
});
|
2019-04-19 07:50:04 +00:00
|
|
|
return SplayTreeMap.from(data);
|
2017-01-13 15:50:38 +00:00
|
|
|
}
|
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
dynamic _splay(value) {
|
2017-01-13 15:50:38 +00:00
|
|
|
if (value is Iterable) {
|
|
|
|
return value.map(_splay).toList();
|
2021-03-20 23:51:20 +00:00
|
|
|
} else if (value is Map) {
|
2017-01-13 15:50:38 +00:00
|
|
|
return _splayify(value);
|
2021-03-20 23:51:20 +00:00
|
|
|
} else {
|
2017-01-13 15:50:38 +00:00
|
|
|
return value;
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2017-01-13 15:50:38 +00:00
|
|
|
}
|