diff --git a/.gitignore b/.gitignore
index a6816c59..c947f260 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml
new file mode 100644
index 00000000..a824b209
--- /dev/null
+++ b/.idea/runConfigurations/All_Tests.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Local_Tests.xml b/.idea/runConfigurations/Local_Tests.xml
new file mode 100644
index 00000000..26d8b79e
--- /dev/null
+++ b/.idea/runConfigurations/Local_Tests.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 5da64613..3fc8dfd7 100644
--- a/README.md
+++ b/README.md
@@ -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)
\ No newline at end of file
diff --git a/lib/angel_auth.dart b/lib/angel_auth.dart
index 43bf967c..d90e95d0 100644
--- a/lib/angel_auth.dart
+++ b/lib/angel_auth.dart
@@ -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 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';
diff --git a/lib/middleware/require_auth.dart b/lib/middleware/require_auth.dart
deleted file mode 100644
index 4a2c3948..00000000
--- a/lib/middleware/require_auth.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-part of angel_auth;
-
-/// Restricts access to a resource via authentication.
-Future 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();
-}
diff --git a/lib/middleware/serialization.dart b/lib/middleware/serialization.dart
deleted file mode 100644
index 56d478b5..00000000
--- a/lib/middleware/serialization.dart
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/lib/src/defs.dart b/lib/src/defs.dart
new file mode 100644
index 00000000..9d046397
--- /dev/null
+++ b/lib/src/defs.dart
@@ -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);
\ No newline at end of file
diff --git a/lib/src/middleware/require_auth.dart b/lib/src/middleware/require_auth.dart
new file mode 100644
index 00000000..c5d95ff0
--- /dev/null
+++ b/lib/src/middleware/require_auth.dart
@@ -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 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);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/options.dart b/lib/src/options.dart
new file mode 100644
index 00000000..be01c5c3
--- /dev/null
+++ b/lib/src/options.dart
@@ -0,0 +1,6 @@
+class AngelAuthOptions {
+ String successRedirect;
+ String failureRedirect;
+
+ AngelAuthOptions({String this.successRedirect, String this.failureRedirect});
+}
\ No newline at end of file
diff --git a/lib/src/plugin.dart b/lib/src/plugin.dart
new file mode 100644
index 00000000..b7144739
--- /dev/null
+++ b/lib/src/plugin.dart
@@ -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 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;
+ };
+ }
+}
\ No newline at end of file
diff --git a/lib/strategies/local.dart b/lib/src/strategies/local.dart
similarity index 84%
rename from lib/strategies/local.dart
rename to lib/src/strategies/local.dart
index fad52557..7a2cfb44 100644
--- a/lib/strategies/local.dart
+++ b/lib/src/strategies/local.dart
@@ -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);
diff --git a/lib/strategies/oauth2.dart b/lib/src/strategies/oauth2.dart
similarity index 91%
rename from lib/strategies/oauth2.dart
rename to lib/src/strategies/oauth2.dart
index adfc0873..1a663bbd 100644
--- a/lib/strategies/oauth2.dart
+++ b/lib/src/strategies/oauth2.dart
@@ -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);
diff --git a/lib/src/strategies/strategies.dart b/lib/src/strategies/strategies.dart
new file mode 100644
index 00000000..3861d127
--- /dev/null
+++ b/lib/src/strategies/strategies.dart
@@ -0,0 +1,3 @@
+export 'local.dart';
+export 'oauth2.dart';
+export 'token.dart';
\ No newline at end of file
diff --git a/lib/strategies/token.dart b/lib/src/strategies/token.dart
similarity index 51%
rename from lib/strategies/token.dart
rename to lib/src/strategies/token.dart
index b27f87cc..145eda86 100644
--- a/lib/strategies/token.dart
+++ b/lib/src/strategies/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 canLogout(RequestContext req, ResponseContext res) {
-
- }
+ Future canLogout(RequestContext req, ResponseContext res) async => false;
}
\ No newline at end of file
diff --git a/lib/strategy.dart b/lib/src/strategy.dart
similarity index 79%
rename from lib/strategy.dart
rename to lib/src/strategy.dart
index 42d9e6ad..b464f8e1 100644
--- a/lib/strategy.dart
+++ b/lib/src/strategy.dart
@@ -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 {
diff --git a/pubspec.yaml b/pubspec.yaml
index f8ebb4de..2a35581a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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
homepage: https://github.com/angel-dart/angel_auth
dependencies:
diff --git a/test/everything.dart b/test/all_tests.dart
similarity index 94%
rename from test/everything.dart
rename to test/all_tests.dart
index 592e04ec..2941da27 100644
--- a/test/everything.dart
+++ b/test/all_tests.dart
@@ -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 {
});
diff --git a/test/local.dart b/test/local.dart
index e2e81aad..3015c84f 100644
--- a/test/local.dart
+++ b/test/local.dart
@@ -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"'));
});
});
-}
\ No newline at end of file
+}