Fixed bugs
This commit is contained in:
parent
a6b08ae7c4
commit
cba439e773
8 changed files with 86 additions and 77 deletions
|
@ -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`.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue