From 5302f635acd83caad128aab80f9549a32879012e Mon Sep 17 00:00:00 2001 From: thomashii Date: Sun, 21 Mar 2021 07:51:20 +0800 Subject: [PATCH] Migrated angel_auth to NNBD --- CHANGELOG.md | 2 +- packages/auth/example/example.dart | 8 +- packages/auth/lib/src/auth_token.dart | 61 +++--- packages/auth/lib/src/configuration.dart | 29 ++- .../auth/lib/src/middleware/require_auth.dart | 20 +- packages/auth/lib/src/options.dart | 14 +- packages/auth/lib/src/plugin.dart | 193 +++++++++--------- packages/auth/lib/src/strategies/local.dart | 60 +++--- packages/auth/lib/src/strategy.dart | 2 +- packages/auth/pubspec.yaml | 6 +- packages/auth/test/callback_test.dart | 68 +++--- packages/auth/test/local_test.dart | 26 +-- packages/auth/test/protect_cookie_test.dart | 4 +- 13 files changed, 257 insertions(+), 236 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a220dd91..c8f598be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ * Added merge_map and updated to 2.0.0 * Added mock_request and updated to 2.0.0 * Updated angel_framework to 4.0.0 (Revisit TODO) -* Updated angel_auth to 4.0.0 (todo) +* Updated angel_auth to 4.0.0 * Updated angel_configuration to 4.0.0 (todo) # 3.0.0 (Non NNBD) diff --git a/packages/auth/example/example.dart b/packages/auth/example/example.dart index 90e09f2b..87894ddb 100644 --- a/packages/auth/example/example.dart +++ b/packages/auth/example/example.dart @@ -3,11 +3,11 @@ import 'package:angel_auth/angel_auth.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; -main() async { +void main() async { var app = Angel(); - var auth = AngelAuth(); + var auth = AngelAuth(); - auth.serializer = (user) => user.id; + auth.serializer = (user) => user!.id; auth.deserializer = (id) => fetchAUserByIdSomehow(id); @@ -30,7 +30,7 @@ main() async { } class User { - String id, username, password; + String? id, username, password; } Future fetchAUserByIdSomehow(id) async { diff --git a/packages/auth/lib/src/auth_token.dart b/packages/auth/lib/src/auth_token.dart index 338303ef..053b22b5 100644 --- a/packages/auth/lib/src/auth_token.dart +++ b/packages/auth/lib/src/auth_token.dart @@ -26,11 +26,11 @@ String decodeBase64(String str) { class AuthToken { final SplayTreeMap _header = - SplayTreeMap.from({"alg": "HS256", "typ": "JWT"}); + SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'}); - String ipAddress; - DateTime issuedAt; - num lifeSpan; + String? ipAddress; + late DateTime issuedAt; + num? lifeSpan; var userId; Map payload = {}; @@ -38,12 +38,15 @@ class AuthToken { {this.ipAddress, this.lifeSpan = -1, this.userId, - DateTime issuedAt, + DateTime? issuedAt, Map payload = const {}}) { this.issuedAt = issuedAt ?? DateTime.now(); - this.payload.addAll( - payload?.keys?.fold({}, (out, k) => out..[k.toString()] = payload[k]) ?? - {}); + this.payload.addAll(payload?.keys?.fold( + {}, + ((out, k) => out..[k.toString()] = payload[k]) + as Map? Function( + Map?, dynamic)) ?? + {}); } factory AuthToken.fromJson(String jsons) => @@ -51,37 +54,40 @@ class AuthToken { factory AuthToken.fromMap(Map data) { return AuthToken( - ipAddress: data["aud"].toString(), - lifeSpan: data["exp"] as num, - issuedAt: DateTime.parse(data["iat"].toString()), - userId: data["sub"], - payload: data["pld"] as Map ?? {}); + ipAddress: data['aud'].toString(), + lifeSpan: data['exp'] as num?, + issuedAt: DateTime.parse(data['iat'].toString()), + userId: data['sub'], + payload: data['pld'] as Map? ?? {}); } factory AuthToken.parse(String jwt) { - var split = jwt.split("."); + var split = jwt.split('.'); - if (split.length != 3) - throw AngelHttpException.notAuthenticated(message: "Invalid JWT."); + if (split.length != 3) { + throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.'); + } var payloadString = decodeBase64(split[1]); return AuthToken.fromMap(json.decode(payloadString) as Map); } factory AuthToken.validate(String jwt, Hmac hmac) { - var split = jwt.split("."); + var split = jwt.split('.'); - if (split.length != 3) - throw AngelHttpException.notAuthenticated(message: "Invalid JWT."); + if (split.length != 3) { + throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.'); + } // var headerString = decodeBase64(split[0]); var payloadString = decodeBase64(split[1]); - var data = split[0] + "." + split[1]; + var data = split[0] + '.' + split[1]; var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes); - if (signature != split[2]) + if (signature != split[2]) { throw AngelHttpException.notAuthenticated( - message: "JWT payload does not match hashed version."); + message: 'JWT payload does not match hashed version.'); + } return AuthToken.fromMap(json.decode(payloadString) as Map); } @@ -89,9 +95,9 @@ class AuthToken { String serialize(Hmac hmac) { var headerString = base64Url.encode(json.encode(_header).codeUnits); var payloadString = base64Url.encode(json.encode(toJson()).codeUnits); - var data = headerString + "." + payloadString; + var data = headerString + '.' + payloadString; var signature = hmac.convert(data.codeUnits).bytes; - return data + "." + base64Url.encode(signature); + return data + '.' + base64Url.encode(signature); } Map toJson() { @@ -114,11 +120,12 @@ SplayTreeMap _splayify(Map map) { return SplayTreeMap.from(data); } -_splay(value) { +dynamic _splay(value) { if (value is Iterable) { return value.map(_splay).toList(); - } else if (value is Map) + } else if (value is Map) { return _splayify(value); - else + } else { return value; + } } diff --git a/packages/auth/lib/src/configuration.dart b/packages/auth/lib/src/configuration.dart index 65cebb94..d2529440 100644 --- a/packages/auth/lib/src/configuration.dart +++ b/packages/auth/lib/src/configuration.dart @@ -1,15 +1,14 @@ import 'package:charcode/ascii.dart'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; -import 'package:quiver_hashcode/hashcode.dart'; +import 'package:quiver/core.dart'; /// A common class containing parsing and validation logic for third-party authentication configuration. class ExternalAuthOptions { /// The user's identifier, otherwise known as an "application id". - final String clientId; + final String? clientId; /// The user's secret, other known as an "application secret". - final String clientSecret; + final String? clientSecret; /// The user's redirect URI. final Uri redirectUri; @@ -27,9 +26,9 @@ class ExternalAuthOptions { } factory ExternalAuthOptions( - {@required String clientId, - @required String clientSecret, - @required redirectUri, + {required String? clientId, + required String? clientSecret, + required redirectUri, Iterable scopes = const []}) { if (redirectUri is String) { return ExternalAuthOptions._( @@ -51,8 +50,8 @@ class ExternalAuthOptions { /// * `redirect_uri` factory ExternalAuthOptions.fromMap(Map map) { return ExternalAuthOptions( - clientId: map['client_id'] as String, - clientSecret: map['client_secret'] as String, + clientId: map['client_id'] as String?, + clientSecret: map['client_secret'] as String?, redirectUri: map['redirect_uri'], scopes: map['scopes'] is Iterable ? ((map['scopes'] as Iterable).map((x) => x.toString())) @@ -73,10 +72,10 @@ class ExternalAuthOptions { /// Creates a copy of this object, with the specified changes. ExternalAuthOptions copyWith( - {String clientId, - String clientSecret, + {String? clientId, + String? clientSecret, redirectUri, - Iterable scopes}) { + Iterable? scopes}) { return ExternalAuthOptions( clientId: clientId ?? this.clientId, clientSecret: clientSecret ?? this.clientSecret, @@ -111,14 +110,14 @@ class ExternalAuthOptions { /// If no [asteriskCount] is given, then the number of asterisks will equal the length of /// the actual [clientSecret]. @override - String toString({bool obscureSecret = true, int asteriskCount}) { - String secret; + String toString({bool obscureSecret = true, int? asteriskCount}) { + String? secret; if (!obscureSecret) { secret = clientSecret; } else { var codeUnits = - List.filled(asteriskCount ?? clientSecret.length, $asterisk); + List.filled(asteriskCount ?? clientSecret!.length, $asterisk); secret = String.fromCharCodes(codeUnits); } diff --git a/packages/auth/lib/src/middleware/require_auth.dart b/packages/auth/lib/src/middleware/require_auth.dart index 087ddb27..35b7c164 100644 --- a/packages/auth/lib/src/middleware/require_auth.dart +++ b/packages/auth/lib/src/middleware/require_auth.dart @@ -4,12 +4,12 @@ import 'package:angel_framework/angel_framework.dart'; /// Forces Basic authentication over the requested resource, with the given [realm] name, if no JWT is present. /// /// [realm] defaults to `'angel_auth'`. -RequestHandler forceBasicAuth({String realm}) { +RequestHandler forceBasicAuth({String? realm}) { return (RequestContext req, ResponseContext res) async { - if (req.container.has()) + if (req.container!.has()) { return true; - else if (req.container.has>()) { - await req.container.makeAsync(); + } else if (req.container!.has>()) { + await req.container!.makeAsync(); return true; } @@ -26,16 +26,18 @@ RequestHandler requireAuthentication() { if (throwError) { res.statusCode = 403; throw AngelHttpException.forbidden(); - } else + } else { return false; + } } - if (req.container.has() || req.method == 'OPTIONS') + if (req.container!.has() || req.method == 'OPTIONS') { return true; - else if (req.container.has>()) { - await req.container.makeAsync(); + } else if (req.container!.has>()) { + await req.container!.makeAsync(); return true; - } else + } else { return _reject(res); + } }; } diff --git a/packages/auth/lib/src/options.dart b/packages/auth/lib/src/options.dart index 6b9e9323..1e177f47 100644 --- a/packages/auth/lib/src/options.dart +++ b/packages/auth/lib/src/options.dart @@ -3,17 +3,17 @@ import 'dart:async'; import 'package:angel_framework/angel_framework.dart'; import 'auth_token.dart'; -typedef FutureOr AngelAuthCallback( +typedef AngelAuthCallback = FutureOr Function( RequestContext req, ResponseContext res, String token); -typedef FutureOr AngelAuthTokenCallback( +typedef AngelAuthTokenCallback = FutureOr Function( RequestContext req, ResponseContext res, AuthToken token, User user); class AngelAuthOptions { - AngelAuthCallback callback; - AngelAuthTokenCallback tokenCallback; - String successRedirect; - String failureRedirect; + AngelAuthCallback? callback; + AngelAuthTokenCallback? tokenCallback; + String? successRedirect; + String? failureRedirect; /// If `false` (default: `true`), then successful authentication will return `true` and allow the /// execution of subsequent handlers, just like any other middleware. @@ -26,5 +26,5 @@ class AngelAuthOptions { this.tokenCallback, this.canRespondWithJson = true, this.successRedirect, - String this.failureRedirect}); + this.failureRedirect}); } diff --git a/packages/auth/lib/src/plugin.dart b/packages/auth/lib/src/plugin.dart index 71b4b975..57878eca 100644 --- a/packages/auth/lib/src/plugin.dart +++ b/packages/auth/lib/src/plugin.dart @@ -9,12 +9,12 @@ import 'strategy.dart'; /// Handles authentication within an Angel application. class AngelAuth { - Hmac _hs256; - int _jwtLifeSpan; - final StreamController _onLogin = StreamController(), + Hmac? _hs256; + int? _jwtLifeSpan; + final StreamController _onLogin = StreamController(), _onLogout = StreamController(); - Math.Random _random = Math.Random.secure(); - final RegExp _rgxBearer = RegExp(r"^Bearer"); + final Math.Random _random = Math.Random.secure(); + final RegExp _rgxBearer = RegExp(r'^Bearer'); /// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie. final bool allowCookie; @@ -29,7 +29,7 @@ class AngelAuth { /// A domain to restrict emitted cookies to. /// /// Only applies if [allowCookie] is `true`. - final String cookieDomain; + final String? cookieDomain; /// A path to restrict emitted cookies to. /// @@ -48,19 +48,19 @@ class AngelAuth { Map> strategies = {}; /// Serializes a user into a unique identifier associated only with one identity. - FutureOr Function(User) serializer; + FutureOr Function(User)? serializer; /// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance. - FutureOr Function(Object) deserializer; + FutureOr Function(Object?)? deserializer; /// Fires the result of [deserializer] whenever a user signs in to the application. - Stream get onLogin => _onLogin.stream; + Stream get onLogin => _onLogin.stream; /// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application. - Stream get onLogout => _onLogout.stream; + Stream get onLogout => _onLogout.stream; /// The [Hmac] being used to encode JWT's. - Hmac get hmac => _hs256; + Hmac? get hmac => _hs256; String _randomString( {int length = 32, @@ -73,10 +73,10 @@ class AngelAuth { /// `jwtLifeSpan` - should be in *milliseconds*. AngelAuth( - {String jwtKey, + {String? jwtKey, this.serializer, this.deserializer, - num jwtLifeSpan, + num? jwtLifeSpan, this.allowCookie = true, this.allowTokenInQuery = true, this.enforceIp = true, @@ -92,21 +92,24 @@ class AngelAuth { /// Configures an Angel server to decode and validate JSON Web tokens on demand, /// whenever an instance of [User] is injected. Future configureServer(Angel app) async { - if (serializer == null) + if (serializer == null) { throw StateError( 'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.'); - if (deserializer == null) + } + if (deserializer == null) { throw StateError( 'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.'); + } - app.container.registerSingleton(this); - if (runtimeType != AngelAuth) - app.container.registerSingleton(this, as: AngelAuth); + app.container!.registerSingleton(this); + if (runtimeType != AngelAuth) { + app.container!.registerSingleton(this, as: AngelAuth); + } - if (!app.container.has<_AuthResult>()) { - app.container + if (!app.container!.has<_AuthResult>()) { + app.container! .registerLazySingleton>>((container) async { - var req = container.make(); + var req = container.make()!; var res = container.make(); var result = await _decodeJwt(req, res); if (result != null) { @@ -116,20 +119,19 @@ class AngelAuth { } }); - app.container.registerLazySingleton>((container) async { - var result = await container.makeAsync<_AuthResult>(); + app.container!.registerLazySingleton>((container) async { + var result = await container.makeAsync<_AuthResult>()!; return result.user; }); - app.container.registerLazySingleton>((container) async { - var result = await container.makeAsync<_AuthResult>(); + app.container! + .registerLazySingleton>((container) async { + var result = await container.makeAsync<_AuthResult>()!; return result.token; }); } - if (reviveTokenEndpoint != null) { - app.post(reviveTokenEndpoint, reviveJwt); - } + app.post(reviveTokenEndpoint, reviveJwt); app.shutdownHooks.add((_) { _onLogin.close(); @@ -137,17 +139,17 @@ class AngelAuth { } void _apply( - RequestContext req, ResponseContext res, AuthToken token, User user) { - if (!req.container.has()) { - req.container.registerSingleton(user); + RequestContext req, ResponseContext? res, AuthToken token, User user) { + if (!req.container!.has()) { + req.container!.registerSingleton(user); } - if (!req.container.has()) { - req.container.registerSingleton(token); + if (!req.container!.has()) { + req.container!.registerSingleton(token); } if (allowCookie == true) { - _addProtectedCookie(res, 'token', token.serialize(_hs256)); + _addProtectedCookie(res!, 'token', token.serialize(_hs256!)); } } @@ -174,7 +176,7 @@ class AngelAuth { /// ``` @deprecated Future decodeJwt(RequestContext req, ResponseContext res) async { - if (req.method == "POST" && req.path == reviveTokenEndpoint) { + if (req.method == 'POST' && req.path == reviveTokenEndpoint) { return await reviveJwt(req, res); } else { await _decodeJwt(req, res); @@ -182,28 +184,30 @@ class AngelAuth { } } - Future<_AuthResult> _decodeJwt( - RequestContext req, ResponseContext res) async { - String jwt = getJwt(req); + Future<_AuthResult?> _decodeJwt( + RequestContext req, ResponseContext? res) async { + var jwt = getJwt(req); if (jwt != null) { - var token = AuthToken.validate(jwt, _hs256); + var token = AuthToken.validate(jwt, _hs256!); if (enforceIp) { - if (req.ip != null && req.ip != token.ipAddress) + if (req.ip != token.ipAddress) { throw AngelHttpException.forbidden( message: "JWT cannot be accessed from this IP address."); + } } - if (token.lifeSpan > -1) { + if (token.lifeSpan! > -1) { var expiry = - token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt())); + token.issuedAt.add(Duration(milliseconds: token.lifeSpan!.toInt())); - if (!expiry.isAfter(DateTime.now())) + if (!expiry.isAfter(DateTime.now())) { throw AngelHttpException.forbidden(message: "Expired JWT."); + } } - var user = await deserializer(token.userId); + var user = await deserializer!(token.userId); _apply(req, res, token, user); return _AuthResult(user, token); } @@ -212,19 +216,20 @@ class AngelAuth { } /// Retrieves a JWT from a request, if any was sent at all. - String getJwt(RequestContext req) { - if (req.headers.value("Authorization") != null) { - final authHeader = req.headers.value("Authorization"); + String? getJwt(RequestContext req) { + if (req.headers!.value("Authorization") != null) { + final authHeader = req.headers!.value("Authorization")!; // Allow Basic auth to fall through - if (_rgxBearer.hasMatch(authHeader)) + if (_rgxBearer.hasMatch(authHeader)) { return authHeader.replaceAll(_rgxBearer, "").trim(); + } } else if (allowCookie && - req.cookies.any((cookie) => cookie.name == "token")) { - return req.cookies.firstWhere((cookie) => cookie.name == "token").value; + req.cookies!.any((cookie) => cookie.name == "token")) { + return req.cookies!.firstWhere((cookie) => cookie.name == "token").value; } else if (allowTokenInQuery && - req.uri.queryParameters['token'] is String) { - return req.uri.queryParameters['token']?.toString(); + req.uri!.queryParameters['token'] is String) { + return req.uri!.queryParameters['token']?.toString(); } return null; @@ -243,10 +248,10 @@ class AngelAuth { cookie.secure = true; } - if (_jwtLifeSpan > 0) { - cookie.maxAge ??= _jwtLifeSpan < 0 ? -1 : _jwtLifeSpan ~/ 1000; + if (_jwtLifeSpan! > 0) { + cookie.maxAge ??= _jwtLifeSpan! < 0 ? -1 : _jwtLifeSpan! ~/ 1000; cookie.expires ??= - DateTime.now().add(Duration(milliseconds: _jwtLifeSpan)); + DateTime.now().add(Duration(milliseconds: _jwtLifeSpan!)); } cookie.domain ??= cookieDomain; @@ -261,22 +266,23 @@ class AngelAuth { var jwt = getJwt(req); if (jwt == null) { - var body = await req.parseBody().then((_) => req.bodyAsMap); + var body = await req.parseBody().then((_) => req.bodyAsMap!); jwt = body['token']?.toString(); } if (jwt == null) { throw AngelHttpException.forbidden(message: "No JWT provided"); } else { - var token = AuthToken.validate(jwt, _hs256); + var token = AuthToken.validate(jwt, _hs256!); if (enforceIp) { - if (req.ip != token.ipAddress) + if (req.ip != token.ipAddress) { throw AngelHttpException.forbidden( message: "JWT cannot be accessed from this IP address."); + } } - if (token.lifeSpan > -1) { + if (token.lifeSpan! > -1) { var expiry = token.issuedAt - .add(Duration(milliseconds: token.lifeSpan.toInt())); + .add(Duration(milliseconds: token.lifeSpan!.toInt())); if (!expiry.isAfter(DateTime.now())) { //print( @@ -287,11 +293,11 @@ class AngelAuth { } if (allowCookie) { - _addProtectedCookie(res, 'token', token.serialize(_hs256)); + _addProtectedCookie(res, 'token', token.serialize(_hs256!)); } - final data = await deserializer(token.userId); - return {'data': data, 'token': token.serialize(_hs256)}; + final data = await deserializer!(token.userId); + return {'data': data, 'token': token.serialize(_hs256!)}; } } catch (e) { if (e is AngelHttpException) rethrow; @@ -307,14 +313,14 @@ class AngelAuth { /// 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. - RequestHandler authenticate(type, [AngelAuthOptions options]) { + RequestHandler authenticate(type, [AngelAuthOptions? options]) { return (RequestContext req, ResponseContext res) async { - List names = []; + var names = []; var arr = type is Iterable ? type.map((x) => x.toString()).toList() : [type.toString()]; - for (String t in arr) { + for (var t in arr) { var n = t .split(',') .map((s) => s.trim()) @@ -323,34 +329,34 @@ class AngelAuth { names.addAll(n); } - for (int i = 0; i < names.length; i++) { + for (var i = 0; i < names.length; i++) { var name = names[i]; var strategy = strategies[name] ??= throw ArgumentError('No strategy "$name" found.'); - var hasExisting = req.container.has(); + var hasExisting = req.container!.has(); var result = hasExisting - ? req.container.make() - : await strategy.authenticate(req, res, options); - if (result == true) + ? req.container!.make() + : await strategy.authenticate(req, res, options!); + if (result == true) { return result; - else if (result != false && result != null) { - var userId = await serializer(result); + } else if (result != false && result != null) { + var userId = await serializer!(result); // Create JWT var token = AuthToken( userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip); - var jwt = token.serialize(_hs256); + var jwt = token.serialize(_hs256!); if (options?.tokenCallback != null) { - if (!req.container.has()) { - req.container.registerSingleton(result); + if (!req.container!.has()) { + req.container!.registerSingleton(result); } - var r = await options.tokenCallback(req, res, token, result); + var r = await options!.tokenCallback!(req, res, token, result); if (r != null) return r; - jwt = token.serialize(_hs256); + jwt = token.serialize(_hs256!); } _apply(req, res, token, result); @@ -360,17 +366,17 @@ class AngelAuth { } if (options?.callback != null) { - return await options.callback(req, res, jwt); + return await options!.callback!(req, res, jwt); } if (options?.successRedirect?.isNotEmpty == true) { - await res.redirect(options.successRedirect); + await res.redirect(options!.successRedirect); return false; } else if (options?.canRespondWithJson != false && req.accepts('application/json')) { var user = hasExisting ? result - : await deserializer(await serializer(result)); + : await deserializer!(await serializer!(result)); _onLogin.add(user); return {"data": user, "token": jwt}; } @@ -381,13 +387,14 @@ class AngelAuth { // Check if not redirect if (res.statusCode == 301 || res.statusCode == 302 || - res.headers.containsKey('location')) + res.headers.containsKey('location')) { return false; - else if (options?.failureRedirect != null) { - await res.redirect(options.failureRedirect); + } else if (options?.failureRedirect != null) { + await res.redirect(options!.failureRedirect); return false; - } else + } else { throw AngelHttpException.notAuthenticated(); + } } } }; @@ -395,33 +402,33 @@ class AngelAuth { /// Log a user in on-demand. Future login(AuthToken token, RequestContext req, ResponseContext res) async { - var user = await deserializer(token.userId); + var user = await deserializer!(token.userId); _apply(req, res, token, user); _onLogin.add(user); if (allowCookie) { - _addProtectedCookie(res, 'token', token.serialize(_hs256)); + _addProtectedCookie(res, 'token', token.serialize(_hs256!)); } } /// Log a user in on-demand. Future loginById(userId, RequestContext req, ResponseContext res) async { - var user = await deserializer(userId); + var user = await deserializer!(userId); var token = AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip); _apply(req, res, token, user); _onLogin.add(user); if (allowCookie) { - _addProtectedCookie(res, 'token', token.serialize(_hs256)); + _addProtectedCookie(res, 'token', token.serialize(_hs256!)); } } /// Log an authenticated user out. - RequestHandler logout([AngelAuthOptions options]) { + RequestHandler logout([AngelAuthOptions? options]) { return (RequestContext req, ResponseContext res) async { - if (req.container.has()) { - var user = req.container.make(); + if (req.container!.has()) { + var user = req.container!.make(); _onLogout.add(user); } @@ -432,7 +439,7 @@ class AngelAuth { if (options != null && options.successRedirect != null && - options.successRedirect.isNotEmpty) { + options.successRedirect!.isNotEmpty) { await res.redirect(options.successRedirect); } diff --git a/packages/auth/lib/src/strategies/local.dart b/packages/auth/lib/src/strategies/local.dart index 21eeed2f..eb118512 100644 --- a/packages/auth/lib/src/strategies/local.dart +++ b/packages/auth/lib/src/strategies/local.dart @@ -4,15 +4,16 @@ import 'package:angel_framework/angel_framework.dart'; import '../options.dart'; import '../strategy.dart'; -bool _validateString(String str) => str != null && str.isNotEmpty; +bool _validateString(String? str) => str != null && str.isNotEmpty; /// Determines the validity of an incoming username and password. -typedef FutureOr LocalAuthVerifier( - String username, String password); +// typedef FutureOr LocalAuthVerifier(String? username, String? password); +typedef LocalAuthVerifier = FutureOr Function( + String? username, String? password); class LocalAuthStrategy extends AuthStrategy { - RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false); - RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$'); + final RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false); + final RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$'); LocalAuthVerifier verifier; String usernameField; @@ -23,33 +24,32 @@ class LocalAuthStrategy extends AuthStrategy { String realm; LocalAuthStrategy(this.verifier, - {String this.usernameField = 'username', - String this.passwordField = 'password', - String this.invalidMessage = - 'Please provide a valid username and password.', - bool this.allowBasic = true, - bool this.forceBasic = false, - String this.realm = 'Authentication is required.'}); + {this.usernameField = 'username', + this.passwordField = 'password', + this.invalidMessage = 'Please provide a valid username and password.', + this.allowBasic = true, + this.forceBasic = false, + this.realm = 'Authentication is required.'}); @override - Future authenticate(RequestContext req, ResponseContext res, - [AngelAuthOptions options_]) async { - AngelAuthOptions options = options_ ?? AngelAuthOptions(); - User verificationResult; + Future authenticate(RequestContext req, ResponseContext res, + [AngelAuthOptions? options_]) async { + var options = options_ ?? AngelAuthOptions(); + User? verificationResult; if (allowBasic) { - String authHeader = req.headers.value('authorization') ?? ""; + var authHeader = req.headers!.value('authorization') ?? ''; if (_rgxBasic.hasMatch(authHeader)) { - String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1); - String authString = - String.fromCharCodes(base64.decode(base64AuthString)); + var base64AuthString = _rgxBasic.firstMatch(authHeader)!.group(1)!; + var authString = String.fromCharCodes(base64.decode(base64AuthString)); if (_rgxUsrPass.hasMatch(authString)) { - Match usrPassMatch = _rgxUsrPass.firstMatch(authString); + Match usrPassMatch = _rgxUsrPass.firstMatch(authString)!; verificationResult = await verifier(usrPassMatch.group(1), usrPassMatch.group(2)); - } else + } else { throw AngelHttpException.badRequest(errors: [invalidMessage]); + } if (verificationResult == false || verificationResult == null) { res @@ -68,27 +68,29 @@ class LocalAuthStrategy extends AuthStrategy { .parseBody() .then((_) => req.bodyAsMap) .catchError((_) => {}); - if (_validateString(body[usernameField]?.toString()) && - _validateString(body[passwordField]?.toString())) { - verificationResult = await verifier( - body[usernameField]?.toString(), body[passwordField]?.toString()); + if (body != null) { + if (_validateString(body[usernameField]?.toString()) && + _validateString(body[passwordField]?.toString())) { + verificationResult = await verifier( + body[usernameField]?.toString(), body[passwordField]?.toString()); + } } } if (verificationResult == false || verificationResult == null) { if (options.failureRedirect != null && - options.failureRedirect.isNotEmpty) { + options.failureRedirect!.isNotEmpty) { await res.redirect(options.failureRedirect, code: 401); return null; } if (forceBasic) { res.headers['www-authenticate'] = 'Basic realm="$realm"'; - throw AngelHttpException.notAuthenticated(); + return null; } return null; - } else if (verificationResult != null && verificationResult != false) { + } else if (verificationResult != false) { return verificationResult; } else { throw AngelHttpException.notAuthenticated(); diff --git a/packages/auth/lib/src/strategy.dart b/packages/auth/lib/src/strategy.dart index 72073686..a7ec4c70 100644 --- a/packages/auth/lib/src/strategy.dart +++ b/packages/auth/lib/src/strategy.dart @@ -5,6 +5,6 @@ import 'options.dart'; /// A function that handles login and signup for an Angel application. abstract class AuthStrategy { /// Authenticates or rejects an incoming user. - FutureOr authenticate(RequestContext req, ResponseContext res, + FutureOr authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options]); } diff --git a/packages/auth/pubspec.yaml b/packages/auth/pubspec.yaml index fffbf774..1fdbbe8f 100644 --- a/packages/auth/pubspec.yaml +++ b/packages/auth/pubspec.yaml @@ -1,11 +1,11 @@ name: angel_auth description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more. -version: 3.0.0 +version: 4.0.0 author: Tobe O homepage: https://github.com/angel-dart/angel_auth publish_to: none environment: - sdk: ">=2.10.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: angel_framework: git: @@ -17,7 +17,7 @@ dependencies: crypto: ^3.0.0 http_parser: ^4.0.0 meta: ^1.3.0 - quiver_hashcode: ^3.0.0+1 + quiver: ^3.0.0 dev_dependencies: http: ^0.13.1 io: ^1.0.0 diff --git a/packages/auth/test/callback_test.dart b/packages/auth/test/callback_test.dart index 2506c12d..25b86d3e 100644 --- a/packages/auth/test/callback_test.dart +++ b/packages/auth/test/callback_test.dart @@ -3,20 +3,22 @@ import 'package:angel_auth/angel_auth.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; import 'dart:convert'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:http/http.dart' as http; import 'package:io/ansi.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; +import 'package:collection/collection.dart'; class User extends Model { - String username, password; + String? username, password; User({this.username, this.password}); static User parse(Map map) { return User( - username: map['username'] as String, - password: map['password'] as String, + username: map['username'] as String?, + password: map['password'] as String?, ); } @@ -31,27 +33,27 @@ class User extends Model { } } -main() { - Angel app; - AngelHttp angelHttp; - AngelAuth auth; - http.Client client; +void main() { + Angel? app; + late AngelHttp angelHttp; + AngelAuth auth; + http.Client? client; HttpServer server; - String url; + String? url; setUp(() async { hierarchicalLoggingEnabled = true; app = Angel(); angelHttp = AngelHttp(app); - app.use('/users', MapService()); + app!.use('/users', MapService()); - var oldErrorHandler = app.errorHandler; - app.errorHandler = (e, req, res) { - app.logger.severe(e.message, e, e.stackTrace ?? StackTrace.current); + var oldErrorHandler = app!.errorHandler; + app!.errorHandler = (e, req, res) { + app!.logger!.severe(e.message, e, e.stackTrace ?? StackTrace.current); return oldErrorHandler(e, req, res); }; - app.logger = Logger('angel_auth') + app!.logger = Logger('angel_auth') ..level = Level.FINEST ..onRecord.listen((rec) { print(rec); @@ -65,28 +67,30 @@ main() { } }); - await app - .findService('users') + await app! + .findService('users')! .create({'username': 'jdoe1', 'password': 'password'}); - auth = AngelAuth(); - auth.serializer = (u) => u.id; + auth = AngelAuth(); + auth.serializer = (u) => u!.id; auth.deserializer = - (id) async => await app.findService('users').read(id) as User; + (id) async => await app!.findService('users')!.read(id) as User; - await app.configure(auth.configureServer); + await app!.configure(auth.configureServer); auth.strategies['local'] = LocalAuthStrategy((username, password) async { - var users = await app - .findService('users') + var users = await app! + .findService('users')! .index() .then((it) => it.map((m) => User.parse(m as Map)).toList()); - return users.firstWhere( - (user) => user.username == username && user.password == password, - orElse: () => null); + + var result = users.firstWhereOrNull( + (user) => user.username == username && user.password == password); + + return Future.value(result); }); - app.post( + app!.post( '/login', auth.authenticate('local', AngelAuthOptions(callback: (req, res, token) { @@ -95,10 +99,10 @@ main() { ..close(); }))); - app.chain([ + app!.chain([ (req, res) { - if (!req.container.has()) { - req.container.registerSingleton( + if (!req.container!.has()) { + req.container!.registerSingleton( User(username: req.params['name']?.toString())); } return true; @@ -114,7 +118,7 @@ main() { }); tearDown(() async { - client.close(); + client!.close(); await angelHttp.close(); app = null; client = null; @@ -122,7 +126,7 @@ main() { }); test('login', () async { - final response = await client.post(Uri.parse('$url/login'), + final response = await client!.post(Uri.parse('$url/login'), body: {'username': 'jdoe1', 'password': 'password'}); print('Response: ${response.body}'); expect(response.body, equals('Hello!')); @@ -132,7 +136,7 @@ main() { : null); test('preserve existing user', () async { - final response = await client.post(Uri.parse('$url/existing/foo'), + final response = await client!.post(Uri.parse('$url/existing/foo'), body: {'username': 'jdoe1', 'password': 'password'}, headers: {'accept': 'application/json'}); print('Response: ${response.body}'); diff --git a/packages/auth/test/local_test.dart b/packages/auth/test/local_test.dart index f0239e6e..04e9c655 100644 --- a/packages/auth/test/local_test.dart +++ b/packages/auth/test/local_test.dart @@ -7,17 +7,17 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:test/test.dart'; -final AngelAuth> auth = AngelAuth>(); +final AngelAuth?> auth = AngelAuth?>(); var headers = {'accept': 'application/json'}; var localOpts = AngelAuthOptions>( failureRedirect: '/failure', successRedirect: '/success'); Map sampleUser = {'hello': 'world'}; -Future> verifier(String username, String password) async { +Future> verifier(String? username, String? password) async { if (username == 'username' && password == 'password') { return sampleUser; } else { - return null; + throw ArgumentError('Unexpected type for data'); } } @@ -31,10 +31,10 @@ Future wireAuth(Angel app) async { void main() async { Angel app; - AngelHttp angelHttp; - http.Client client; - String url; - String basicAuthUrl; + late AngelHttp angelHttp; + http.Client? client; + String? url; + String? basicAuthUrl; setUp(() async { client = http.Client(); @@ -72,7 +72,7 @@ void main() async { }); test('can use "auth" as middleware', () async { - var response = await client.get(Uri.parse('$url/success'), + var response = await client!.get(Uri.parse('$url/success'), headers: {'Accept': 'application/json'}); print(response.body); expect(response.statusCode, equals(403)); @@ -80,7 +80,7 @@ void main() async { test('successRedirect', () async { var postData = {'username': 'username', 'password': 'password'}; - var response = await client.post(Uri.parse('$url/login'), + var response = await client!.post(Uri.parse('$url/login'), body: json.encode(postData), headers: {'content-type': 'application/json'}); expect(response.statusCode, equals(302)); @@ -89,7 +89,7 @@ void main() async { test('failureRedirect', () async { var postData = {'username': 'password', 'password': 'username'}; - var response = await client.post(Uri.parse('$url/login'), + var response = await client!.post(Uri.parse('$url/login'), body: json.encode(postData), headers: {'content-type': 'application/json'}); print('Login response: ${response.body}'); @@ -99,13 +99,13 @@ void main() async { test('allow basic', () async { var authString = base64.encode('username:password'.runes.toList()); - var response = await client.get(Uri.parse('$url/hello'), + var response = await client!.get(Uri.parse('$url/hello'), headers: {'authorization': 'Basic $authString'}); expect(response.body, equals('"Woo auth"')); }); test('allow basic via URL encoding', () async { - var response = await client.get(Uri.parse('$basicAuthUrl/hello')); + var response = await client!.get(Uri.parse('$basicAuthUrl/hello')); expect(response.body, equals('"Woo auth"')); }); @@ -113,7 +113,7 @@ void main() async { auth.strategies.clear(); auth.strategies['local'] = LocalAuthStrategy(verifier, forceBasic: true, realm: 'test'); - var response = await client.get(Uri.parse('$url/hello'), headers: { + var response = await client!.get(Uri.parse('$url/hello'), headers: { 'accept': 'application/json', 'content-type': 'application/json' }); diff --git a/packages/auth/test/protect_cookie_test.dart b/packages/auth/test/protect_cookie_test.dart index 0d12f3d1..283af961 100644 --- a/packages/auth/test/protect_cookie_test.dart +++ b/packages/auth/test/protect_cookie_test.dart @@ -6,7 +6,7 @@ import 'package:test/test.dart'; const Duration threeDays = const Duration(days: 3); void main() { - Cookie defaultCookie; + late Cookie defaultCookie; var auth = AngelAuth( secureCookies: true, cookieDomain: 'SECURE', @@ -21,7 +21,7 @@ void main() { test('sets expires', () { var now = DateTime.now(); - var expiry = auth.protectCookie(defaultCookie).expires; + var expiry = auth.protectCookie(defaultCookie).expires!; var diff = expiry.difference(now); expect(diff.inSeconds, threeDays.inSeconds); });