This commit is contained in:
thosakwe 2016-09-21 02:19:52 -04:00
parent 30d5293612
commit e168ea7ba3
19 changed files with 216 additions and 199 deletions

21
.gitignore vendored
View file

@ -1,3 +1,5 @@
# Created by .ignore support plugin (hsz.mobi)
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
@ -9,7 +11,7 @@ build/
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
@ -22,7 +24,7 @@ build/
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template
@ -70,19 +72,4 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
# Directory created by dartdoc
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
.idea

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/all_tests.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Local Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/local.dart" />
<method />
</configuration>
</component>

View file

@ -1,2 +1,8 @@
# angel_auth
A complete authentication plugin for Angel. Inspired by Passport.
# Documentation
Coming soon!
# Supported Strategies
* Local (with and without Basic Auth)

View file

@ -1,91 +1,8 @@
library angel_auth;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:oauth2/oauth2.dart' as Oauth2;
part 'strategy.dart';
part 'middleware/require_auth.dart';
part 'middleware/serialization.dart';
part 'strategies/local.dart';
part 'strategies/token.dart';
part 'strategies/oauth2.dart';
_validateString(String str) {
return str != null && str.isNotEmpty;
}
const String FAILURE_REDIRECT = 'failureRedirect';
const String SUCCESS_REDIRECT = 'successRedirect';
class Auth {
static List<AuthStrategy> strategies = [];
static UserSerializer serializer;
static UserDeserializer deserializer;
call(Angel app) async {
app.registerMiddleware('auth', requireAuth);
app.before.add(_serializationMiddleware);
}
static authenticate(String type, [AngelAuthOptions options]) {
return (RequestContext req, ResponseContext res) async {
AuthStrategy strategy =
strategies.firstWhere((AuthStrategy x) => x.name == type);
var result = await strategy.authenticate(req, res, options);
if (result == true)
return result;
else if (result != false) {
req.session['userId'] = await serializer(result);
return true;
} else {
throw new AngelHttpException.NotAuthenticated();
}
};
}
static logout([AngelAuthOptions options]) {
return (RequestContext req, ResponseContext res) async {
for (AuthStrategy strategy in Auth.strategies) {
if (!(await strategy.canLogout(req, res))) {
if (options != null &&
options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) {
return res.redirect(options.failureRedirect);
}
return false;
}
}
req.session.remove('userId');
if (options != null &&
options.successRedirect != null &&
options.successRedirect.isNotEmpty) {
return res.redirect(options.successRedirect);
}
return true;
};
}
}
class AngelAuthOptions {
String successRedirect;
String failureRedirect;
AngelAuthOptions({String this.successRedirect, String this.failureRedirect});
}
/// Configures an app to use angel_auth. :)
Future AngelAuth(Angel app) async {
await app.configure(new Auth());
}
export 'src/middleware/require_auth.dart';
export 'src/strategies/strategies.dart';
export 'src/defs.dart';
export 'src/options.dart';
export 'src/plugin.dart';
export 'src/strategy.dart';

View file

@ -1,32 +0,0 @@
part of angel_auth;
/// Restricts access to a resource via authentication.
Future<bool> requireAuth(RequestContext req, ResponseContext res,
{bool throws: true}) async {
reject() {
if (throws) {
res.status(HttpStatus.UNAUTHORIZED);
throw new AngelHttpException.Forbidden();
} else
return false;
}
if (req.session.containsKey('userId'))
return true;
else if (req.headers.value("Authorization") != null) {
var jwt = req.headers
.value("Authorization")
.replaceAll(new RegExp(r"^Bearer", caseSensitive: false), "")
.trim();
var split = jwt.split(".");
if (split.length != 3) return reject();
Map header = JSON.decode(UTF8.decode(BASE64URL.decode(split[0])));
if (header['typ'] != "JWT" || header['alg'] != "HS256") return reject();
Map payload = JSON.decode(UTF8.decode(BASE64URL.decode(split[1])));
} else
return reject();
}

View file

@ -1,15 +0,0 @@
part of angel_auth;
/// Serializes a user to the session.
typedef Future UserSerializer(user);
/// Deserializes a user from the session.
typedef Future UserDeserializer(userId);
_serializationMiddleware(RequestContext req, ResponseContext res) async {
if (await requireAuth(req, res, throws: false)) {
req.properties['user'] = await Auth.deserializer(req.session['userId']);
}
return true;
}

7
lib/src/defs.dart Normal file
View file

@ -0,0 +1,7 @@
import 'dart:async';
/// Serializes a user to the session.
typedef Future UserSerializer(user);
/// Deserializes a user from the session.
typedef Future UserDeserializer(userId);

View file

