diff --git a/CHANGELOG.md b/CHANGELOG.md index fcbb8d5b..bec9795a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.0.0-alpha.1 +* Made `AuthStrategy` generic. +* `AngelAuth.strategies` is now a `Map>`. +* Removed `AuthStrategy.canLogout`. + # 2.0.0-alpha * Depend on Dart 2 and Angel 2. * Remove `dart2_constant`. diff --git a/lib/src/plugin.dart b/lib/src/plugin.dart index 61abae1f..26ede098 100644 --- a/lib/src/plugin.dart +++ b/lib/src/plugin.dart @@ -9,11 +9,11 @@ import 'options.dart'; import 'strategy.dart'; /// Handles authentication within an Angel application. -class AngelAuth { +class AngelAuth { Hmac _hs256; int _jwtLifeSpan; - final StreamController _onLogin = new StreamController(), - _onLogout = new StreamController(); + final StreamController _onLogin = new StreamController(), + _onLogout = new StreamController(); Math.Random _random = new Math.Random.secure(); final RegExp _rgxBearer = new RegExp(r"^Bearer"); @@ -46,19 +46,19 @@ class AngelAuth { String reviveTokenEndpoint; /// A set of [AuthStrategy] instances used to authenticate users. - List strategies = []; + Map> strategies = {}; /// Serializes a user into a unique identifier associated only with one identity. - UserSerializer serializer; + UserSerializer serializer; /// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance. - UserDeserializer deserializer; + UserDeserializer 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; @@ -110,10 +110,12 @@ class AngelAuth { } void _apply( - RequestContext req, ResponseContext res, AuthToken token, T user) { - req.container - ..registerSingleton(token) - ..registerSingleton(user); + RequestContext req, ResponseContext res, AuthToken token, User user) { + if (!req.container.has()) { + req.container + ..registerSingleton(token) + ..registerSingleton(user); + } if (allowCookie == true) { _addProtectedCookie(res, 'token', token.serialize(_hs256)); @@ -265,15 +267,13 @@ class AngelAuth { for (int i = 0; i < names.length; i++) { var name = names[i]; - AuthStrategy strategy = strategies.firstWhere( - (AuthStrategy x) => x.name == name, - orElse: () => - throw new ArgumentError('No strategy "$name" found.')); + var strategy = strategies[name] ??= + throw new 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) as T; + ? req.container.make() + : await strategy.authenticate(req, res, options); if (result == true) return result; else if (result != false) { @@ -285,7 +285,10 @@ class AngelAuth { var jwt = token.serialize(_hs256); if (options?.tokenCallback != null) { - req.container.registerSingleton(result); + if (!req.container.has()) { + req.container.registerSingleton(result); + } + var r = await options.tokenCallback(req, res, token, result); if (r != null) return r; jwt = token.serialize(_hs256); @@ -355,20 +358,8 @@ class AngelAuth { /// Log an authenticated user out. RequestHandler 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) { - res.redirect(options.failureRedirect); - } - - return false; - } - } - - if (req.container.has()) { - var user = req.container.make(); + if (req.container.has()) { + var user = req.container.make(); _onLogout.add(user); } diff --git a/lib/src/popup_page.dart b/lib/src/popup_page.dart index 45a92b14..a6e72f76 100644 --- a/lib/src/popup_page.dart +++ b/lib/src/popup_page.dart @@ -1,10 +1,14 @@ +import 'dart:convert'; import 'package:angel_framework/angel_framework.dart'; import 'package:http_parser/http_parser.dart'; import 'options.dart'; /// Displays a default callback page to confirm authentication via popups. AngelAuthCallback confirmPopupAuthentication({String eventName: 'token'}) { - return (req, ResponseContext res, String jwt) async { + return (req, ResponseContext res, String jwt) { + var evt = json.encode(eventName); + var detail = json.encode({'detail': jwt}); + res ..contentType = new MediaType('text', 'html') ..write(''' @@ -14,7 +18,7 @@ AngelAuthCallback confirmPopupAuthentication({String eventName: 'token'}) { Authentication Success diff --git a/lib/src/strategies/local.dart b/lib/src/strategies/local.dart index c8858f8d..85a493f7 100644 --- a/lib/src/strategies/local.dart +++ b/lib/src/strategies/local.dart @@ -7,15 +7,14 @@ import '../strategy.dart'; bool _validateString(String str) => str != null && str.isNotEmpty; /// Determines the validity of an incoming username and password. -typedef Future LocalAuthVerifier(String username, String password); +typedef FutureOr LocalAuthVerifier( + String username, String password); -class LocalAuthStrategy extends AuthStrategy { +class LocalAuthStrategy extends AuthStrategy { RegExp _rgxBasic = new RegExp(r'^Basic (.+)$', caseSensitive: false); RegExp _rgxUsrPass = new RegExp(r'^([^:]+):(.+)$'); - @override - String name = 'local'; - LocalAuthVerifier verifier; + LocalAuthVerifier verifier; String usernameField; String passwordField; String invalidMessage; @@ -23,7 +22,7 @@ class LocalAuthStrategy extends AuthStrategy { final bool forceBasic; String realm; - LocalAuthStrategy(LocalAuthVerifier this.verifier, + LocalAuthStrategy(this.verifier, {String this.usernameField: 'username', String this.passwordField: 'password', String this.invalidMessage: @@ -33,15 +32,10 @@ class LocalAuthStrategy extends AuthStrategy { String this.realm: 'Authentication is required.'}) {} @override - Future canLogout(RequestContext req, ResponseContext res) async { - return true; - } - - @override - Future authenticate(RequestContext req, ResponseContext res, + Future authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options_]) async { AngelAuthOptions options = options_ ?? new AngelAuthOptions(); - var verificationResult; + User verificationResult; if (allowBasic) { String authHeader = req.headers.value('authorization') ?? ""; @@ -62,7 +56,7 @@ class LocalAuthStrategy extends AuthStrategy { ..statusCode = 401 ..headers['www-authenticate'] = 'Basic realm="$realm"' ..close(); - return false; + return null; } return verificationResult; @@ -82,17 +76,15 @@ class LocalAuthStrategy extends AuthStrategy { if (options.failureRedirect != null && options.failureRedirect.isNotEmpty) { res.redirect(options.failureRedirect, code: 401); - return false; + return null; } if (forceBasic) { - res - ..statusCode = 401 - ..headers['www-authenticate'] = 'Basic realm="$realm"' - ..close(); + res.headers['www-authenticate'] = 'Basic realm="$realm"'; + throw new AngelHttpException.notAuthenticated(); } - return false; + return null; } else if (verificationResult != null && verificationResult != false) { return verificationResult; } else { diff --git a/lib/src/strategy.dart b/lib/src/strategy.dart index 965fe76c..9558799d 100644 --- a/lib/src/strategy.dart +++ b/lib/src/strategy.dart @@ -3,13 +3,8 @@ import 'package:angel_framework/angel_framework.dart'; import 'options.dart'; /// A function that handles login and signup for an Angel application. -abstract class AuthStrategy { - String name; - +abstract class AuthStrategy { /// Authenticates or rejects an incoming user. - Future authenticate(RequestContext req, ResponseContext res, + FutureOr authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options]); - - /// Determines whether a signed-in user can log out or not. - Future canLogout(RequestContext req, ResponseContext res); } diff --git a/pubspec.yaml b/pubspec.yaml index 721c23ab..fe9deb99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_auth description: A complete authentication plugin for Angel. -version: 2.0.0-alpha +version: 2.0.0-alpha.1 author: Tobe O homepage: https://github.com/angel-dart/angel_auth environment: diff --git a/test/callback_test.dart b/test/callback_test.dart index c74c0191..bb41e837 100644 --- a/test/callback_test.dart +++ b/test/callback_test.dart @@ -11,6 +11,23 @@ class User extends Model { String username, password; User({this.username, this.password}); + + static User parse(Map map) { + return new User( + username: map['username'] as String, + password: map['password'] as String, + ); + } + + Map toJson() { + return { + 'id': id, + 'username': username, + 'password': password, + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String() + }; + } } main() { @@ -59,14 +76,16 @@ main() { await app.configure(auth.configureServer); app.fallback(auth.decodeJwt); - auth.strategies.add(new LocalAuthStrategy((username, password) async { - final List users = await app.service('users').index(); - final found = users.firstWhere( + auth.strategies['local'] = + new LocalAuthStrategy((username, password) async { + var users = (await app + .service('users') + .index() + .then((it) => it.map(User.parse).toList())) as Iterable; + return users.firstWhere( (user) => user.username == username && user.password == password, orElse: () => null); - - return found != null ? found : false; - })); + }); app.post( '/login', @@ -79,8 +98,10 @@ main() { app.chain([ (req, res) { - req.container.registerSingleton( - new User(username: req.params['name']?.toString())); + if (!req.container.has()) { + req.container.registerSingleton( + new User(username: req.params['name']?.toString())); + } return true; } ]).post( diff --git a/test/local_test.dart b/test/local_test.dart index 2eb8a5a6..6f0a34b1 100644 --- a/test/local_test.dart +++ b/test/local_test.dart @@ -13,18 +13,18 @@ AngelAuthOptions localOpts = new 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 false; + return null; } Future wireAuth(Angel app) async { auth.serializer = (user) async => 1337; auth.deserializer = (id) async => sampleUser; - auth.strategies.add(new LocalAuthStrategy(verifier)); + auth.strategies['local'] = new LocalAuthStrategy(verifier); await app.configure(auth.configureServer); app.fallback(auth.decodeJwt); } @@ -103,10 +103,11 @@ main() async { test('force basic', () async { auth.strategies.clear(); - auth.strategies - .add(new LocalAuthStrategy(verifier, forceBasic: true, realm: 'test')); + auth.strategies['local'] = + new LocalAuthStrategy(verifier, forceBasic: true, realm: 'test'); var response = await client.get("$url/hello", headers: headers); print(response.headers); + print('Body <${response.body}>'); expect(response.headers['www-authenticate'], equals('Basic realm="test"')); }); }