2016-09-21 23:09:23 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:collection';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:math' as Math;
|
2016-09-21 06:19:52 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2016-09-21 23:09:23 +00:00
|
|
|
import 'package:crypto/crypto.dart';
|
2016-09-21 06:19:52 +00:00
|
|
|
import 'middleware/require_auth.dart';
|
2016-09-21 23:09:23 +00:00
|
|
|
import 'auth_token.dart';
|
2016-09-21 06:19:52 +00:00
|
|
|
import 'defs.dart';
|
|
|
|
import 'options.dart';
|
|
|
|
import 'strategy.dart';
|
|
|
|
|
|
|
|
class AngelAuth extends AngelPlugin {
|
2016-09-21 23:09:23 +00:00
|
|
|
Hmac _hs256;
|
|
|
|
num _jwtLifeSpan;
|
|
|
|
Math.Random _random = new Math.Random.secure();
|
|
|
|
final RegExp _rgxBearer = new RegExp(r"^Bearer");
|
|
|
|
RequireAuthorizationMiddleware _requireAuth =
|
|
|
|
new RequireAuthorizationMiddleware();
|
2016-10-08 11:39:39 +00:00
|
|
|
String middlewareName;
|
|
|
|
bool debug;
|
2016-09-21 23:09:23 +00:00
|
|
|
bool enforceIp;
|
2016-09-22 09:26:38 +00:00
|
|
|
String reviveTokenEndpoint;
|
2016-09-21 06:19:52 +00:00
|
|
|
List<AuthStrategy> strategies = [];
|
|
|
|
UserSerializer serializer;
|
|
|
|
UserDeserializer deserializer;
|
|
|
|
|
2016-10-08 11:39:39 +00:00
|
|
|
String _randomString(
|
|
|
|
{int length: 32,
|
|
|
|
String validChars:
|
|
|
|
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
|
2016-09-21 23:09:23 +00:00
|
|
|
var chars = <int>[];
|
|
|
|
|
|
|
|
while (chars.length < length) chars.add(_random.nextInt(validChars.length));
|
|
|
|
|
|
|
|
return new String.fromCharCodes(chars);
|
|
|
|
}
|
|
|
|
|
2016-10-08 11:39:39 +00:00
|
|
|
AngelAuth(
|
|
|
|
{String jwtKey,
|
|
|
|
num jwtLifeSpan,
|
|
|
|
this.debug: false,
|
|
|
|
this.enforceIp: true,
|
|
|
|
this.middlewareName: 'auth',
|
|
|
|
this.reviveTokenEndpoint: "/auth/token"})
|
|
|
|
: super() {
|
2016-09-21 23:09:23 +00:00
|
|
|
_hs256 = new Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
|
|
|
|
_jwtLifeSpan = jwtLifeSpan ?? -1;
|
|
|
|
}
|
|
|
|
|
2016-09-21 06:19:52 +00:00
|
|
|
@override
|
|
|
|
call(Angel app) async {
|
|
|
|
app.container.singleton(this);
|
2016-09-21 23:09:23 +00:00
|
|
|
if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth);
|
2016-09-21 06:19:52 +00:00
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
app.before.add(_decodeJwt);
|
2016-10-08 11:39:39 +00:00
|
|
|
app.registerMiddleware(middlewareName, _requireAuth);
|
2016-09-22 09:26:38 +00:00
|
|
|
|
|
|
|
if (reviveTokenEndpoint != null) {
|
|
|
|
app.post(reviveTokenEndpoint, _reviveJwt);
|
|
|
|
}
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
_decodeJwt(RequestContext req, ResponseContext res) async {
|
2016-10-08 11:39:39 +00:00
|
|
|
if (req.method == "POST" && req.path == reviveTokenEndpoint) {
|
2016-09-22 09:26:38 +00:00
|
|
|
// Shouldn't block invalid JWT if we are reviving it
|
2016-10-08 11:39:39 +00:00
|
|
|
|
|
|
|
if (debug)
|
|
|
|
print('Token revival endpoint accessed.');
|
|
|
|
|
|
|
|
return await _reviveJwt(req, res);
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
String jwt = _getJwt(req);
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
if (jwt != null) {
|
|
|
|
var token = new AuthToken.validate(jwt, _hs256);
|
|
|
|
|
|
|
|
if (enforceIp) {
|
|
|
|
if (req.ip != token.ipAddress)
|
|
|
|
throw new AngelHttpException.Forbidden(
|
|
|
|
message: "JWT cannot be accessed from this IP address.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (token.lifeSpan > -1) {
|
|
|
|
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
|
|
|
|
|
|
|
|
if (!token.issuedAt.isAfter(new DateTime.now()))
|
|
|
|
throw new AngelHttpException.Forbidden(message: "Expired JWT.");
|
|
|
|
}
|
|
|
|
|
|
|
|
req.properties["user"] = await deserializer(token.userId);
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
_getJwt(RequestContext req) {
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug) {
|
|
|
|
print('Attempting to parse JWT');
|
|
|
|
}
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
if (req.headers.value("Authorization") != null) {
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug) {
|
|
|
|
print('Found Auth header');
|
|
|
|
}
|
|
|
|
|
|
|
|
return req.headers
|
|
|
|
.value("Authorization")
|
|
|
|
.replaceAll(_rgxBearer, "")
|
|
|
|
.trim();
|
2016-09-22 09:26:38 +00:00
|
|
|
} else if (req.cookies.any((cookie) => cookie.name == "token")) {
|
2016-10-08 11:39:39 +00:00
|
|
|
print('Request has "token" cookie...');
|
2016-09-22 09:26:38 +00:00
|
|
|
return req.cookies.firstWhere((cookie) => cookie.name == "token").value;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
_reviveJwt(RequestContext req, ResponseContext res) async {
|
|
|
|
try {
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug)
|
|
|
|
print('Attempting to revive JWT...');
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
var jwt = _getJwt(req);
|
|
|
|
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug)
|
|
|
|
print('Found JWT: $jwt');
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
if (jwt == null) {
|
|
|
|
throw new AngelHttpException.Forbidden(message: "No JWT provided");
|
|
|
|
} else {
|
|
|
|
var token = new AuthToken.validate(jwt, _hs256);
|
|
|
|
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug)
|
|
|
|
print('Validated and deserialized: $token');
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
if (enforceIp) {
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug)
|
|
|
|
print('Token IP: ${token.ipAddress}. Current request sent from: ${req.ip}');
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
if (req.ip != token.ipAddress)
|
|
|
|
throw new AngelHttpException.Forbidden(
|
|
|
|
message: "JWT cannot be accessed from this IP address.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (token.lifeSpan > -1) {
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug) {
|
|
|
|
print('Checking if token has expired... Life span is ${token.lifeSpan}');
|
|
|
|
}
|
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
|
|
|
|
|
|
|
|
if (!token.issuedAt.isAfter(new DateTime.now())) {
|
2016-10-08 11:39:39 +00:00
|
|
|
print('Token has indeed expired! Resetting assignment date to current timestamp...');
|
2016-09-22 09:26:38 +00:00
|
|
|
// Extend its lifespan by changing iat
|
|
|
|
token.issuedAt = new DateTime.now();
|
2016-10-08 11:39:39 +00:00
|
|
|
} else if (debug) {
|
|
|
|
print('Token has not expired yet.');
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
2016-10-08 11:39:39 +00:00
|
|
|
} else if(debug) {
|
|
|
|
print('This token never expires, so it is still valid.');
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
|
|
|
|
2016-10-08 11:39:39 +00:00
|
|
|
if (debug) {
|
|
|
|
print('Final, valid token: ${token.toJson()}');
|
|
|
|
}
|
|
|
|
|
|
|
|
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
|
2016-09-22 09:26:38 +00:00
|
|
|
return token.toJson();
|
|
|
|
}
|
2016-10-08 11:39:39 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
if (debug) {
|
|
|
|
print('An error occurred while reviving this token.');
|
|
|
|
print(e);
|
|
|
|
print(st);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e is AngelHttpException) rethrow;
|
2016-09-22 09:26:38 +00:00
|
|
|
throw new AngelHttpException.BadRequest(message: "Malformed JWT");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-21 06:19:52 +00:00
|
|
|
authenticate(String type, [AngelAuthOptions options]) {
|
|
|
|
return (RequestContext req, ResponseContext res) async {
|
|
|
|
AuthStrategy strategy =
|
2016-09-21 23:09:23 +00:00
|
|
|
strategies.firstWhere((AuthStrategy x) => x.name == type);
|
2016-09-21 06:19:52 +00:00
|
|
|
var result = await strategy.authenticate(req, res, options);
|
|
|
|
if (result == true)
|
|
|
|
return result;
|
|
|
|
else if (result != false) {
|
2016-09-21 23:09:23 +00:00
|
|
|
var userId = await serializer(result);
|
|
|
|
|
|
|
|
// Create JWT
|
|
|
|
var jwt = new AuthToken(userId: userId, lifeSpan: _jwtLifeSpan)
|
|
|
|
.serialize(_hs256);
|
|
|
|
req.cookies.add(new Cookie("token", jwt));
|
|
|
|
|
|
|
|
if (req.headers.value("accept") != null &&
|
|
|
|
(req.headers.value("accept").contains("application/json") ||
|
|
|
|
req.headers.value("accept").contains("*/*") ||
|
|
|
|
req.headers.value("accept").contains("application/*"))) {
|
|
|
|
return {"data": result, "token": jwt};
|
2016-10-08 11:39:39 +00:00
|
|
|
} else if (options != null &&
|
|
|
|
options.successRedirect != null &&
|
2016-09-21 23:09:23 +00:00
|
|
|
options.successRedirect.isNotEmpty) {
|
|
|
|
return res.redirect(options.successRedirect, code: HttpStatus.OK);
|
|
|
|
}
|
|
|
|
|
2016-09-21 06:19:52 +00:00
|
|
|
return true;
|
|
|
|
} else {
|
2016-09-21 23:09:23 +00:00
|
|
|
await authenticationFailure(req, res);
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
Future authenticationFailure(RequestContext req, ResponseContext res) async {
|
|
|
|
throw new AngelHttpException.NotAuthenticated();
|
|
|
|
}
|
|
|
|
|
2016-09-21 06:19:52 +00:00
|
|
|
logout([AngelAuthOptions options]) {
|
|
|
|
return (RequestContext req, ResponseContext res) async {
|
|
|
|
for (AuthStrategy strategy in strategies) {
|
|
|
|
if (!(await strategy.canLogout(req, res))) {
|
|
|
|
if (options != null &&
|
|
|
|
options.failureRedirect != null &&
|
|
|
|
options.failureRedirect.isNotEmpty) {
|
|
|
|
return res.redirect(options.failureRedirect);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
req.cookies.removeWhere((cookie) => cookie.name == "token");
|
2016-09-21 06:19:52 +00:00
|
|
|
|
|
|
|
if (options != null &&
|
|
|
|
options.successRedirect != null &&
|
|
|
|
options.successRedirect.isNotEmpty) {
|
|
|
|
return res.redirect(options.successRedirect);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|