@ -0,0 +1,42 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
/// Restricts access to a resource via authentication.
class RequireAuthorizationMiddleware extends BaseMiddleware {
@override
Future<bool> call(RequestContext req, ResponseContext res, {bool throwError: true}) async {
bool _reject(ResponseContext res) {
if (throwError) {
res.status(HttpStatus.FORBIDDEN);
throw new AngelHttpException.Forbidden();
} else
return false;
}
if (req.session.containsKey('userId'))
return true;
else if (req.headers.value("Authorization") != null) {
var jwt = req.headers
.value("Authorization")
.replaceAll(new RegExp(r"^Bearer", caseSensitive: false), "")
.trim();
var split = jwt.split(".");
if (split.length != 3) return _reject(res);
Map header = JSON.decode(UTF8.decode(BASE64URL.decode(split[0])));
if (header['typ'] != "JWT" || header['alg'] != "HS256")
return _reject(res);
Map payload = JSON.decode(UTF8.decode(BASE64URL.decode(split[1])));
// Todo: JWT
return false;
} else
return _reject(res);
}
}

6
lib/src/options.dart Normal file
View file

@ -0,0 +1,6 @@
class AngelAuthOptions {
String successRedirect;
String failureRedirect;
AngelAuthOptions({String this.successRedirect, String this.failureRedirect});
}

73
lib/src/plugin.dart Normal file
View file

@ -0,0 +1,73 @@
import 'package:angel_framework/angel_framework.dart';
import 'middleware/require_auth.dart';
import 'defs.dart';
import 'options.dart';
import 'strategy.dart';
class AngelAuth extends AngelPlugin {
RequireAuthorizationMiddleware _requireAuth = new RequireAuthorizationMiddleware();
List<AuthStrategy> strategies = [];
UserSerializer serializer;
UserDeserializer deserializer;
@override
call(Angel app) async {
app.container.singleton(this);
if (runtimeType != AngelAuth)
app.container.singleton(this, as: AngelAuth);
app.registerMiddleware('auth', _requireAuth);
app.before.add(_serializationMiddleware);
}
_serializationMiddleware(RequestContext req, ResponseContext res) async {
if (await _requireAuth(req, res, throwError: false)) {
req.properties['user'] = await deserializer(req.session['userId']);
}
return true;
}
authenticate(String type, [AngelAuthOptions options]) {
return (RequestContext req, ResponseContext res) async {
AuthStrategy strategy =
strategies.firstWhere((AuthStrategy x) => x.name == type);
var result = await strategy.authenticate(req, res, options);
if (result == true)
return result;
else if (result != false) {
req.session['userId'] = await serializer(result);
return true;
} else {
throw new AngelHttpException.NotAuthenticated();
}
};
}
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) {
return res.redirect(options.failureRedirect);
}
return false;
}
}
req.session.remove('userId');
if (options != null &&
options.successRedirect != null &&
options.successRedirect.isNotEmpty) {
return res.redirect(options.successRedirect);
}
return true;
};
}
}

View file

