From 5faaceefa47e92f6e0d39ecf8ab416e8acb32cb3 Mon Sep 17 00:00:00 2001 From: regiostech Date: Mon, 9 May 2016 16:47:28 -0400 Subject: [PATCH] Local done, OAuth2 started --- lib/angel_auth.dart | 50 +++++++++++++++++++++--- lib/middleware/require_auth.dart | 5 ++- lib/strategies/local.dart | 18 +++++---- lib/strategies/oauth2.dart | 65 ++++++++++++++++++++++++++++++++ lib/strategy.dart | 2 +- pubspec.yaml | 2 + test/everything.dart | 4 ++ test/local.dart | 14 ++++--- 8 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 lib/strategies/oauth2.dart diff --git a/lib/angel_auth.dart b/lib/angel_auth.dart index f6f0d025..188028fd 100644 --- a/lib/angel_auth.dart +++ b/lib/angel_auth.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; +import 'package:crypto/crypto.dart' as Crypto; +import 'package:oauth2/oauth2.dart' as Oauth2; part 'strategy.dart'; @@ -13,10 +15,15 @@ part 'middleware/serialization.dart'; part 'strategies/local.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; @@ -27,16 +34,14 @@ class Auth { app.before.add(_serializationMiddleware); } - static authenticate(String type, [Map options]) { + 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: options ?? {}); - print("${req.path} -> $result"); + strategies.firstWhere((AuthStrategy x) => x.name == type); + var result = await strategy.authenticate(req, res, options); if (result == true) return result; - else if(result != false) { + else if (result != false) { req.session['userId'] = await serializer(result); return true; } else { @@ -44,6 +49,39 @@ class Auth { } }; } + + 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. :) diff --git a/lib/middleware/require_auth.dart b/lib/middleware/require_auth.dart index c87d1eae..e4e4b436 100644 --- a/lib/middleware/require_auth.dart +++ b/lib/middleware/require_auth.dart @@ -5,6 +5,9 @@ Future requireAuth(RequestContext req, ResponseContext res, {bool throws: true}) async { if (req.session.containsKey('userId')) return true; - else if (throws) throw new AngelHttpException.NotAuthenticated(); + else if (throws) { + res.status(HttpStatus.UNAUTHORIZED); + throw new AngelHttpException.NotAuthenticated(); + } else return false; } \ No newline at end of file diff --git a/lib/strategies/local.dart b/lib/strategies/local.dart index b98ac87d..7db160f5 100644 --- a/lib/strategies/local.dart +++ b/lib/strategies/local.dart @@ -15,7 +15,7 @@ class LocalAuthStrategy extends AuthStrategy { String invalidMessage; bool allowBasic; bool forceBasic; - String basicRealm; + String realm; LocalAuthStrategy(LocalAuthVerifier this.verifier, {String this.usernameField: 'username', @@ -24,7 +24,7 @@ class LocalAuthStrategy extends AuthStrategy { 'Please provide a valid username and password.', bool this.allowBasic: true, bool this.forceBasic: false, - String this.basicRealm: 'Authentication is required.'}) {} + String this.realm: 'Authentication is required.'}) {} @override Future canLogout(RequestContext req, ResponseContext res) async { @@ -33,7 +33,8 @@ class LocalAuthStrategy extends AuthStrategy { @override Future authenticate(RequestContext req, ResponseContext res, - {Map options: const {}}) async { + [AngelAuthOptions options_]) async { + AngelAuthOptions options = options_ ?? new AngelAuthOptions(); var verificationResult; if (allowBasic) { @@ -60,23 +61,24 @@ class LocalAuthStrategy extends AuthStrategy { } if (verificationResult == false || verificationResult == null) { - if (options.containsKey('failureRedirect')) { + if (options.failureRedirect != null && + options.failureRedirect.isNotEmpty) { return res.redirect( - options['failureRedirect'], code: HttpStatus.UNAUTHORIZED); + options.failureRedirect, code: HttpStatus.UNAUTHORIZED); } if (forceBasic) { res ..status(401) - ..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$basicRealm"') + ..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$realm"') ..end(); return false; } else throw new AngelHttpException.NotAuthenticated(); } req.session['user'] = await Auth.serializer(verificationResult); - if (options.containsKey('successRedirect')) { - return res.redirect(options['successRedirect'], code: HttpStatus.OK); + if (options.successRedirect != null && options.successRedirect.isNotEmpty) { + return res.redirect(options.successRedirect, code: HttpStatus.OK); } return true; diff --git a/lib/strategies/oauth2.dart b/lib/strategies/oauth2.dart new file mode 100644 index 00000000..adfc0873 --- /dev/null +++ b/lib/strategies/oauth2.dart @@ -0,0 +1,65 @@ +part of angel_auth; + +/// Logs a user in based on an incoming OAuth access and refresh token. +typedef Future OAuth2AuthVerifier(String accessToken, String refreshToken, + Map profile); + +class OAuth2AuthStrategy extends AuthStrategy { + @override + String name = "oauth2"; + OAuth2AuthVerifier verifier; + + Uri authEndPoint; + Uri tokenEndPoint; + String clientId; + String clientSecret; + Uri callbackUri; + List scopes; + + @override + Future authenticate(RequestContext req, ResponseContext res, + [AngelAuthOptions options_]) async { + Oauth2.Client client = await makeGrant().handleAuthorizationResponse(req.query); + // Remember: Do stuff + } + + @override + Future canLogout(RequestContext req, ResponseContext res) async { + return true; + } + + OAuth2AuthStrategy(String this.name, OAuth2AuthVerifier this.verifier, + {Uri this.authEndPoint, + Uri this.tokenEndPoint, + String this.clientId, + String this.clientSecret, + Uri this.callbackUri, + List this.scopes: const[]}) { + if (this.authEndPoint == null) + throw new ArgumentError.notNull('authEndPoint'); + if (this.tokenEndPoint == null) + throw new ArgumentError.notNull('tokenEndPoint'); + if (this.clientId == null || this.clientId.isEmpty) + throw new ArgumentError.notNull('clientId'); + } + + call(RequestContext req, ResponseContext res) async { + var grant = makeGrant(); + + Uri to = grant.getAuthorizationUrl(callbackUri, scopes: scopes); + return res.redirect(to.path); + } + + Oauth2.AuthorizationCodeGrant makeGrant() { + return new Oauth2.AuthorizationCodeGrant( + clientId, authEndPoint, tokenEndPoint, secret: clientSecret); + } +} + +class OAuth2AuthorizationError extends AngelHttpException { + OAuth2AuthorizationError({String message: "OAuth2 Authorization Error", + List errors: const []}) + : super.NotAuthenticated(message: message) { + this.errors = errors; + } +} diff --git a/lib/strategy.dart b/lib/strategy.dart index 73e65858..42d9e6ad 100644 --- a/lib/strategy.dart +++ b/lib/strategy.dart @@ -5,7 +5,7 @@ abstract class AuthStrategy { String name; /// Authenticates or rejects an incoming user. - Future authenticate(RequestContext req, ResponseContext res, {Map options: const {}}); + Future authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options]); /// Determines whether a signed-in user can log out or not. Future canLogout(RequestContext req, ResponseContext res); diff --git a/pubspec.yaml b/pubspec.yaml index 470eb1f8..04e34fa5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,6 +5,8 @@ author: thosakwe homepage: https://github.com/angel-dart/angel_auth dependencies: angel_framework: ">=0.0.0-dev < 0.1.0" + crypto: ">= 1.1.1 < 2.0.0" + oauth2: ">= 1.0.2 < 2.0.0" dev_dependencies: http: ">= 0.11.3 < 0.12.0" test: ">= 0.12.13 < 0.13.0" \ No newline at end of file diff --git a/test/everything.dart b/test/everything.dart index b60c958a..592e04ec 100644 --- a/test/everything.dart +++ b/test/everything.dart @@ -48,5 +48,9 @@ main() async { test('force everything', () async { }); + + test('logout', () async { + + }); }); } \ No newline at end of file diff --git a/test/local.dart b/test/local.dart index 2eb3f0a0..e2e81aad 100644 --- a/test/local.dart +++ b/test/local.dart @@ -7,7 +7,10 @@ import 'package:merge_map/merge_map.dart'; import 'package:test/test.dart'; Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType}; -Map localOpts = {'failureRedirect': '/failure'}; +AngelAuthOptions localOpts = new AngelAuthOptions( + failureRedirect: '/failure', + successRedirect: '/success' +); Map sampleUser = {'hello': 'world'}; verifier(username, password) async { @@ -35,7 +38,6 @@ main() async { setUp(() async { client = new http.Client(); app = new Angel(); - await app.configure(wireAuth); app.get('/hello', 'Woo auth', middleware: [Auth.authenticate('local')]); app.post('/login', 'This should not be shown', @@ -58,7 +60,8 @@ main() async { }); test('can use login as middleware', () async { - var response = await client.get("$url/success", headers: {'Accept': 'application/json'}); + var response = await client.get( + "$url/success", headers: {'Accept': 'application/json'}); print(response.body); expect(response.statusCode, equals(401)); }); @@ -96,15 +99,14 @@ main() async { }); test('allow basic via URL encoding', () async { - var response = await client.get( - basicAuthUrl, headers: headers); + var response = await client.get("$basicAuthUrl/hello", headers: headers); expect(response.body, equals('"Woo auth"')); }); test('force basic', () async { Auth.strategies.clear(); Auth.strategies.add(new LocalAuthStrategy( - verifier, forceBasic: true, basicRealm: 'test')); + verifier, forceBasic: true, realm: 'test')); var response = await client.get("$url/hello", headers: headers); expect(response.headers[HttpHeaders.WWW_AUTHENTICATE], equals('Basic realm="test"'));