From 5b7e017f31e941a7dc6cfb6422d4e5292b52ba5c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 23 Nov 2016 15:37:40 -0500 Subject: [PATCH] +10 --- .idea/angel_auth.iml | 1 + .idea/misc.xml | 10 -- .idea/runConfigurations/All_Tests.xml | 3 +- .idea/runConfigurations/Auth_Token_Tests.xml | 2 +- .idea/runConfigurations/Local_Tests.xml | 2 +- .travis.yml | 1 + README.md | 6 +- lib/src/middleware/require_auth.dart | 3 +- lib/src/plugin.dart | 93 ++++++++++----- lib/src/strategies/local.dart | 8 +- pubspec.yaml | 2 +- test/{all_tests.dart => all_tests.old.dart} | 4 +- .../{auth_token.dart => auth_token_test.dart} | 0 test/local.dart | 109 ----------------- test/local_test.dart | 111 ++++++++++++++++++ test/packages | 1 - 16 files changed, 195 insertions(+), 161 deletions(-) create mode 100644 .travis.yml rename test/{all_tests.dart => all_tests.old.dart} (93%) rename test/{auth_token.dart => auth_token_test.dart} (100%) delete mode 100644 test/local.dart create mode 100644 test/local_test.dart delete mode 120000 test/packages diff --git a/.idea/angel_auth.iml b/.idea/angel_auth.iml index e7d9f5eb..a928d34a 100644 --- a/.idea/angel_auth.iml +++ b/.idea/angel_auth.iml @@ -4,6 +4,7 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index eac82ac0..c65900a0 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -25,14 +25,4 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml index a824b209..ac11209e 100644 --- a/.idea/runConfigurations/All_Tests.xml +++ b/.idea/runConfigurations/All_Tests.xml @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/Auth_Token_Tests.xml b/.idea/runConfigurations/Auth_Token_Tests.xml index c72b7c75..342f9f6f 100644 --- a/.idea/runConfigurations/Auth_Token_Tests.xml +++ b/.idea/runConfigurations/Auth_Token_Tests.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/Local_Tests.xml b/.idea/runConfigurations/Local_Tests.xml index 26d8b79e..3279da4d 100644 --- a/.idea/runConfigurations/Local_Tests.xml +++ b/.idea/runConfigurations/Local_Tests.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/README.md b/README.md index 3fc8dfd7..3fd2c055 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # angel_auth + +![version 1.1.0-dev+10](https://img.shields.io/badge/version-1.1.0--dev+10-red.svg) +![build status](https://travis-ci.org/angel-dart/auth.svg?branch=master) + A complete authentication plugin for Angel. Inspired by Passport. # Documentation -Coming soon! +[Click here](https://github.com/angel-dart/auth/wiki). # Supported Strategies * Local (with and without Basic Auth) \ No newline at end of file diff --git a/lib/src/middleware/require_auth.dart b/lib/src/middleware/require_auth.dart index 02299f16..43f14580 100644 --- a/lib/src/middleware/require_auth.dart +++ b/lib/src/middleware/require_auth.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; @@ -16,7 +15,7 @@ class RequireAuthorizationMiddleware extends BaseMiddleware { return false; } - if (req.properties.containsKey('user')) + if (req.properties.containsKey('user') || req.method == 'OPTIONS') return true; else return _reject(res); diff --git a/lib/src/plugin.dart b/lib/src/plugin.dart index 8a70a7e2..ff7900f0 100644 --- a/lib/src/plugin.dart +++ b/lib/src/plugin.dart @@ -18,6 +18,8 @@ class AngelAuth extends AngelPlugin { final RegExp _rgxBearer = new RegExp(r"^Bearer"); RequireAuthorizationMiddleware _requireAuth = new RequireAuthorizationMiddleware(); + final bool allowCookie; + final bool allowTokenInQuery; String middlewareName; bool debug; bool enforceIp; @@ -40,6 +42,8 @@ class AngelAuth extends AngelPlugin { AngelAuth( {String jwtKey, num jwtLifeSpan, + this.allowCookie: true, + this.allowTokenInQuery: true, this.debug: false, this.enforceIp: true, this.middlewareName: 'auth', @@ -54,49 +58,74 @@ class AngelAuth extends AngelPlugin { app.container.singleton(this); if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth); - app.before.add(_decodeJwt); + app.before.add(decodeJwt); app.registerMiddleware(middlewareName, _requireAuth); if (reviveTokenEndpoint != null) { - app.post(reviveTokenEndpoint, _reviveJwt); + app.post(reviveTokenEndpoint, reviveJwt); } } - _decodeJwt(RequestContext req, ResponseContext res) async { + 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('Token revival endpoint accessed.'); + return await reviveJwt(req, res); } - String jwt = _getJwt(req); + 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 (req.ip != token.ipAddress) + 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}'); + } + + req.inject(AuthToken, req.properties['token'] = token); req.properties["user"] = await deserializer(token.userId); } return true; } - _getJwt(RequestContext req) { + getJwt(RequestContext req) { if (debug) { print('Attempting to parse JWT'); } @@ -106,39 +135,40 @@ class AngelAuth extends AngelPlugin { print('Found Auth header'); } - return req.headers - .value("Authorization") - .replaceAll(_rgxBearer, "") - .trim(); + final authHeader = req.headers.value("Authorization"); + + // Allow Basic auth to fall through + if (_rgxBearer.hasMatch(authHeader)) + return authHeader.replaceAll(_rgxBearer, "").trim(); } else if (req.cookies.any((cookie) => cookie.name == "token")) { 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']; } return null; } - _reviveJwt(RequestContext req, ResponseContext res) async { + reviveJwt(RequestContext req, ResponseContext res) async { try { - if (debug) - print('Attempting to revive JWT...'); + if (debug) print('Attempting to revive JWT...'); - var jwt = _getJwt(req); + var jwt = getJwt(req); - if (debug) - print('Found JWT: $jwt'); + 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 (debug) print('Validated and deserialized: $token'); if (enforceIp) { if (debug) - print('Token IP: ${token.ipAddress}. Current request sent from: ${req.ip}'); + print( + 'Token IP: ${token.ipAddress}. Current request sent from: ${req.ip}'); if (req.ip != token.ipAddress) throw new AngelHttpException.Forbidden( @@ -147,19 +177,21 @@ class AngelAuth extends AngelPlugin { if (token.lifeSpan > -1) { if (debug) { - print('Checking if token has expired... Life span is ${token.lifeSpan}'); + 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())) { - print('Token has indeed expired! Resetting assignment date to current timestamp...'); + print( + '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) { + } else if (debug) { print('This token never expires, so it is still valid.'); } @@ -193,9 +225,12 @@ class AngelAuth extends AngelPlugin { var userId = await serializer(result); // Create JWT - var jwt = new AuthToken(userId: userId, lifeSpan: _jwtLifeSpan) + var jwt = new AuthToken( + userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip) .serialize(_hs256); - req.cookies.add(new Cookie("token", jwt)); + req.inject(AuthToken, jwt); + + if (allowCookie) req.cookies.add(new Cookie("token", jwt)); if (req.headers.value("accept") != null && (req.headers.value("accept").contains("application/json") || diff --git a/lib/src/strategies/local.dart b/lib/src/strategies/local.dart index 3153d33a..19723285 100644 --- a/lib/src/strategies/local.dart +++ b/lib/src/strategies/local.dart @@ -21,8 +21,8 @@ class LocalAuthStrategy extends AuthStrategy { String usernameField; String passwordField; String invalidMessage; - bool allowBasic; - bool forceBasic; + final bool allowBasic; + final bool forceBasic; String realm; LocalAuthStrategy(LocalAuthVerifier this.verifier, @@ -32,7 +32,8 @@ class LocalAuthStrategy extends AuthStrategy { 'Please provide a valid username and password.', bool this.allowBasic: true, bool this.forceBasic: false, - String this.realm: 'Authentication is required.'}) {} + String this.realm: 'Authentication is required.'}) { + } @override Future canLogout(RequestContext req, ResponseContext res) async { @@ -47,6 +48,7 @@ class LocalAuthStrategy extends AuthStrategy { if (allowBasic) { String authHeader = req.headers.value(HttpHeaders.AUTHORIZATION) ?? ""; + if (_rgxBasic.hasMatch(authHeader)) { String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1); String authString = diff --git a/pubspec.yaml b/pubspec.yaml index d5d790eb..78222c14 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+9 +version: 1.0.0-dev+10 author: Tobe O homepage: https://github.com/angel-dart/angel_auth dependencies: diff --git a/test/all_tests.dart b/test/all_tests.old.dart similarity index 93% rename from test/all_tests.dart rename to test/all_tests.old.dart index da6c84e2..fbd6cb5f 100644 --- a/test/all_tests.dart +++ b/test/all_tests.old.dart @@ -3,8 +3,8 @@ 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 'auth_token.dart' as authToken; -import 'local.dart' as local; +import 'auth_token_test.dart' as authToken; +import 'local_test.dart' as local; wireAuth(Angel app) async { diff --git a/test/auth_token.dart b/test/auth_token_test.dart similarity index 100% rename from test/auth_token.dart rename to test/auth_token_test.dart diff --git a/test/local.dart b/test/local.dart deleted file mode 100644 index f31516a8..00000000 --- a/test/local.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_auth/angel_auth.dart'; -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'); -Map sampleUser = {'hello': 'world'}; - -verifier(username, 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; - - Auth.strategies.add(new LocalAuthStrategy(verifier)); - await app.configure(Auth); -} - -main() async { - group('local', () { - Angel app; - http.Client client; - String url; - String basicAuthUrl; - - 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', - middleware: [Auth.authenticate('local', localOpts)]); - app.get('/success', "yep", middleware: ['auth']); - app.get('/failure', "nope"); - - 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}"; - }); - - tearDown(() async { - await app.httpServer.close(force: true); - client = null; - url = null; - basicAuthUrl = null; - }); - - 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(403)); - }); - - test('successRedirect', () async { - 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), - headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType}); - print("Login response: ${response.body}"); - expect(response.headers[HttpHeaders.LOCATION], equals('/failure')); - expect(response.statusCode, equals(401)); - }); - - test('allow basic', () async { - String authString = BASE64.encode("username:password".runes.toList()); - var response = await client.get("$url/hello", - headers: {HttpHeaders.AUTHORIZATION: 'Basic $authString'}); - expect(response.body, equals('"Woo auth"')); - }); - - test('allow basic via URL encoding', () async { - var response = await client.get("$basicAuthUrl/hello"); - expect(response.body, equals('"Woo auth"')); - }); - - test('force basic', () async { - 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); - expect(response.headers[HttpHeaders.WWW_AUTHENTICATE], - equals('Basic realm="test"')); - }); - }); -} diff --git a/test/local_test.dart b/test/local_test.dart new file mode 100644 index 00000000..3d0e1473 --- /dev/null +++ b/test/local_test.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_auth/angel_auth.dart'; +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'); +Map sampleUser = {'hello': 'world'}; + +verifier(username, 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; + + Auth.strategies.add(new LocalAuthStrategy(verifier)); + await app.configure(Auth); +} + +main() async { + Angel app; + http.Client client; + String url; + String basicAuthUrl; + + setUp(() async { + client = new http.Client(); + app = new Angel(debug: true); + await app.configure(wireAuth); + app.get('/hello', 'Woo auth', middleware: [Auth.authenticate('local')]); + app.post('/login', 'This should not be shown', + middleware: [Auth.authenticate('local', localOpts)]); + app.get('/success', "yep", middleware: ['auth']); + app.get('/failure', "nope"); + + app + ..normalize() + ..dumpTree(); + + 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}"; + }); + + tearDown(() async { + await app.httpServer.close(force: true); + client = null; + url = null; + basicAuthUrl = null; + }); + + 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(403)); + }); + + test('successRedirect', () async { + 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), + headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType}); + print("Login response: ${response.body}"); + expect(response.headers[HttpHeaders.LOCATION], equals('/failure')); + expect(response.statusCode, equals(401)); + }); + + test('allow basic', () async { + String authString = BASE64.encode("username:password".runes.toList()); + var response = await client.get("$url/hello", + headers: {HttpHeaders.AUTHORIZATION: 'Basic $authString'}); + expect(response.body, equals('"Woo auth"')); + }); + + test('allow basic via URL encoding', () async { + var response = await client.get("$basicAuthUrl/hello"); + expect(response.body, equals('"Woo auth"')); + }); + + test('force basic', () async { + 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); + expect(response.headers[HttpHeaders.WWW_AUTHENTICATE], + equals('Basic realm="test"')); + }); +} diff --git a/test/packages b/test/packages deleted file mode 120000 index a16c4050..00000000 --- a/test/packages +++ /dev/null @@ -1 +0,0 @@ -../packages \ No newline at end of file