@ -1,9 +1,18 @@
part of angel_auth;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import '../options.dart';
import '../plugin.dart';
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);
class LocalAuthStrategy extends AuthStrategy {
AngelAuth _plugin;
RegExp _rgxBasic = new RegExp(r'^Basic (.+)$', caseSensitive: false);
RegExp _rgxUsrPass = new RegExp(r'^([^:]+):(.+)$');
@ -17,7 +26,7 @@ class LocalAuthStrategy extends AuthStrategy {
bool forceBasic;
String realm;
LocalAuthStrategy(LocalAuthVerifier this.verifier,
LocalAuthStrategy(AngelAuth this._plugin, LocalAuthVerifier this.verifier,
{String this.usernameField: 'username',
String this.passwordField: 'password',
String this.invalidMessage:
@ -64,7 +73,7 @@ class LocalAuthStrategy extends AuthStrategy {
if (options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) {
return res.redirect(
options.failureRedirect, code: HttpStatus.UNAUTHORIZED);
options.failureRedirect, code: HttpStatus.FORBIDDEN);
}
if (forceBasic) {
@ -77,7 +86,7 @@ class LocalAuthStrategy extends AuthStrategy {
}
else if (verificationResult != null && verificationResult != false) {
req.session['userId'] = await Auth.serializer(verificationResult);
req.session['userId'] = await _plugin.serializer(verificationResult);
if (options.successRedirect != null &&
options.successRedirect.isNotEmpty) {
return res.redirect(options.successRedirect, code: HttpStatus.OK);

View file

@ -1,5 +1,8 @@
part of angel_auth;
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:oauth2/oauth2.dart' as Oauth2;
import '../options.dart';
import '../strategy.dart';
/// Logs a user in based on an incoming OAuth access and refresh token.
typedef Future OAuth2AuthVerifier(String accessToken, String refreshToken,
Map profile);

View file

@ -0,0 +1,3 @@
export 'local.dart';
export 'oauth2.dart';
export 'token.dart';

View file

@ -1,15 +1,16 @@
part of angel_auth;
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import '../options.dart';
import '../strategy.dart';
class JwtAuthStrategy extends AuthStrategy {
@override
Future authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options]) {
[AngelAuthOptions options]) async {
}
@override
Future<bool> canLogout(RequestContext req, ResponseContext res) {
}
Future<bool> canLogout(RequestContext req, ResponseContext res) async => false;
}

View file

@ -1,4 +1,6 @@
part of angel_auth;
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'options.dart';
/// A function that handles login and signup for an Angel application.
abstract class AuthStrategy {

View file

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

View file

@ -3,6 +3,7 @@ import 'package:angel_framework/angel_framework.dart';
import 'package:angel_auth/angel_auth.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
import 'local.dart' as local;
wireAuth(Angel app) async {
@ -29,6 +30,8 @@ main() async {
url = null;
});
group("local", local.main);
test('can use login as middleware', () async {
});

View file

@ -6,30 +6,29 @@ import 'package:http/http.dart' as http;
import 'package:merge_map/merge_map.dart';
import 'package:test/test.dart';
final AngelAuth Auth = new AngelAuth();
Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType};
AngelAuthOptions localOpts = new AngelAuthOptions(
failureRedirect: '/failure',
successRedirect: '/success'
);
failureRedirect: '/failure', successRedirect: '/success');
Map sampleUser = {'hello': 'world'};
verifier(username, password) async {
if (username == 'username' && password == 'password') {
return sampleUser;
} else return false;
} else
return false;
}
wireAuth(Angel app) async {
Auth.serializer = (user) async => 1337;
Auth.deserializer = (id) async => sampleUser;
Auth.strategies.add(new LocalAuthStrategy(verifier));
await app.configure(AngelAuth);
Auth.strategies.add(new LocalAuthStrategy(Auth, verifier));
await app.configure(Auth);
}
main() async {
group
('local', () {
group('local', () {
Angel app;
http.Client client;
String url;
@ -45,11 +44,11 @@ main() async {
app.get('/success', "yep", middleware: ['auth']);
app.get('/failure', "nope");
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
HttpServer server =
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
basicAuthUrl =
"http://username:password@${server.address.host}:${server.port}";
"http://username:password@${server.address.host}:${server.port}";
});
tearDown(() async {
@ -59,42 +58,36 @@ main() async {
basicAuthUrl = null;
});
test('can use login as middleware', () async {
var response = await client.get(
"$url/success", headers: {'Accept': 'application/json'});
test('can use "auth" as middleware', () async {
var response = await client
.get("$url/success", headers: {'Accept': 'application/json'});
print(response.body);
expect(response.statusCode, equals(401));
expect(response.statusCode, equals(403));
});
test('successRedirect', () async {
Map postData = {
'username': 'username',
'password': 'password'
};
var response = await client.post(
"$url/login", body: JSON.encode(postData),
Map postData = {'username': 'username', 'password': 'password'};
var response = await client.post("$url/login",
body: JSON.encode(postData),
headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType});
expect(response.statusCode, equals(200));
expect(response.headers[HttpHeaders.LOCATION], equals('/success'));
});
test('failureRedirect', () async {
Map postData = {
'username': 'password',
'password': 'username'
};
var response = await client.post(
"$url/login", body: JSON.encode(postData),
Map postData = {'username': 'password', 'password': 'username'};
var response = await client.post("$url/login",
body: JSON.encode(postData),
headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType});
expect(response.statusCode, equals(401));
expect(response.statusCode, equals(403));
expect(response.headers[HttpHeaders.LOCATION], equals('/failure'));
});
test('allow basic', () async {
String authString = BASE64.encode("username:password".runes.toList());
Map auth = {HttpHeaders.AUTHORIZATION: 'Basic $authString'};
var response = await client.get(
"$url/hello", headers: mergeMap([auth, headers]));
var response =
await client.get("$url/hello", headers: mergeMap([auth, headers]));
expect(response.body, equals('"Woo auth"'));
});
@ -105,11 +98,11 @@ main() async {
test('force basic', () async {
Auth.strategies.clear();
Auth.strategies.add(new LocalAuthStrategy(
verifier, forceBasic: true, realm: 'test'));
Auth.strategies.add(new LocalAuthStrategy(Auth, verifier,
forceBasic: true, realm: 'test'));
var response = await client.get("$url/hello", headers: headers);
expect(response.headers[HttpHeaders.WWW_AUTHENTICATE],
equals('Basic realm="test"'));
});
});
}
}