Fixed bugs

This commit is contained in:
Tobe O 2018-09-11 18:03:35 -04:00
parent a6b08ae7c4
commit cba439e773
8 changed files with 86 additions and 77 deletions

View file

@ -1,3 +1,8 @@
# 2.0.0-alpha.1
* Made `AuthStrategy` generic.
* `AngelAuth.strategies` is now a `Map<String, AuthStrategy<User>>`.
* Removed `AuthStrategy.canLogout`.
# 2.0.0-alpha # 2.0.0-alpha
* Depend on Dart 2 and Angel 2. * Depend on Dart 2 and Angel 2.
* Remove `dart2_constant`. * Remove `dart2_constant`.

View file

@ -9,11 +9,11 @@ import 'options.dart';
import 'strategy.dart'; import 'strategy.dart';
/// Handles authentication within an Angel application. /// Handles authentication within an Angel application.
class AngelAuth<T> { class AngelAuth<User> {
Hmac _hs256; Hmac _hs256;
int _jwtLifeSpan; int _jwtLifeSpan;
final StreamController<T> _onLogin = new StreamController<T>(), final StreamController<User> _onLogin = new StreamController<User>(),
_onLogout = new StreamController<T>(); _onLogout = new StreamController<User>();
Math.Random _random = new Math.Random.secure(); Math.Random _random = new Math.Random.secure();
final RegExp _rgxBearer = new RegExp(r"^Bearer"); final RegExp _rgxBearer = new RegExp(r"^Bearer");
@ -46,19 +46,19 @@ class AngelAuth<T> {
String reviveTokenEndpoint; String reviveTokenEndpoint;
/// A set of [AuthStrategy] instances used to authenticate users. /// A set of [AuthStrategy] instances used to authenticate users.
List<AuthStrategy> strategies = []; Map<String, AuthStrategy<User>> strategies = {};
/// Serializes a user into a unique identifier associated only with one identity. /// Serializes a user into a unique identifier associated only with one identity.
UserSerializer<T> serializer; UserSerializer<User> serializer;
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance. /// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
UserDeserializer<T> deserializer; UserDeserializer<User> deserializer;
/// Fires the result of [deserializer] whenever a user signs in to the application. /// Fires the result of [deserializer] whenever a user signs in to the application.
Stream<T> get onLogin => _onLogin.stream; Stream<User> get onLogin => _onLogin.stream;
/// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application. /// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application.
Stream<T> get onLogout => _onLogout.stream; Stream<User> get onLogout => _onLogout.stream;
/// The [Hmac] being used to encode JWT's. /// The [Hmac] being used to encode JWT's.
Hmac get hmac => _hs256; Hmac get hmac => _hs256;
@ -110,10 +110,12 @@ class AngelAuth<T> {
} }
void _apply( void _apply(
RequestContext req, ResponseContext res, AuthToken token, T user) { RequestContext req, ResponseContext res, AuthToken token, User user) {
req.container if (!req.container.has<User>()) {
..registerSingleton<AuthToken>(token) req.container
..registerSingleton<T>(user); ..registerSingleton<AuthToken>(token)
..registerSingleton<User>(user);
}
if (allowCookie == true) { if (allowCookie == true) {
_addProtectedCookie(res, 'token', token.serialize(_hs256)); _addProtectedCookie(res, 'token', token.serialize(_hs256));
@ -265,15 +267,13 @@ class AngelAuth<T> {
for (int i = 0; i < names.length; i++) { for (int i = 0; i < names.length; i++) {
var name = names[i]; var name = names[i];
AuthStrategy strategy = strategies.firstWhere( var strategy = strategies[name] ??=
(AuthStrategy x) => x.name == name, throw new ArgumentError('No strategy "$name" found.');
orElse: () =>
throw new ArgumentError('No strategy "$name" found.'));
var hasExisting = req.container.has<T>(); var hasExisting = req.container.has<User>();
var result = hasExisting var result = hasExisting
? req.container.make<T>() ? req.container.make<User>()
: await strategy.authenticate(req, res, options) as T; : await strategy.authenticate(req, res, options);
if (result == true) if (result == true)
return result; return result;
else if (result != false) { else if (result != false) {
@ -285,7 +285,10 @@ class AngelAuth<T> {
var jwt = token.serialize(_hs256); var jwt = token.serialize(_hs256);
if (options?.tokenCallback != null) { if (options?.tokenCallback != null) {
req.container.registerSingleton<T>(result); if (!req.container.has<User>()) {
req.container.registerSingleton<User>(result);
}
var r = await options.tokenCallback(req, res, token, result); var r = await options.tokenCallback(req, res, token, result);
if (r != null) return r; if (r != null) return r;
jwt = token.serialize(_hs256); jwt = token.serialize(_hs256);
@ -355,20 +358,8 @@ class AngelAuth<T> {
/// Log an authenticated user out. /// Log an authenticated user out.
RequestHandler logout([AngelAuthOptions options]) { RequestHandler logout([AngelAuthOptions options]) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
for (AuthStrategy strategy in strategies) { if (req.container.has<User>()) {
if (!(await strategy.canLogout(req, res))) { var user = req.container.make<User>();
if (options != null &&
options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) {
res.redirect(options.failureRedirect);
}
return false;
}
}
if (req.container.has<T>()) {
var user = req.container.make<T>();
_onLogout.add(user); _onLogout.add(user);
} }

View file

@ -1,10 +1,14 @@
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'options.dart'; import 'options.dart';
/// Displays a default callback page to confirm authentication via popups. /// Displays a default callback page to confirm authentication via popups.
AngelAuthCallback confirmPopupAuthentication({String eventName: 'token'}) { 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 res
..contentType = new MediaType('text', 'html') ..contentType = new MediaType('text', 'html')
..write(''' ..write('''
@ -14,7 +18,7 @@ AngelAuthCallback confirmPopupAuthentication({String eventName: 'token'}) {
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Authentication Success</title> <title>Authentication Success</title>
<script> <script>
var ev = new CustomEvent('$eventName', { detail: '$jwt' }); var ev = new CustomEvent($evt, $detail);
window.opener.dispatchEvent(ev); window.opener.dispatchEvent(ev);
window.close(); window.close();
</script> </script>

View file

@ -7,15 +7,14 @@ 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. /// Determines the validity of an incoming username and password.
typedef Future LocalAuthVerifier(String username, String password); typedef FutureOr<User> LocalAuthVerifier<User>(
String username, String password);
class LocalAuthStrategy extends AuthStrategy { class LocalAuthStrategy<User> extends AuthStrategy<User> {
RegExp _rgxBasic = new RegExp(r'^Basic (.+)$', caseSensitive: false); RegExp _rgxBasic = new RegExp(r'^Basic (.+)$', caseSensitive: false);
RegExp _rgxUsrPass = new RegExp(r'^([^:]+):(.+)$'); RegExp _rgxUsrPass = new RegExp(r'^([^:]+):(.+)$');
@override LocalAuthVerifier<User> verifier;
String name = 'local';
LocalAuthVerifier verifier;
String usernameField; String usernameField;
String passwordField; String passwordField;
String invalidMessage; String invalidMessage;
@ -23,7 +22,7 @@ class LocalAuthStrategy extends AuthStrategy {
final bool forceBasic; final bool forceBasic;
String realm; String realm;
LocalAuthStrategy(LocalAuthVerifier this.verifier, LocalAuthStrategy(this.verifier,
{String this.usernameField: 'username', {String this.usernameField: 'username',
String this.passwordField: 'password', String this.passwordField: 'password',
String this.invalidMessage: String this.invalidMessage:
@ -33,15 +32,10 @@ class LocalAuthStrategy extends AuthStrategy {
String this.realm: 'Authentication is required.'}) {} String this.realm: 'Authentication is required.'}) {}
@override @override
Future<bool> canLogout(RequestContext req, ResponseContext res) async { Future<User> authenticate(RequestContext req, ResponseContext res,
return true;
}
@override
Future authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options_]) async { [AngelAuthOptions options_]) async {
AngelAuthOptions options = options_ ?? new AngelAuthOptions(); AngelAuthOptions options = options_ ?? new AngelAuthOptions();
var verificationResult; User verificationResult;
if (allowBasic) { if (allowBasic) {
String authHeader = req.headers.value('authorization') ?? ""; String authHeader = req.headers.value('authorization') ?? "";
@ -62,7 +56,7 @@ class LocalAuthStrategy extends AuthStrategy {
..statusCode = 401 ..statusCode = 401
..headers['www-authenticate'] = 'Basic realm="$realm"' ..headers['www-authenticate'] = 'Basic realm="$realm"'
..close(); ..close();
return false; return null;
} }
return verificationResult; return verificationResult;
@ -82,17 +76,15 @@ class LocalAuthStrategy extends AuthStrategy {
if (options.failureRedirect != null && if (options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) { options.failureRedirect.isNotEmpty) {
res.redirect(options.failureRedirect, code: 401); res.redirect(options.failureRedirect, code: 401);
return false; return null;
} }
if (forceBasic) { if (forceBasic) {
res res.headers['www-authenticate'] = 'Basic realm="$realm"';
..statusCode = 401 throw new AngelHttpException.notAuthenticated();
..headers['www-authenticate'] = 'Basic realm="$realm"'
..close();
} }
return false; return null;
} else if (verificationResult != null && verificationResult != false) { } else if (verificationResult != null && verificationResult != false) {
return verificationResult; return verificationResult;
} else { } else {

View file

@ -3,13 +3,8 @@ import 'package:angel_framework/angel_framework.dart';
import 'options.dart'; import 'options.dart';
/// A function that handles login and signup for an Angel application. /// A function that handles login and signup for an Angel application.
abstract class AuthStrategy { abstract class AuthStrategy<User> {
String name;
/// Authenticates or rejects an incoming user. /// Authenticates or rejects an incoming user.
Future authenticate(RequestContext req, ResponseContext res, FutureOr<User> authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options]); [AngelAuthOptions options]);
/// Determines whether a signed-in user can log out or not.
Future<bool> canLogout(RequestContext req, ResponseContext res);
} }

View file

@ -1,6 +1,6 @@
name: angel_auth name: angel_auth
description: A complete authentication plugin for Angel. description: A complete authentication plugin for Angel.
version: 2.0.0-alpha version: 2.0.0-alpha.1
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_auth homepage: https://github.com/angel-dart/angel_auth
environment: environment:

View file

@ -11,6 +11,23 @@ class User extends Model {
String username, password; String username, password;
User({this.username, this.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<String, dynamic> toJson() {
return {
'id': id,
'username': username,
'password': password,
'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String()
};
}
} }
main() { main() {
@ -59,14 +76,16 @@ main() {
await app.configure(auth.configureServer); await app.configure(auth.configureServer);
app.fallback(auth.decodeJwt); app.fallback(auth.decodeJwt);
auth.strategies.add(new LocalAuthStrategy((username, password) async { auth.strategies['local'] =
final List<User> users = await app.service('users').index(); new LocalAuthStrategy((username, password) async {
final found = users.firstWhere( var users = (await app
.service('users')
.index()
.then((it) => it.map<User>(User.parse).toList())) as Iterable<User>;
return users.firstWhere(
(user) => user.username == username && user.password == password, (user) => user.username == username && user.password == password,
orElse: () => null); orElse: () => null);
});
return found != null ? found : false;
}));
app.post( app.post(
'/login', '/login',
@ -79,8 +98,10 @@ main() {
app.chain([ app.chain([
(req, res) { (req, res) {
req.container.registerSingleton<User>( if (!req.container.has<User>()) {
new User(username: req.params['name']?.toString())); req.container.registerSingleton<User>(
new User(username: req.params['name']?.toString()));
}
return true; return true;
} }
]).post( ]).post(

View file

@ -13,18 +13,18 @@ AngelAuthOptions localOpts = new AngelAuthOptions(
failureRedirect: '/failure', successRedirect: '/success'); failureRedirect: '/failure', successRedirect: '/success');
Map<String, String> sampleUser = {'hello': 'world'}; Map<String, String> sampleUser = {'hello': 'world'};
Future verifier(String username, String password) async { Future<Map<String, String>> verifier(String username, String password) async {
if (username == 'username' && password == 'password') { if (username == 'username' && password == 'password') {
return sampleUser; return sampleUser;
} else } else
return false; return null;
} }
Future wireAuth(Angel app) async { Future wireAuth(Angel app) async {
auth.serializer = (user) async => 1337; auth.serializer = (user) async => 1337;
auth.deserializer = (id) async => sampleUser; auth.deserializer = (id) async => sampleUser;
auth.strategies.add(new LocalAuthStrategy(verifier)); auth.strategies['local'] = new LocalAuthStrategy(verifier);
await app.configure(auth.configureServer); await app.configure(auth.configureServer);
app.fallback(auth.decodeJwt); app.fallback(auth.decodeJwt);
} }
@ -103,10 +103,11 @@ main() async {
test('force basic', () async { test('force basic', () async {
auth.strategies.clear(); auth.strategies.clear();
auth.strategies auth.strategies['local'] =
.add(new LocalAuthStrategy(verifier, forceBasic: true, realm: 'test')); new LocalAuthStrategy(verifier, forceBasic: true, realm: 'test');
var response = await client.get("$url/hello", headers: headers); var response = await client.get("$url/hello", headers: headers);
print(response.headers); print(response.headers);
print('Body <${response.body}>');
expect(response.headers['www-authenticate'], equals('Basic realm="test"')); expect(response.headers['www-authenticate'], equals('Basic realm="test"'));
}); });
} }