platform/lib/src/plugin.dart

329 lines
9.7 KiB
Dart
Raw Normal View History

2016-09-21 23:09:23 +00:00
import 'dart:async';
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-11-23 20:37:40 +00:00
final bool allowCookie;
final bool allowTokenInQuery;
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;
2017-01-20 23:15:21 +00:00
Hmac get hmac => _hs256;
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,
2016-11-23 20:37:40 +00:00
this.allowCookie: true,
this.allowTokenInQuery: true,
2016-10-08 11:39:39 +00:00
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-11-23 20:37:40 +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) {
2016-11-23 20:37:40 +00:00
app.post(reviveTokenEndpoint, reviveJwt);
2016-09-22 09:26:38 +00:00
}
2016-09-21 06:19:52 +00:00
}
2016-11-23 20:37:40 +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-11-23 20:37:40 +00:00
if (debug) print('Token revival endpoint accessed.');
return await reviveJwt(req, res);
}
2016-10-08 11:39:39 +00:00
2016-11-23 20:37:40 +00:00
if (debug) {
print('Enforcing JWT authentication...');
2016-09-21 23:09:23 +00:00
}
2016-11-23 20:37:40 +00:00
String jwt = getJwt(req);
if (debug) {
print('Found JWT: $jwt');
}
2016-09-22 09:26:38 +00:00
2016-09-21 23:09:23 +00:00
if (jwt != null) {
var token = new AuthToken.validate(jwt, _hs256);
2016-11-23 20:37:40 +00:00
if (debug) {
print('Decoded auth token: ${token.toJson()}');
}
2016-09-21 23:09:23 +00:00
if (enforceIp) {
2016-11-23 20:37:40 +00:00
if (debug) {
2016-12-03 18:23:11 +00:00
print('Token IP: ${token.ipAddress}. Current request sent from: ${req
.ip}');
2016-11-23 20:37:40 +00:00
}
if (req.ip != null && req.ip != token.ipAddress)
2017-01-13 15:50:38 +00:00
throw new AngelHttpException.forbidden(
2016-09-21 23:09:23 +00:00
message: "JWT cannot be accessed from this IP address.");
}
if (token.lifeSpan > -1) {
2016-11-23 20:37:40 +00:00
if (debug) {
print("Making sure this token hasn't already expired...");
}
2016-09-21 23:09:23 +00:00
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
if (!token.issuedAt.isAfter(new DateTime.now()))
2017-01-13 15:50:38 +00:00
throw new AngelHttpException.forbidden(message: "Expired JWT.");
2016-11-23 20:37:40 +00:00
} else if (debug) {
print('This token has an infinite life span.');
2016-09-21 23:09:23 +00:00
}
2016-11-23 20:37:40 +00:00
if (debug) {
print('Now deserializing from this userId: ${token.userId}');
}
2016-12-13 16:39:20 +00:00
final user = await deserializer(token.userId);
req
..inject(AuthToken, req.properties['token'] = token)
..inject(user.runtimeType, req.properties["user"] = user);
2016-09-21 06:19:52 +00:00
}
return true;
}
2016-11-23 20:37:40 +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');
}
2016-11-23 20:37:40 +00:00
final authHeader = req.headers.value("Authorization");
// Allow Basic auth to fall through
if (_rgxBearer.hasMatch(authHeader))
return authHeader.replaceAll(_rgxBearer, "").trim();
2016-12-03 18:23:11 +00:00
} else if (allowCookie &&
req.cookies.any((cookie) => cookie.name == "token")) {
if (debug) print('Request has "token" cookie...');
2016-09-22 09:26:38 +00:00
return req.cookies.firstWhere((cookie) => cookie.name == "token").value;
2016-11-23 20:37:40 +00:00
} else if (allowTokenInQuery && req.query['token'] is String) {
return req.query['token'];
2016-09-22 09:26:38 +00:00
}
return null;
}
2016-11-23 20:37:40 +00:00
reviveJwt(RequestContext req, ResponseContext res) async {
2016-09-22 09:26:38 +00:00
try {
2016-11-23 20:37:40 +00:00
if (debug) print('Attempting to revive JWT...');
2016-10-08 11:39:39 +00:00
2016-11-23 20:37:40 +00:00
var jwt = getJwt(req);
2016-09-22 09:26:38 +00:00
2016-11-23 20:37:40 +00:00
if (debug) print('Found JWT: $jwt');
2016-10-08 11:39:39 +00:00
2016-09-22 09:26:38 +00:00
if (jwt == null) {
2017-01-13 15:50:38 +00:00
throw new AngelHttpException.forbidden(message: "No JWT provided");
2016-09-22 09:26:38 +00:00
} else {
var token = new AuthToken.validate(jwt, _hs256);
2016-11-23 20:37:40 +00:00
if (debug) print('Validated and deserialized: $token');
2016-10-08 11:39:39 +00:00
2016-09-22 09:26:38 +00:00
if (enforceIp) {
2016-10-08 11:39:39 +00:00
if (debug)
2016-11-23 20:37:40 +00:00
print(
2016-12-03 18:23:11 +00:00
'Token IP: ${token.ipAddress}. Current request sent from: ${req
.ip}');
2016-10-08 11:39:39 +00:00
2016-09-22 09:26:38 +00:00
if (req.ip != token.ipAddress)
2017-01-13 15:50:38 +00:00
throw new AngelHttpException.forbidden(
2016-09-22 09:26:38 +00:00
message: "JWT cannot be accessed from this IP address.");
}
if (token.lifeSpan > -1) {
2016-10-08 11:39:39 +00:00
if (debug) {
2016-12-03 18:23:11 +00:00
print('Checking if token has expired... Life span is ${token
.lifeSpan}');
2016-10-08 11:39:39 +00:00
}
2016-09-22 09:26:38 +00:00
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
if (!token.issuedAt.isAfter(new DateTime.now())) {
2016-11-23 20:37:40 +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-11-23 20:37:40 +00:00
} else if (debug) {
2016-10-08 11:39:39 +00:00
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()}');
}
2016-12-03 18:23:11 +00:00
if (allowCookie)
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
final data = await deserializer(token.userId);
return {'data': data, 'token': token.serialize(_hs256)};
2016-09-22 09:26:38 +00:00
}
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;
2017-01-13 15:50:38 +00:00
throw new AngelHttpException.badRequest(message: "Malformed JWT");
2016-09-22 09:26:38 +00:00
}
}
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
2016-12-21 18:28:51 +00:00
var token = new AuthToken(
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
var jwt = token.serialize(_hs256);
2017-01-20 23:15:21 +00:00
if (options?.tokenCallback != null) {
var r = await options.tokenCallback(
req, res, token, req.properties["user"] = result);
if (r != null) return r;
}
2016-12-21 18:28:51 +00:00
req
..inject(AuthToken, req.properties['token'] = token)
..inject(result.runtimeType, req.properties["user"] = result);
2016-11-23 20:37:40 +00:00
2016-12-22 17:24:01 +00:00
if (allowCookie) res.cookies.add(new Cookie("token", jwt));
2016-09-21 23:09:23 +00:00
2016-12-07 23:09:21 +00:00
if (options?.callback != null) {
return await options.callback(req, res, jwt);
}
2016-12-22 17:24:01 +00:00
if (options?.successRedirect?.isNotEmpty == true) {
return res.redirect(options.successRedirect, code: HttpStatus.OK);
} else if (options?.canRespondWithJson != false &&
2016-11-28 01:04:52 +00:00
req.headers.value("accept") != null &&
2016-09-21 23:09:23 +00:00
(req.headers.value("accept").contains("application/json") ||
req.headers.value("accept").contains("*/*") ||
req.headers.value("accept").contains("application/*"))) {
return {"data": result, "token": jwt};
}
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 {
2017-01-13 15:50:38 +00:00
throw new AngelHttpException.notAuthenticated();
2016-09-21 23:09:23 +00:00
}
2017-01-20 23:15:21 +00:00
/// Log a user in on-demand.
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
var user = await deserializer(token.userId);
req
..inject(AuthToken, req.properties['token'] = token)
..inject(user.runtimeType, req.properties["user"] = user);
if (allowCookie)
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
}
/// Log a user in on-demand.
Future loginById(userId, RequestContext req, ResponseContext res) async {
var user = await deserializer(userId);
var token = new AuthToken(
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
req
..inject(AuthToken, req.properties['token'] = token)
..inject(user.runtimeType, req.properties["user"] = user);
if (allowCookie)
res.cookies.add(new Cookie('token', token.serialize(_hs256)));
}
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-12-22 17:24:01 +00:00
res.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
}