From e168ea7ba32653d7eb2e6be5733f0c98b118645f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 21 Sep 2016 02:19:52 -0400 Subject: [PATCH] +6 --- .gitignore | 21 +----- .idea/runConfigurations/All_Tests.xml | 6 ++ .idea/runConfigurations/Local_Tests.xml | 6 ++ README.md | 6 ++ lib/angel_auth.dart | 95 ++---------------------- lib/middleware/require_auth.dart | 32 -------- lib/middleware/serialization.dart | 15 ---- lib/src/defs.dart | 7 ++ lib/src/middleware/require_auth.dart | 42 +++++++++++ lib/src/options.dart | 6 ++ lib/src/plugin.dart | 73 ++++++++++++++++++ lib/{ => src}/strategies/local.dart | 17 ++++- lib/{ => src}/strategies/oauth2.dart | 7 +- lib/src/strategies/strategies.dart | 3 + lib/{ => src}/strategies/token.dart | 11 +-- lib/{ => src}/strategy.dart | 4 +- pubspec.yaml | 2 +- test/{everything.dart => all_tests.dart} | 3 + test/local.dart | 59 +++++++-------- 19 files changed, 216 insertions(+), 199 deletions(-) create mode 100644 .idea/runConfigurations/All_Tests.xml create mode 100644 .idea/runConfigurations/Local_Tests.xml delete mode 100644 lib/middleware/require_auth.dart delete mode 100644 lib/middleware/serialization.dart create mode 100644 lib/src/defs.dart create mode 100644 lib/src/middleware/require_auth.dart create mode 100644 lib/src/options.dart create mode 100644 lib/src/plugin.dart rename lib/{ => src}/strategies/local.dart (84%) rename lib/{ => src}/strategies/oauth2.dart (91%) create mode 100644 lib/src/strategies/strategies.dart rename lib/{ => src}/strategies/token.dart (51%) rename lib/{ => src}/strategy.dart (79%) rename test/{everything.dart => all_tests.dart} (94%) 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 +}