+6
This commit is contained in:
parent
30d5293612
commit
e168ea7ba3
19 changed files with 216 additions and 199 deletions
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -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
|
||||
|
@ -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
|
6
.idea/runConfigurations/All_Tests.xml
Normal file
6
.idea/runConfigurations/All_Tests.xml
Normal 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>
|
6
.idea/runConfigurations/Local_Tests.xml
Normal file
6
.idea/runConfigurations/Local_Tests.xml
Normal 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>
|
|
@ -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)
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
7
lib/src/defs.dart
Normal 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);
|
42
lib/src/middleware/require_auth.dart
Normal file
42
lib/src/middleware/require_auth.dart
Normal 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
6
lib/src/options.dart
Normal 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
73
lib/src/plugin.dart
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
3
lib/src/strategies/strategies.dart
Normal file
3
lib/src/strategies/strategies.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'local.dart';
|
||||
export 'oauth2.dart';
|
||||
export 'token.dart';
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
||||
});
|
|
@ -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,8 +44,8 @@ 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}";
|
||||
|
@ -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,8 +98,8 @@ 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"'));
|
||||
|
|
Loading…
Reference in a new issue