diff --git a/.idea/angel_auth.iml b/.idea/angel_auth.iml index 02bd9dfb..eae13016 100644 --- a/.idea/angel_auth.iml +++ b/.idea/angel_auth.iml @@ -5,9 +5,7 @@ - - diff --git a/README.md b/README.md index f8b1b222..2a68e513 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,15 @@ configureServer(Angel app) async { // If the last strategy throws an authentication failure, then // a `401 Not Authenticated` is thrown. var chainedHandler = auth.authenticate( - 'basic,facebook', + ['basic','facebook'], authOptions ); + + // Apply angel_auth-specific configuration + await app.configure(auth.configureServer); + + // Middleware to decode JWT's... + app.use(auth.decodeJwt); } ``` @@ -60,7 +66,7 @@ configureServer(Angel app) async { // If the last strategy throws an authentication failure, then // a `401 Not Authenticated` is thrown. var chainedHandler = auth.authenticate( - 'basic,facebook', + ['basic','facebook'], authOptions ); } diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..518eb901 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,2 @@ +analyzer: + strong-mode: true \ No newline at end of file diff --git a/lib/src/plugin.dart b/lib/src/plugin.dart index 5e4d7ae8..04460fb3 100644 --- a/lib/src/plugin.dart +++ b/lib/src/plugin.dart @@ -10,7 +10,7 @@ import 'options.dart'; import 'strategy.dart'; /// Handles authentication within an Angel application. -class AngelAuth extends AngelPlugin { +class AngelAuth { Hmac _hs256; num _jwtLifeSpan; final StreamController _onLogin = new StreamController(), @@ -27,8 +27,6 @@ class AngelAuth extends AngelPlugin { /// The name to register [requireAuth] as. Default: `auth`. String middlewareName; - bool debug; - /// If `true` (default), then JWT's will be considered invalid if used from a different IP than the first user's it was issued to. /// /// This is a security provision. Even if a user's JWT is stolen, a remote attacker will not be able to impersonate anyone. @@ -69,7 +67,6 @@ class AngelAuth extends AngelPlugin { num jwtLifeSpan, this.allowCookie: true, this.allowTokenInQuery: true, - this.debug: false, this.enforceIp: true, this.middlewareName: 'auth', this.reviveTokenEndpoint: "/auth/token"}) @@ -78,8 +75,7 @@ class AngelAuth extends AngelPlugin { _jwtLifeSpan = jwtLifeSpan ?? -1; } - @override - call(Angel app) async { + Future configureServer(Angel app) async { if (serializer == null) throw new StateError( 'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.'); @@ -90,14 +86,13 @@ class AngelAuth extends AngelPlugin { app.container.singleton(this); if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth); - app.before.add(decodeJwt); app.registerMiddleware(middlewareName, requireAuth); if (reviveTokenEndpoint != null) { app.post(reviveTokenEndpoint, reviveJwt); } - app.justBeforeStop.add((_) { + app.shutdownHooks.add((_) { _onLogin.close(); }); } @@ -109,56 +104,27 @@ class AngelAuth extends AngelPlugin { } /// A middleware that decodes a JWT from a request, and injects a corresponding user. - decodeJwt(RequestContext req, ResponseContext res) async { + Future decodeJwt(RequestContext req, ResponseContext res) async { if (req.method == "POST" && req.path == reviveTokenEndpoint) { - // Shouldn't block invalid JWT if we are reviving it - if (debug) print('Token revival endpoint accessed.'); return await reviveJwt(req, res); } - if (debug) { - print('Enforcing JWT authentication...'); - } - String jwt = getJwt(req); - if (debug) { - print('Found JWT: $jwt'); - } - if (jwt != null) { var token = new AuthToken.validate(jwt, _hs256); - if (debug) { - print('Decoded auth token: ${token.toJson()}'); - } - if (enforceIp) { - if (debug) { - print('Token IP: ${token.ipAddress}. Current request sent from: ${req - .ip}'); - } - if (req.ip != null && req.ip != token.ipAddress) throw new AngelHttpException.forbidden( message: "JWT cannot be accessed from this IP address."); } if (token.lifeSpan > -1) { - if (debug) { - print("Making sure this token hasn't already expired..."); - } - token.issuedAt.add(new Duration(milliseconds: token.lifeSpan)); if (!token.issuedAt.isAfter(new DateTime.now())) throw new AngelHttpException.forbidden(message: "Expired JWT."); - } else if (debug) { - print('This token has an infinite life span.'); - } - - if (debug) { - print('Now deserializing from this userId: ${token.userId}'); } final user = await deserializer(token.userId); @@ -169,16 +135,8 @@ class AngelAuth extends AngelPlugin { } /// Retrieves a JWT from a request, if any was sent at all. - getJwt(RequestContext req) { - if (debug) { - print('Attempting to parse JWT'); - } - + String getJwt(RequestContext req) { if (req.headers.value("Authorization") != null) { - if (debug) { - print('Found Auth header'); - } - final authHeader = req.headers.value("Authorization"); // Allow Basic auth to fall through @@ -186,7 +144,6 @@ class AngelAuth extends AngelPlugin { return authHeader.replaceAll(_rgxBearer, "").trim(); } else if (allowCookie && req.cookies.any((cookie) => cookie.name == "token")) { - if (debug) print('Request has "token" cookie...'); return req.cookies.firstWhere((cookie) => cookie.name == "token").value; } else if (allowTokenInQuery && req.query['token'] is String) { return req.query['token']; @@ -196,43 +153,25 @@ class AngelAuth extends AngelPlugin { } /// Attempts to revive an expired (or still alive) JWT. - reviveJwt(RequestContext req, ResponseContext res) async { + Future> reviveJwt(RequestContext req, ResponseContext res) async { try { - if (debug) print('Attempting to revive JWT...'); - var jwt = getJwt(req); if (jwt == null) { var body = await req.lazyBody(); jwt = body['token']; } - - if (debug) print('Found JWT: $jwt'); - if (jwt == null) { throw new AngelHttpException.forbidden(message: "No JWT provided"); } else { var token = new AuthToken.validate(jwt, _hs256); - - if (debug) print('Validated and deserialized: $token'); - if (enforceIp) { - if (debug) - print( - 'Token IP: ${token.ipAddress}. Current request sent from: ${req - .ip}'); - if (req.ip != token.ipAddress) throw new AngelHttpException.forbidden( message: "JWT cannot be accessed from this IP address."); } if (token.lifeSpan > -1) { - if (debug) { - print('Checking if token has expired... Life span is ${token - .lifeSpan}'); - } - token.issuedAt.add(new Duration(milliseconds: token.lifeSpan)); if (!token.issuedAt.isAfter(new DateTime.now())) { @@ -240,15 +179,7 @@ class AngelAuth extends AngelPlugin { 'Token has indeed expired! Resetting assignment date to current timestamp...'); // Extend its lifespan by changing iat token.issuedAt = new DateTime.now(); - } else if (debug) { - print('Token has not expired yet.'); } - } else if (debug) { - print('This token never expires, so it is still valid.'); - } - - if (debug) { - print('Final, valid token: ${token.toJson()}'); } if (allowCookie) @@ -257,13 +188,7 @@ class AngelAuth extends AngelPlugin { final data = await deserializer(token.userId); return {'data': data, 'token': token.serialize(_hs256)}; } - } catch (e, st) { - if (debug) { - print('An error occurred while reviving this token.'); - print(e); - print(st); - } - + } catch (e) { if (e is AngelHttpException) rethrow; throw new AngelHttpException.badRequest(message: "Malformed JWT"); } @@ -271,19 +196,25 @@ class AngelAuth extends AngelPlugin { /// Attempts to authenticate a user using one or more strategies. /// - /// [type] is a comma-separated list of strategy names to try. + /// [type] is a strategy name to try, or a `List` of such. /// /// If a strategy returns `null` or `false`, either the next one is tried, /// or a `401 Not Authenticated` is thrown, if it is the last one. /// /// Any other result is considered an authenticated user, and terminates the loop. - authenticate(String type, [AngelAuthOptions options]) { + RequestHandler authenticate(type, [AngelAuthOptions options]) { return (RequestContext req, ResponseContext res) async { - var names = type - .split(',') - .map((s) => s.trim()) - .where((String s) => s.isNotEmpty) - .toList(); + List names = []; + var arr = type is Iterable ? type.toList() : [type]; + + for (var t in arr) { + var n = t + .split(',') + .map((s) => s.trim()) + .where((String s) => s.isNotEmpty) + .toList(); + names.addAll(n); + } for (int i = 0; i < names.length; i++) { var name = names[i]; @@ -368,14 +299,14 @@ class AngelAuth extends AngelPlugin { } /// Log an authenticated user out. - logout([AngelAuthOptions options]) { + RequestMiddleware 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); + res.redirect(options.failureRedirect); } return false; @@ -394,7 +325,7 @@ class AngelAuth extends AngelPlugin { if (options != null && options.successRedirect != null && options.successRedirect.isNotEmpty) { - return res.redirect(options.successRedirect); + res.redirect(options.successRedirect); } return true; diff --git a/pubspec.yaml b/pubspec.yaml index 42f728cf..def61a6b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: angel_auth description: A complete authentication plugin for Angel. -version: 1.0.5 +version: 1.1.0-alpha author: Tobe O homepage: https://github.com/angel-dart/angel_auth environment: sdk: ">=1.19.0" dependencies: - angel_framework: ^1.0.0-dev + angel_framework: ^1.1.0-alpha crypto: ^2.0.0 dev_dependencies: http: ^0.11.0 diff --git a/test/callback_test.dart b/test/callback_test.dart index ac405427..654ea123 100644 --- a/test/callback_test.dart +++ b/test/callback_test.dart @@ -30,7 +30,8 @@ main() { auth.serializer = (User user) async => user.id; auth.deserializer = app.service('users').read; - await app.configure(auth); + await app.configure(auth.configureServer); + app.use(auth.decodeJwt); auth.strategies.add(new LocalAuthStrategy((username, password) async { final List users = await app.service('users').index(); diff --git a/test/local_test.dart b/test/local_test.dart index ffae76d7..8c611a96 100644 --- a/test/local_test.dart +++ b/test/local_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; @@ -5,25 +6,26 @@ import 'package:angel_auth/angel_auth.dart'; import 'package:http/http.dart' as http; import 'package:test/test.dart'; -final AngelAuth Auth = new AngelAuth(); +final AngelAuth auth = new AngelAuth(); Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType}; AngelAuthOptions localOpts = new AngelAuthOptions( failureRedirect: '/failure', successRedirect: '/success'); Map sampleUser = {'hello': 'world'}; -verifier(username, password) async { +Future verifier(String username, String password) async { if (username == 'username' && password == 'password') { return sampleUser; } else return false; } -wireAuth(Angel app) async { - Auth.serializer = (user) async => 1337; - Auth.deserializer = (id) async => sampleUser; +Future wireAuth(Angel app) async { + auth.serializer = (user) async => 1337; + auth.deserializer = (id) async => sampleUser; - Auth.strategies.add(new LocalAuthStrategy(verifier)); - await app.configure(Auth); + auth.strategies.add(new LocalAuthStrategy(verifier)); + await app.configure(auth.configureServer); + app.use(auth.decodeJwt); } main() async { @@ -34,11 +36,11 @@ main() async { setUp(() async { client = new http.Client(); - app = new Angel(debug: true); + app = new Angel(); await app.configure(wireAuth); - app.get('/hello', 'Woo auth', middleware: [Auth.authenticate('local')]); + app.get('/hello', 'Woo auth', middleware: [auth.authenticate('local')]); app.post('/login', 'This should not be shown', - middleware: [Auth.authenticate('local', localOpts)]); + middleware: [auth.authenticate('local', localOpts)]); app.get('/success', "yep", middleware: ['auth']); app.get('/failure', "nope"); @@ -95,8 +97,8 @@ main() async { }); test('force basic', () async { - Auth.strategies.clear(); - Auth.strategies + auth.strategies.clear(); + auth.strategies .add(new LocalAuthStrategy(verifier, forceBasic: true, realm: 'test')); var response = await client.get("$url/hello", headers: headers); print(response.headers);