17
This commit is contained in:
parent
ee72bf748c
commit
771a4e8a5c
8 changed files with 72 additions and 25 deletions
|
@ -1,6 +1,6 @@
|
||||||
# angel_auth
|
# angel_auth
|
||||||
|
|
||||||
[](https://pub.dartlang.org/packages/angel_auth)
|
[](https://pub.dartlang.org/packages/angel_auth)
|
||||||

|

|
||||||
|
|
||||||
A complete authentication plugin for Angel. Inspired by Passport.
|
A complete authentication plugin for Angel. Inspired by Passport.
|
||||||
|
|
|
@ -11,27 +11,35 @@ class AuthToken {
|
||||||
DateTime issuedAt;
|
DateTime issuedAt;
|
||||||
num lifeSpan;
|
num lifeSpan;
|
||||||
var userId;
|
var userId;
|
||||||
|
Map<String, dynamic> payload = {};
|
||||||
|
|
||||||
AuthToken(
|
AuthToken(
|
||||||
{this.ipAddress, this.lifeSpan: -1, this.userId, DateTime issuedAt}) {
|
{this.ipAddress,
|
||||||
|
this.lifeSpan: -1,
|
||||||
|
this.userId,
|
||||||
|
DateTime issuedAt,
|
||||||
|
Map<String, dynamic> payload: const {}}) {
|
||||||
this.issuedAt = issuedAt ?? new DateTime.now();
|
this.issuedAt = issuedAt ?? new DateTime.now();
|
||||||
|
this.payload.addAll(payload ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthToken.fromJson(String json) => new AuthToken.fromMap(JSON.decode(json));
|
factory AuthToken.fromJson(String json) =>
|
||||||
|
new AuthToken.fromMap(JSON.decode(json));
|
||||||
|
|
||||||
factory AuthToken.fromMap(Map data) {
|
factory AuthToken.fromMap(Map data) {
|
||||||
return new AuthToken(
|
return new AuthToken(
|
||||||
ipAddress: data["aud"],
|
ipAddress: data["aud"],
|
||||||
lifeSpan: data["exp"],
|
lifeSpan: data["exp"],
|
||||||
issuedAt: DateTime.parse(data["iat"]),
|
issuedAt: DateTime.parse(data["iat"]),
|
||||||
userId: data["sub"]);
|
userId: data["sub"],
|
||||||
|
payload: data["pld"] ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthToken.validate(String jwt, Hmac hmac) {
|
factory AuthToken.validate(String jwt, Hmac hmac) {
|
||||||
var split = jwt.split(".");
|
var split = jwt.split(".");
|
||||||
|
|
||||||
if (split.length != 3)
|
if (split.length != 3)
|
||||||
throw new AngelHttpException.NotAuthenticated(message: "Invalid JWT.");
|
throw new AngelHttpException.notAuthenticated(message: "Invalid JWT.");
|
||||||
|
|
||||||
// var headerString = new String.fromCharCodes(BASE64URL.decode(split[0]));
|
// var headerString = new String.fromCharCodes(BASE64URL.decode(split[0]));
|
||||||
var payloadString = new String.fromCharCodes(BASE64URL.decode(split[1]));
|
var payloadString = new String.fromCharCodes(BASE64URL.decode(split[1]));
|
||||||
|
@ -39,7 +47,7 @@ class AuthToken {
|
||||||
var signature = BASE64URL.encode(hmac.convert(data.codeUnits).bytes);
|
var signature = BASE64URL.encode(hmac.convert(data.codeUnits).bytes);
|
||||||
|
|
||||||
if (signature != split[2])
|
if (signature != split[2])
|
||||||
throw new AngelHttpException.NotAuthenticated(
|
throw new AngelHttpException.notAuthenticated(
|
||||||
message: "JWT payload does not match hashed version.");
|
message: "JWT payload does not match hashed version.");
|
||||||
|
|
||||||
return new AuthToken.fromMap(JSON.decode(payloadString));
|
return new AuthToken.fromMap(JSON.decode(payloadString));
|
||||||
|
@ -47,19 +55,37 @@ class AuthToken {
|
||||||
|
|
||||||
String serialize(Hmac hmac) {
|
String serialize(Hmac hmac) {
|
||||||
var headerString = BASE64URL.encode(JSON.encode(_header).codeUnits);
|
var headerString = BASE64URL.encode(JSON.encode(_header).codeUnits);
|
||||||
var payloadString = BASE64URL.encode(JSON.encode(this).codeUnits);
|
var payloadString = BASE64URL.encode(JSON.encode(toJson()).codeUnits);
|
||||||
var data = headerString + "." + payloadString;
|
var data = headerString + "." + payloadString;
|
||||||
var signature = hmac.convert(data.codeUnits).bytes;
|
var signature = hmac.convert(data.codeUnits).bytes;
|
||||||
return data + "." + BASE64URL.encode(signature);
|
return data + "." + BASE64URL.encode(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map toJson() {
|
Map toJson() {
|
||||||
return new SplayTreeMap.from({
|
return _splayify({
|
||||||
"iss": "angel_auth",
|
"iss": "angel_auth",
|
||||||
"aud": ipAddress,
|
"aud": ipAddress,
|
||||||
"exp": lifeSpan,
|
"exp": lifeSpan,
|
||||||
"iat": issuedAt.toIso8601String(),
|
"iat": issuedAt.toIso8601String(),
|
||||||
"sub": userId
|
"sub": userId,
|
||||||
|
"pld": _splayify(payload)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SplayTreeMap _splayify(Map map) {
|
||||||
|
var data = {};
|
||||||
|
map.forEach((k, v) {
|
||||||
|
data[k] = _splay(v);
|
||||||
|
});
|
||||||
|
return new SplayTreeMap.from(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_splay(value) {
|
||||||
|
if (value is Iterable) {
|
||||||
|
return value.map(_splay).toList();
|
||||||
|
} else if (value is Map)
|
||||||
|
return _splayify(value);
|
||||||
|
else
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ class RequireAuthorizationMiddleware extends AngelMiddleware {
|
||||||
bool _reject(ResponseContext res) {
|
bool _reject(ResponseContext res) {
|
||||||
if (throwError) {
|
if (throwError) {
|
||||||
res.statusCode = HttpStatus.FORBIDDEN;
|
res.statusCode = HttpStatus.FORBIDDEN;
|
||||||
throw new AngelHttpException.Forbidden();
|
throw new AngelHttpException.forbidden();
|
||||||
} else
|
} else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.ip != null && req.ip != token.ipAddress)
|
if (req.ip != null && req.ip != token.ipAddress)
|
||||||
throw new AngelHttpException.Forbidden(
|
throw new AngelHttpException.forbidden(
|
||||||
message: "JWT cannot be accessed from this IP address.");
|
message: "JWT cannot be accessed from this IP address.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
|
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
|
||||||
|
|
||||||
if (!token.issuedAt.isAfter(new DateTime.now()))
|
if (!token.issuedAt.isAfter(new DateTime.now()))
|
||||||
throw new AngelHttpException.Forbidden(message: "Expired JWT.");
|
throw new AngelHttpException.forbidden(message: "Expired JWT.");
|
||||||
} else if (debug) {
|
} else if (debug) {
|
||||||
print('This token has an infinite life span.');
|
print('This token has an infinite life span.');
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
if (debug) print('Found JWT: $jwt');
|
if (debug) print('Found JWT: $jwt');
|
||||||
|
|
||||||
if (jwt == null) {
|
if (jwt == null) {
|
||||||
throw new AngelHttpException.Forbidden(message: "No JWT provided");
|
throw new AngelHttpException.forbidden(message: "No JWT provided");
|
||||||
} else {
|
} else {
|
||||||
var token = new AuthToken.validate(jwt, _hs256);
|
var token = new AuthToken.validate(jwt, _hs256);
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
.ip}');
|
.ip}');
|
||||||
|
|
||||||
if (req.ip != token.ipAddress)
|
if (req.ip != token.ipAddress)
|
||||||
throw new AngelHttpException.Forbidden(
|
throw new AngelHttpException.forbidden(
|
||||||
message: "JWT cannot be accessed from this IP address.");
|
message: "JWT cannot be accessed from this IP address.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is AngelHttpException) rethrow;
|
if (e is AngelHttpException) rethrow;
|
||||||
throw new AngelHttpException.BadRequest(message: "Malformed JWT");
|
throw new AngelHttpException.badRequest(message: "Malformed JWT");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,7 +262,7 @@ class AngelAuth extends AngelPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future authenticationFailure(RequestContext req, ResponseContext res) async {
|
Future authenticationFailure(RequestContext req, ResponseContext res) async {
|
||||||
throw new AngelHttpException.NotAuthenticated();
|
throw new AngelHttpException.notAuthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
logout([AngelAuthOptions options]) {
|
logout([AngelAuthOptions options]) {
|
||||||
|
|
|
@ -56,7 +56,7 @@ class LocalAuthStrategy extends AuthStrategy {
|
||||||
verificationResult =
|
verificationResult =
|
||||||
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
|
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
|
||||||
} else
|
} else
|
||||||
throw new AngelHttpException.BadRequest(errors: [invalidMessage]);
|
throw new AngelHttpException.badRequest(errors: [invalidMessage]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ class LocalAuthStrategy extends AuthStrategy {
|
||||||
} else if (verificationResult != null && verificationResult != false) {
|
} else if (verificationResult != null && verificationResult != false) {
|
||||||
return verificationResult;
|
return verificationResult;
|
||||||
} else {
|
} else {
|
||||||
throw new AngelHttpException.NotAuthenticated();
|
throw new AngelHttpException.notAuthenticated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
pubspec.yaml
12
pubspec.yaml
|
@ -1,11 +1,13 @@
|
||||||
name: angel_auth
|
name: angel_auth
|
||||||
description: A complete authentication plugin for Angel.
|
description: A complete authentication plugin for Angel.
|
||||||
version: 1.0.0-dev+16
|
version: 1.0.0-dev+17
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_auth
|
homepage: https://github.com/angel-dart/angel_auth
|
||||||
|
environment:
|
||||||
|
sdk: ">=1.19.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework: ">=1.0.0-dev <2.0.0"
|
angel_framework: ^1.0.0-dev
|
||||||
crypto: ">=2.0.0 <3.0.0"
|
crypto: ^2.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
http: ">= 0.11.3 < 0.12.0"
|
http: ^0.11.0
|
||||||
test: ">= 0.12.13 < 0.13.0"
|
test: ^0.12.0
|
||||||
|
|
|
@ -12,5 +12,24 @@ main() async {
|
||||||
|
|
||||||
var parsed = new AuthToken.validate(jwt, hmac);
|
var parsed = new AuthToken.validate(jwt, hmac);
|
||||||
print(parsed.toJson());
|
print(parsed.toJson());
|
||||||
|
expect(parsed.toJson()['aud'], equals(token.ipAddress));
|
||||||
|
expect(parsed.toJson()['sub'], equals(token.userId));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom payload', () {
|
||||||
|
var token =
|
||||||
|
new AuthToken(ipAddress: "localhost", userId: "thosakwe", payload: {
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": {
|
||||||
|
"one": 1,
|
||||||
|
"franken": ["stein"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var jwt = token.serialize(hmac);
|
||||||
|
print(jwt);
|
||||||
|
|
||||||
|
var parsed = new AuthToken.validate(jwt, hmac);
|
||||||
|
print(parsed.toJson());
|
||||||
|
expect(parsed.toJson()['pld'], equals(token.payload));
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -43,7 +43,7 @@ main() {
|
||||||
'/login',
|
'/login',
|
||||||
auth.authenticate('local',
|
auth.authenticate('local',
|
||||||
new AngelAuthOptions(callback: (req, res, token) {
|
new AngelAuthOptions(callback: (req, res, token) {
|
||||||
return res
|
res
|
||||||
..write('Hello!')
|
..write('Hello!')
|
||||||
..end();
|
..end();
|
||||||
})));
|
})));
|
||||||
|
|
Loading…
Reference in a new issue