2016-09-21 23:09:23 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:io';
|
2021-05-09 11:16:15 +00:00
|
|
|
import 'dart:math';
|
2021-05-14 11:09:48 +00:00
|
|
|
import 'package:angel3_framework/angel3_framework.dart';
|
2016-09-21 23:09:23 +00:00
|
|
|
import 'package:crypto/crypto.dart';
|
2021-06-07 00:50:39 +00:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
import 'auth_token.dart';
|
2016-09-21 06:19:52 +00:00
|
|
|
import 'options.dart';
|
|
|
|
import 'strategy.dart';
|
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// Handles authentication within an Angel application.
|
2018-09-11 22:03:35 +00:00
|
|
|
class AngelAuth<User> {
|
2021-07-08 01:20:21 +00:00
|
|
|
final _log = Logger('AngelAuth');
|
|
|
|
|
2021-05-09 11:16:15 +00:00
|
|
|
late Hmac _hs256;
|
|
|
|
late int _jwtLifeSpan;
|
|
|
|
final StreamController<User> _onLogin = StreamController<User>(),
|
2019-04-19 07:50:04 +00:00
|
|
|
_onLogout = StreamController<User>();
|
2021-05-09 11:16:15 +00:00
|
|
|
final Random _random = Random.secure();
|
2021-03-20 23:51:20 +00:00
|
|
|
final RegExp _rgxBearer = RegExp(r'^Bearer');
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
|
2016-11-23 20:37:40 +00:00
|
|
|
final bool allowCookie;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// If `true` (default), then users can include a JWT in the query string as `token`.
|
2016-11-23 20:37:40 +00:00
|
|
|
final bool allowTokenInQuery;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
2018-06-27 16:36:31 +00:00
|
|
|
/// Whether emitted cookies should have the `secure` and `HttpOnly` flags,
|
|
|
|
/// as well as being restricted to a specific domain.
|
|
|
|
final bool secureCookies;
|
|
|
|
|
|
|
|
/// A domain to restrict emitted cookies to.
|
|
|
|
///
|
2018-06-27 18:10:56 +00:00
|
|
|
/// Only applies if [allowCookie] is `true`.
|
2021-03-20 23:51:20 +00:00
|
|
|
final String? cookieDomain;
|
2018-06-27 16:36:31 +00:00
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
/// A path to restrict emitted cookies to.
|
|
|
|
///
|
|
|
|
/// Only applies if [allowCookie] is `true`.
|
|
|
|
final String cookiePath;
|
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// If `true` (default), then JWT's will be considered invalid if used from a different IP than the first user's it was issued to.
|
|
|
|
///
|
|
|
|
/// This is a security provision. Even if a user's JWT is stolen, a remote attacker will not be able to impersonate anyone.
|
2018-06-27 16:36:31 +00:00
|
|
|
final bool enforceIp;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// The endpoint to mount [reviveJwt] at. If `null`, then no revival route is mounted. Default: `/auth/token`.
|
2016-09-22 09:26:38 +00:00
|
|
|
String reviveTokenEndpoint;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// A set of [AuthStrategy] instances used to authenticate users.
|
2018-09-11 22:03:35 +00:00
|
|
|
Map<String, AuthStrategy<User>> strategies = {};
|
2016-09-21 06:19:52 +00:00
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// Serializes a user into a unique identifier associated only with one identity.
|
2021-06-07 00:50:39 +00:00
|
|
|
FutureOr Function(User) serializer;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
|
2021-06-07 00:50:39 +00:00
|
|
|
FutureOr<User> Function(Object) deserializer;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// Fires the result of [deserializer] whenever a user signs in to the application.
|
2021-05-09 11:16:15 +00:00
|
|
|
Stream<User> get onLogin => _onLogin.stream;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application.
|
2021-05-09 11:16:15 +00:00
|
|
|
Stream<User> get onLogout => _onLogout.stream;
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
/// The [Hmac] being used to encode JWT's.
|
2021-05-09 11:16:15 +00:00
|
|
|
Hmac get hmac => _hs256;
|
2017-01-20 23:15:21 +00:00
|
|
|
|
2016-10-08 11:39:39 +00:00
|
|
|
String _randomString(
|
2019-01-05 23:54:48 +00:00
|
|
|
{int length = 32,
|
|
|
|
String validChars =
|
2021-05-09 11:16:15 +00:00
|
|
|
'ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'}) {
|
2016-09-21 23:09:23 +00:00
|
|
|
var chars = <int>[];
|
2021-05-09 11:16:15 +00:00
|
|
|
while (chars.length < length) {
|
|
|
|
chars.add(_random.nextInt(validChars.length));
|
|
|
|
}
|
2019-04-19 07:50:04 +00:00
|
|
|
return String.fromCharCodes(chars);
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2018-06-27 16:36:31 +00:00
|
|
|
/// `jwtLifeSpan` - should be in *milliseconds*.
|
2016-10-08 11:39:39 +00:00
|
|
|
AngelAuth(
|
2021-03-20 23:51:20 +00:00
|
|
|
{String? jwtKey,
|
2021-06-07 00:50:39 +00:00
|
|
|
required this.serializer,
|
|
|
|
required this.deserializer,
|
|
|
|
num jwtLifeSpan = -1,
|
2019-01-05 23:54:48 +00:00
|
|
|
this.allowCookie = true,
|
|
|
|
this.allowTokenInQuery = true,
|
|
|
|
this.enforceIp = true,
|
2018-06-27 16:36:31 +00:00
|
|
|
this.cookieDomain,
|
2019-01-05 23:54:48 +00:00
|
|
|
this.cookiePath = '/',
|
|
|
|
this.secureCookies = true,
|
2021-05-09 11:16:15 +00:00
|
|
|
this.reviveTokenEndpoint = '/auth/token'})
|
2016-10-08 11:39:39 +00:00
|
|
|
: super() {
|
2019-04-19 07:50:04 +00:00
|
|
|
_hs256 = Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
|
2021-06-07 00:50:39 +00:00
|
|
|
_jwtLifeSpan = jwtLifeSpan.toInt();
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2019-04-19 09:08:06 +00:00
|
|
|
/// Configures an Angel server to decode and validate JSON Web tokens on demand,
|
|
|
|
/// whenever an instance of [User] is injected.
|
|
|
|
Future<void> configureServer(Angel app) async {
|
2021-06-07 00:50:39 +00:00
|
|
|
/*
|
2021-03-20 23:51:20 +00:00
|
|
|
if (serializer == null) {
|
2019-04-19 07:50:04 +00:00
|
|
|
throw StateError(
|
2017-06-03 21:39:55 +00:00
|
|
|
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
|
|
|
if (deserializer == null) {
|
2019-04-19 07:50:04 +00:00
|
|
|
throw StateError(
|
2017-06-03 21:39:55 +00:00
|
|
|
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2021-06-07 00:50:39 +00:00
|
|
|
*/
|
2021-06-21 15:04:36 +00:00
|
|
|
if (app.container == null) {
|
2021-07-08 01:20:21 +00:00
|
|
|
_log.severe('Angel3 container is null');
|
2021-06-21 15:04:36 +00:00
|
|
|
throw StateError(
|
|
|
|
'Angel.container is null. All authentication will fail.');
|
|
|
|
}
|
2021-06-07 00:50:39 +00:00
|
|
|
var appContainer = app.container!;
|
2021-06-21 15:04:36 +00:00
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
appContainer.registerSingleton(this);
|
2021-03-20 23:51:20 +00:00
|
|
|
if (runtimeType != AngelAuth) {
|
2021-06-07 00:50:39 +00:00
|
|
|
appContainer.registerSingleton(this, as: AngelAuth);
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2016-09-22 09:26:38 +00:00
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
if (!appContainer.has<_AuthResult<User>>()) {
|
|
|
|
appContainer
|
2019-04-19 09:08:06 +00:00
|
|
|
.registerLazySingleton<Future<_AuthResult<User>>>((container) async {
|
2021-06-07 00:50:39 +00:00
|
|
|
var req = container.make<RequestContext>();
|
|
|
|
var res = container.make<ResponseContext>();
|
|
|
|
if (req == null || res == null) {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('RequestContext or responseContext is null');
|
2021-06-07 00:50:39 +00:00
|
|
|
throw AngelHttpException.forbidden();
|
|
|
|
}
|
|
|
|
|
2019-04-19 09:08:06 +00:00
|
|
|
var result = await _decodeJwt(req, res);
|
|
|
|
if (result != null) {
|
|
|
|
return result;
|
|
|
|
} else {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('JWT is null');
|
2019-04-19 09:08:06 +00:00
|
|
|
throw AngelHttpException.forbidden();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-06-21 15:04:36 +00:00
|
|
|
appContainer.registerLazySingleton<Future<User?>>((container) async {
|
2021-06-07 00:50:39 +00:00
|
|
|
var result = await container.makeAsync<_AuthResult<User>>();
|
2021-06-21 15:04:36 +00:00
|
|
|
return result?.user;
|
2019-04-19 09:08:06 +00:00
|
|
|
});
|
|
|
|
|
2021-06-21 15:04:36 +00:00
|
|
|
appContainer.registerLazySingleton<Future<AuthToken?>>((container) async {
|
2021-06-07 00:50:39 +00:00
|
|
|
var result = await container.makeAsync<_AuthResult<User>>();
|
2021-06-21 15:04:36 +00:00
|
|
|
return result?.token;
|
2019-04-19 09:08:06 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-07-08 01:20:21 +00:00
|
|
|
app.post(reviveTokenEndpoint, _reviveJwt);
|
2017-06-03 21:39:55 +00:00
|
|
|
|
2017-09-24 04:32:38 +00:00
|
|
|
app.shutdownHooks.add((_) {
|
2017-06-03 21:39:55 +00:00
|
|
|
_onLogin.close();
|
|
|
|
});
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
|
2018-08-26 23:11:37 +00:00
|
|
|
void _apply(
|
2021-06-21 15:04:36 +00:00
|
|
|
RequestContext req, ResponseContext res, AuthToken token, User user) {
|
|
|
|
if (req.container == null) {
|
2021-07-08 01:20:21 +00:00
|
|
|
_log.severe('RequestContext.container is null');
|
2021-06-21 15:04:36 +00:00
|
|
|
throw StateError(
|
|
|
|
'RequestContext.container is not set. All authentication will fail.');
|
|
|
|
}
|
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
var reqContainer = req.container!;
|
|
|
|
if (!reqContainer.has<User>()) {
|
|
|
|
reqContainer.registerSingleton<User>(user);
|
2019-04-20 00:08:05 +00:00
|
|
|
}
|
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
if (!reqContainer.has<AuthToken>()) {
|
|
|
|
reqContainer.registerSingleton<AuthToken>(token);
|
2018-09-11 22:03:35 +00:00
|
|
|
}
|
2018-06-27 16:59:40 +00:00
|
|
|
|
2021-05-09 11:16:15 +00:00
|
|
|
if (allowCookie) {
|
2021-06-21 15:04:36 +00:00
|
|
|
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
2018-06-27 16:59:40 +00:00
|
|
|
}
|
2017-04-25 03:02:50 +00:00
|
|
|
}
|
|
|
|
|
2019-04-19 09:08:06 +00:00
|
|
|
/// DEPRECATED: A middleware that decodes a JWT from a request, and injects a corresponding user.
|
|
|
|
///
|
|
|
|
/// Now that `package:angel_framework` supports asynchronous injections, this middleware
|
|
|
|
/// is no longer directly necessary. Instead, call [configureServer]. You can then use
|
|
|
|
/// `makeAsync<User>`, or Angel's injections directly:
|
|
|
|
///
|
|
|
|
/// ```dart
|
|
|
|
/// var auth = AngelAuth<User>(...);
|
|
|
|
/// await app.configure(auth.configureServer);
|
|
|
|
///
|
|
|
|
/// app.get('/hmm', (User user) async {
|
|
|
|
/// // `package:angel_auth` decodes the JWT on demand.
|
|
|
|
/// print(user.name);
|
|
|
|
/// });
|
|
|
|
///
|
|
|
|
/// @Expose('/my')
|
|
|
|
/// class MyController extends Controller {
|
|
|
|
/// @Expose('/hmm')
|
|
|
|
/// String getUsername(User user) => user.name
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
@deprecated
|
2021-07-08 01:20:21 +00:00
|
|
|
Future decodeJwt(RequestContext req, ResponseContext res) async {
|
2021-03-20 23:51:20 +00:00
|
|
|
if (req.method == 'POST' && req.path == reviveTokenEndpoint) {
|
2021-07-08 01:20:21 +00:00
|
|
|
return await _reviveJwt(req, res);
|
2019-04-19 09:08:06 +00:00
|
|
|
} else {
|
|
|
|
await _decodeJwt(req, res);
|
|
|
|
return true;
|
2016-11-23 20:37:40 +00:00
|
|
|
}
|
2019-04-19 09:08:06 +00:00
|
|
|
}
|
2016-10-08 11:39:39 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
Future<_AuthResult<User>?> _decodeJwt(
|
2021-05-09 11:16:15 +00:00
|
|
|
RequestContext req, ResponseContext res) async {
|
2021-03-20 23:51:20 +00:00
|
|
|
var jwt = getJwt(req);
|
2016-11-23 20:37:40 +00:00
|
|
|
|
2016-09-21 23:09:23 +00:00
|
|
|
if (jwt != null) {
|
2021-05-09 11:16:15 +00:00
|
|
|
var token = AuthToken.validate(jwt, _hs256);
|
2016-09-21 23:09:23 +00:00
|
|
|
|
|
|
|
if (enforceIp) {
|
2021-03-20 23:51:20 +00:00
|
|
|
if (req.ip != token.ipAddress) {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('JWT cannot be accessed from this IP address');
|
2019-04-19 07:50:04 +00:00
|
|
|
throw AngelHttpException.forbidden(
|
2021-05-09 11:16:15 +00:00
|
|
|
message: 'JWT cannot be accessed from this IP address.');
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2021-05-09 11:16:15 +00:00
|
|
|
if (token.lifeSpan > -1) {
|
2019-04-19 07:50:04 +00:00
|
|
|
var expiry =
|
2021-05-09 11:16:15 +00:00
|
|
|
token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt()));
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
if (!expiry.isAfter(DateTime.now())) {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('Expired JWT');
|
2021-05-09 11:16:15 +00:00
|
|
|
throw AngelHttpException.forbidden(message: 'Expired JWT.');
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2016-11-23 20:37:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
var user = await deserializer(token.userId as Object);
|
2018-06-27 16:59:40 +00:00
|
|
|
_apply(req, res, token, user);
|
2019-04-19 09:08:06 +00:00
|
|
|
return _AuthResult(user, token);
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
|
2019-04-19 09:08:06 +00:00
|
|
|
return null;
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// Retrieves a JWT from a request, if any was sent at all.
|
2021-03-20 23:51:20 +00:00
|
|
|
String? getJwt(RequestContext req) {
|
2021-05-09 11:16:15 +00:00
|
|
|
if (req.headers?.value('Authorization') != null) {
|
2021-06-21 15:04:36 +00:00
|
|
|
final authHeader = req.headers?.value('Authorization');
|
|
|
|
if (authHeader != null) {
|
|
|
|
// Allow Basic auth to fall through
|
|
|
|
if (_rgxBearer.hasMatch(authHeader)) {
|
|
|
|
return authHeader.replaceAll(_rgxBearer, '').trim();
|
|
|
|
}
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2021-06-21 15:04:36 +00:00
|
|
|
|
2021-07-08 01:20:21 +00:00
|
|
|
_log.info('RequestContext.headers is null');
|
2016-12-03 18:23:11 +00:00
|
|
|
} else if (allowCookie &&
|
2021-04-10 11:23:57 +00:00
|
|
|
req.cookies.any((cookie) => cookie.name == 'token')) {
|
|
|
|
return req.cookies.firstWhere((cookie) => cookie.name == 'token').value;
|
2021-06-21 15:04:36 +00:00
|
|
|
} else if (allowTokenInQuery) {
|
|
|
|
//&& req.uri?.queryParameters['token'] is String) {
|
|
|
|
if (req.uri != null) {
|
|
|
|
return req.uri?.queryParameters['token']?.toString();
|
|
|
|
}
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
void _addProtectedCookie(ResponseContext res, String name, String value) {
|
|
|
|
if (!res.cookies.any((c) => c.name == name)) {
|
2019-04-19 07:50:04 +00:00
|
|
|
res.cookies.add(protectCookie(Cookie(name, value)));
|
2018-06-27 18:10:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-27 16:36:31 +00:00
|
|
|
/// Applies security protections to a [cookie].
|
|
|
|
Cookie protectCookie(Cookie cookie) {
|
|
|
|
if (secureCookies != false) {
|
|
|
|
cookie.httpOnly = true;
|
|
|
|
cookie.secure = true;
|
|
|
|
}
|
|
|
|
|
2021-05-09 11:16:15 +00:00
|
|
|
var lifeSpan = _jwtLifeSpan;
|
|
|
|
if (lifeSpan > 0) {
|
|
|
|
cookie.maxAge ??= lifeSpan < 0 ? -1 : lifeSpan ~/ 1000;
|
|
|
|
cookie.expires ??= DateTime.now().add(Duration(milliseconds: lifeSpan));
|
2018-06-27 16:36:31 +00:00
|
|
|
}
|
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
cookie.domain ??= cookieDomain;
|
|
|
|
cookie.path ??= cookiePath;
|
2018-06-27 16:36:31 +00:00
|
|
|
return cookie;
|
|
|
|
}
|
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// Attempts to revive an expired (or still alive) JWT.
|
2021-07-08 01:20:21 +00:00
|
|
|
Future<Map<String, dynamic>> _reviveJwt(
|
2018-06-27 16:36:31 +00:00
|
|
|
RequestContext req, ResponseContext res) async {
|
2016-09-22 09:26:38 +00:00
|
|
|
try {
|
2016-11-23 20:37:40 +00:00
|
|
|
var jwt = getJwt(req);
|
2016-09-22 09:26:38 +00:00
|
|
|
|
2017-04-26 13:59:27 +00:00
|
|
|
if (jwt == null) {
|
2021-04-10 11:23:57 +00:00
|
|
|
var body = await req.parseBody().then((_) => req.bodyAsMap);
|
2018-06-27 16:36:31 +00:00
|
|
|
jwt = body['token']?.toString();
|
2017-04-26 13:59:27 +00:00
|
|
|
}
|
2021-07-08 01:20:21 +00:00
|
|
|
|
2016-09-22 09:26:38 +00:00
|
|
|
if (jwt == null) {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('No JWT provided');
|
2021-05-09 11:16:15 +00:00
|
|
|
throw AngelHttpException.forbidden(message: 'No JWT provided');
|
2016-09-22 09:26:38 +00:00
|
|
|
} else {
|
2021-05-09 11:16:15 +00:00
|
|
|
var token = AuthToken.validate(jwt, _hs256);
|
2016-09-22 09:26:38 +00:00
|
|
|
if (enforceIp) {
|
2021-03-20 23:51:20 +00:00
|
|
|
if (req.ip != token.ipAddress) {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('WT cannot be accessed from this IP address');
|
2019-04-19 07:50:04 +00:00
|
|
|
throw AngelHttpException.forbidden(
|
2021-05-09 11:16:15 +00:00
|
|
|
message: 'JWT cannot be accessed from this IP address.');
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
|
|
|
|
2021-05-09 11:16:15 +00:00
|
|
|
if (token.lifeSpan > -1) {
|
2018-06-27 17:43:46 +00:00
|
|
|
var expiry = token.issuedAt
|
2021-05-09 11:16:15 +00:00
|
|
|
.add(Duration(milliseconds: token.lifeSpan.toInt()));
|
2016-09-22 09:26:38 +00:00
|
|
|
|
2019-04-19 07:50:04 +00:00
|
|
|
if (!expiry.isAfter(DateTime.now())) {
|
2018-06-27 17:43:46 +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
|
2019-04-19 07:50:04 +00:00
|
|
|
token.issuedAt = DateTime.now();
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
2016-10-08 11:39:39 +00:00
|
|
|
}
|
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
if (allowCookie) {
|
2021-05-09 11:16:15 +00:00
|
|
|
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
2018-06-27 18:10:56 +00:00
|
|
|
}
|
2016-12-03 18:23:11 +00:00
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
final data = await deserializer(token.userId as Object);
|
2021-05-09 11:16:15 +00:00
|
|
|
return {'data': data, 'token': token.serialize(_hs256)};
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
2017-09-24 04:32:38 +00:00
|
|
|
} catch (e) {
|
2021-07-08 01:20:21 +00:00
|
|
|
if (e is AngelHttpException) {
|
|
|
|
rethrow;
|
|
|
|
}
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('Malformed JWT');
|
2021-05-09 11:16:15 +00:00
|
|
|
throw AngelHttpException.badRequest(message: 'Malformed JWT');
|
2016-09-22 09:26:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// Attempts to authenticate a user using one or more strategies.
|
|
|
|
///
|
2017-09-24 04:32:38 +00:00
|
|
|
/// [type] is a strategy name to try, or a `List` of such.
|
2017-06-03 21:39:55 +00:00
|
|
|
///
|
|
|
|
/// If a strategy returns `null` or `false`, either the next one is tried,
|
|
|
|
/// or a `401 Not Authenticated` is thrown, if it is the last one.
|
|
|
|
///
|
|
|
|
/// Any other result is considered an authenticated user, and terminates the loop.
|
2021-03-20 23:51:20 +00:00
|
|
|
RequestHandler authenticate(type, [AngelAuthOptions<User>? options]) {
|
2016-09-21 06:19:52 +00:00
|
|
|
return (RequestContext req, ResponseContext res) async {
|
2021-03-20 23:51:20 +00:00
|
|
|
var names = <String>[];
|
2019-04-19 07:50:04 +00:00
|
|
|
var arr = type is Iterable
|
|
|
|
? type.map((x) => x.toString()).toList()
|
|
|
|
: [type.toString()];
|
2017-09-24 04:32:38 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
for (var t in arr) {
|
2017-09-24 04:32:38 +00:00
|
|
|
var n = t
|
|
|
|
.split(',')
|
|
|
|
.map((s) => s.trim())
|
|
|
|
.where((String s) => s.isNotEmpty)
|
|
|
|
.toList();
|
|
|
|
names.addAll(n);
|
|
|
|
}
|
2017-06-03 21:39:55 +00:00
|
|
|
|
2021-03-20 23:51:20 +00:00
|
|
|
for (var i = 0; i < names.length; i++) {
|
2017-06-03 21:39:55 +00:00
|
|
|
var name = names[i];
|
|
|
|
|
2021-07-08 04:49:34 +00:00
|
|
|
var strategy = strategies[name];
|
|
|
|
if (strategy == null) {
|
|
|
|
_log.severe('No strategy "$name" found.');
|
|
|
|
throw ArgumentError('No strategy "$name" found.');
|
|
|
|
}
|
2018-06-27 17:17:44 +00:00
|
|
|
|
2021-06-07 00:50:39 +00:00
|
|
|
var reqContainer = req.container;
|
|
|
|
|
|
|
|
if (reqContainer == null) {
|
|
|
|
print('req.container is null');
|
|
|
|
}
|
|
|
|
|
|
|
|
var hasExisting = reqContainer?.has<User>() ?? false;
|
2018-06-27 17:17:44 +00:00
|
|
|
var result = hasExisting
|
2021-06-07 00:50:39 +00:00
|
|
|
? reqContainer?.make<User>()
|
|
|
|
: await strategy.authenticate(req, res, options);
|
|
|
|
|
|
|
|
if (result != null && result == true) {
|
2017-06-03 21:39:55 +00:00
|
|
|
return result;
|
2021-03-20 23:51:20 +00:00
|
|
|
} else if (result != false && result != null) {
|
2021-06-07 00:50:39 +00:00
|
|
|
var userId = await serializer(result);
|
2017-06-03 21:39:55 +00:00
|
|
|
|
|
|
|
// Create JWT
|
2019-04-19 07:50:04 +00:00
|
|
|
var token = AuthToken(
|
2017-06-03 21:39:55 +00:00
|
|
|
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
2021-05-09 11:16:15 +00:00
|
|
|
var jwt = token.serialize(_hs256);
|
2017-06-03 21:39:55 +00:00
|
|
|
|
2021-07-08 01:20:21 +00:00
|
|
|
if (options != null && options.tokenCallback != null) {
|
2021-06-07 00:50:39 +00:00
|
|
|
var hasUser = reqContainer?.has<User>() ?? false;
|
|
|
|
if (!hasUser) {
|
|
|
|
reqContainer?.registerSingleton<User>(result);
|
2018-09-11 22:03:35 +00:00
|
|
|
}
|
|
|
|
|
2021-07-08 01:20:21 +00:00
|
|
|
var r = await options.tokenCallback!(req, res, token, result);
|
2017-06-03 21:39:55 +00:00
|
|
|
if (r != null) return r;
|
2021-05-09 11:16:15 +00:00
|
|
|
jwt = token.serialize(_hs256);
|
2017-06-03 21:39:55 +00:00
|
|
|
}
|
2017-01-20 23:15:21 +00:00
|
|
|
|
2018-06-27 16:59:40 +00:00
|
|
|
_apply(req, res, token, result);
|
2016-11-23 20:37:40 +00:00
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
if (allowCookie) {
|
|
|
|
_addProtectedCookie(res, 'token', jwt);
|
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2021-07-08 01:20:21 +00:00
|
|
|
// Options is not null
|
|
|
|
if (options != null) {
|
|
|
|
if (options.callback != null) {
|
|
|
|
return await options.callback!(req, res, jwt);
|
|
|
|
}
|
2016-12-07 23:09:21 +00:00
|
|
|
|
2021-07-08 01:20:21 +00:00
|
|
|
if (options.successRedirect?.isNotEmpty == true) {
|
|
|
|
await res.redirect(options.successRedirect);
|
|
|
|
return false;
|
|
|
|
} else if (options.canRespondWithJson &&
|
|
|
|
req.accepts('application/json')) {
|
|
|
|
var user = hasExisting
|
|
|
|
? result
|
|
|
|
: await deserializer((await serializer(result)) as Object);
|
|
|
|
_onLogin.add(user);
|
|
|
|
return {'data': user, 'token': jwt};
|
|
|
|
}
|
|
|
|
// Options is null
|
|
|
|
} else if (hasExisting && req.accepts('application/json')) {
|
2018-06-27 17:17:44 +00:00
|
|
|
var user = hasExisting
|
2018-08-26 23:11:37 +00:00
|
|
|
? result
|
2021-06-07 00:50:39 +00:00
|
|
|
: await deserializer((await serializer(result)) as Object);
|
2017-06-03 21:39:55 +00:00
|
|
|
_onLogin.add(user);
|
2021-06-07 00:50:39 +00:00
|
|
|
return {'data': user, 'token': jwt};
|
2017-06-03 21:39:55 +00:00
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
if (i < names.length - 1) continue;
|
|
|
|
// Check if not redirect
|
|
|
|
if (res.statusCode == 301 ||
|
|
|
|
res.statusCode == 302 ||
|
2021-03-20 23:51:20 +00:00
|
|
|
res.headers.containsKey('location')) {
|
2017-06-03 21:39:55 +00:00
|
|
|
return false;
|
2021-07-08 01:20:21 +00:00
|
|
|
} else if (options != null && options.failureRedirect != null) {
|
|
|
|
await res.redirect(options.failureRedirect);
|
2018-11-09 18:01:37 +00:00
|
|
|
return false;
|
2021-03-20 23:51:20 +00:00
|
|
|
} else {
|
2021-07-08 04:49:34 +00:00
|
|
|
_log.warning('Not authenticated');
|
2019-04-19 07:50:04 +00:00
|
|
|
throw AngelHttpException.notAuthenticated();
|
2021-03-20 23:51:20 +00:00
|
|
|
}
|
2017-06-03 21:39:55 +00:00
|
|
|
}
|
2016-09-21 06:19:52 +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 {
|
2021-06-07 00:50:39 +00:00
|
|
|
var user = await deserializer(token.userId as Object);
|
2018-06-27 16:59:40 +00:00
|
|
|
_apply(req, res, token, user);
|
2017-06-03 21:39:55 +00:00
|
|
|
_onLogin.add(user);
|
2017-01-20 23:15:21 +00:00
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
if (allowCookie) {
|
2021-05-09 11:16:15 +00:00
|
|
|
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
2018-06-27 18:10:56 +00:00
|
|
|
}
|
2017-01-20 23:15:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Log a user in on-demand.
|
|
|
|
Future loginById(userId, RequestContext req, ResponseContext res) async {
|
2021-06-07 00:50:39 +00:00
|
|
|
var user = await deserializer(userId as Object);
|
2019-04-19 07:50:04 +00:00
|
|
|
var token =
|
|
|
|
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
2018-06-27 16:59:40 +00:00
|
|
|
_apply(req, res, token, user);
|
2017-06-03 21:39:55 +00:00
|
|
|
_onLogin.add(user);
|
2017-01-20 23:15:21 +00:00
|
|
|
|
2018-06-27 18:10:56 +00:00
|
|
|
if (allowCookie) {
|
2021-05-09 11:16:15 +00:00
|
|
|
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
2018-06-27 18:10:56 +00:00
|
|
|
}
|
2017-01-20 23:15:21 +00:00
|
|
|
}
|
|
|
|
|
2017-06-03 21:39:55 +00:00
|
|
|
/// Log an authenticated user out.
|
2021-03-20 23:51:20 +00:00
|
|
|
RequestHandler logout([AngelAuthOptions<User>? options]) {
|
2016-09-21 06:19:52 +00:00
|
|
|
return (RequestContext req, ResponseContext res) async {
|
2021-05-09 11:16:15 +00:00
|
|
|
if (req.container?.has<User>() == true) {
|
|
|
|
var user = req.container?.make<User>();
|
|
|
|
if (user != null) {
|
|
|
|
_onLogout.add(user);
|
|
|
|
}
|
2018-08-26 23:11:37 +00:00
|
|
|
}
|
2017-06-03 21:39:55 +00:00
|
|
|
|
2018-06-27 17:43:46 +00:00
|
|
|
if (allowCookie == true) {
|
2021-05-09 11:16:15 +00:00
|
|
|
res.cookies.removeWhere((cookie) => cookie.name == 'token');
|
2019-04-04 06:44:55 +00:00
|
|
|
_addProtectedCookie(res, 'token', '""');
|
2018-06-27 17:43:46 +00:00
|
|
|
}
|
2016-09-21 06:19:52 +00:00
|
|
|
|
|
|
|
if (options != null &&
|
|
|
|
options.successRedirect != null &&
|
2021-03-20 23:51:20 +00:00
|
|
|
options.successRedirect!.isNotEmpty) {
|
2019-04-19 07:50:04 +00:00
|
|
|
await res.redirect(options.successRedirect);
|
2016-09-21 06:19:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
}
|
2016-09-21 23:09:23 +00:00
|
|
|
}
|
2019-04-19 09:08:06 +00:00
|
|
|
|
|
|
|
class _AuthResult<User> {
|
|
|
|
final User user;
|
|
|
|
final AuthToken token;
|
|
|
|
|
|
|
|
_AuthResult(this.user, this.token);
|
|
|
|
}
|