This commit is contained in:
thosakwe 2016-11-23 15:37:40 -05:00
parent afb554fba0
commit 5b7e017f31
16 changed files with 195 additions and 161 deletions

View file

@ -4,6 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" /> <excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" /> <excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/packages" /> <excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/temp" /> <excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/test/packages" /> <excludeFolder url="file://$MODULE_DIR$/test/packages" />

View file

@ -25,14 +25,4 @@
</profile-state> </profile-state>
</entry> </entry>
</component> </component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
</project> </project>

View file

@ -1,6 +1,7 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true"> <configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/all_tests.dart" /> <option name="filePath" value="$PROJECT_DIR$/test" />
<option name="scope" value="FOLDER" />
<method /> <method />
</configuration> </configuration>
</component> </component>

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Auth Token Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true"> <configuration default="false" name="Auth Token Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/auth_token.dart" /> <option name="filePath" value="$PROJECT_DIR$/test/auth_token_test.dart" />
<method /> <method />
</configuration> </configuration>
</component> </component>

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Local Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true"> <configuration default="false" name="Local Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/local.dart" /> <option name="filePath" value="$PROJECT_DIR$/test/local_test.dart" />
<method /> <method />
</configuration> </configuration>
</component> </component>

1
.travis.yml Normal file
View file

@ -0,0 +1 @@
language: dart

View file

@ -1,8 +1,12 @@
# angel_auth # 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. A complete authentication plugin for Angel. Inspired by Passport.
# Documentation # Documentation
Coming soon! [Click here](https://github.com/angel-dart/auth/wiki).
# Supported Strategies # Supported Strategies
* Local (with and without Basic Auth) * Local (with and without Basic Auth)

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
@ -16,7 +15,7 @@ class RequireAuthorizationMiddleware extends BaseMiddleware {
return false; return false;
} }
if (req.properties.containsKey('user')) if (req.properties.containsKey('user') || req.method == 'OPTIONS')
return true; return true;
else else
return _reject(res); return _reject(res);

View file

@ -18,6 +18,8 @@ class AngelAuth extends AngelPlugin {
final RegExp _rgxBearer = new RegExp(r"^Bearer"); final RegExp _rgxBearer = new RegExp(r"^Bearer");
RequireAuthorizationMiddleware _requireAuth = RequireAuthorizationMiddleware _requireAuth =
new RequireAuthorizationMiddleware(); new RequireAuthorizationMiddleware();
final bool allowCookie;
final bool allowTokenInQuery;
String middlewareName; String middlewareName;
bool debug; bool debug;
bool enforceIp; bool enforceIp;
@ -40,6 +42,8 @@ class AngelAuth extends AngelPlugin {
AngelAuth( AngelAuth(
{String jwtKey, {String jwtKey,
num jwtLifeSpan, num jwtLifeSpan,
this.allowCookie: true,
this.allowTokenInQuery: true,
this.debug: false, this.debug: false,
this.enforceIp: true, this.enforceIp: true,
this.middlewareName: 'auth', this.middlewareName: 'auth',
@ -54,49 +58,74 @@ class AngelAuth extends AngelPlugin {
app.container.singleton(this); app.container.singleton(this);
if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth); if (runtimeType != AngelAuth) app.container.singleton(this, as: AngelAuth);
app.before.add(_decodeJwt); app.before.add(decodeJwt);
app.registerMiddleware(middlewareName, _requireAuth); app.registerMiddleware(middlewareName, _requireAuth);
if (reviveTokenEndpoint != null) { 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) { if (req.method == "POST" && req.path == reviveTokenEndpoint) {
// Shouldn't block invalid JWT if we are reviving it // Shouldn't block invalid JWT if we are reviving it
if (debug) print('Token revival endpoint accessed.');
if (debug) return await reviveJwt(req, res);
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) { if (jwt != null) {
var token = new AuthToken.validate(jwt, _hs256); var token = new AuthToken.validate(jwt, _hs256);
if (debug) {
print('Decoded auth token: ${token.toJson()}');
}
if (enforceIp) { 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( throw new AngelHttpException.Forbidden(
message: "JWT cannot be accessed from this IP address."); message: "JWT cannot be accessed from this IP address.");
} }
if (token.lifeSpan > -1) { if (token.lifeSpan > -1) {
if (debug) {
print("Making sure this token hasn't already expired...");
}
token.issuedAt.add(new Duration(milliseconds: token.lifeSpan)); token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
if (!token.issuedAt.isAfter(new DateTime.now())) if (!token.issuedAt.isAfter(new DateTime.now()))
throw new AngelHttpException.Forbidden(message: "Expired JWT."); 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); req.properties["user"] = await deserializer(token.userId);
} }
return true; return true;
} }
_getJwt(RequestContext req) { getJwt(RequestContext req) {
if (debug) { if (debug) {
print('Attempting to parse JWT'); print('Attempting to parse JWT');
} }
@ -106,39 +135,40 @@ class AngelAuth extends AngelPlugin {
print('Found Auth header'); print('Found Auth header');
} }
return req.headers final authHeader = req.headers.value("Authorization");
.value("Authorization")
.replaceAll(_rgxBearer, "") // Allow Basic auth to fall through
.trim(); if (_rgxBearer.hasMatch(authHeader))
return authHeader.replaceAll(_rgxBearer, "").trim();
} else if (req.cookies.any((cookie) => cookie.name == "token")) { } else if (req.cookies.any((cookie) => cookie.name == "token")) {
print('Request has "token" cookie...'); print('Request has "token" cookie...');
return req.cookies.firstWhere((cookie) => cookie.name == "token").value; return req.cookies.firstWhere((cookie) => cookie.name == "token").value;
} else if (allowTokenInQuery && req.query['token'] is String) {
return req.query['token'];
} }
return null; return null;
} }
_reviveJwt(RequestContext req, ResponseContext res) async { reviveJwt(RequestContext req, ResponseContext res) async {
try { try {
if (debug) if (debug) print('Attempting to revive JWT...');
print('Attempting to revive JWT...');
var jwt = _getJwt(req); var jwt = getJwt(req);
if (debug) if (debug) print('Found JWT: $jwt');
print('Found JWT: $jwt');
if (jwt == null) { if (jwt == null) {
throw new AngelHttpException.Forbidden(message: "No JWT provided"); throw new AngelHttpException.Forbidden(message: "No JWT provided");
} else { } else {
var token = new AuthToken.validate(jwt, _hs256); var token = new AuthToken.validate(jwt, _hs256);
if (debug) if (debug) print('Validated and deserialized: $token');
print('Validated and deserialized: $token');
if (enforceIp) { if (enforceIp) {
if (debug) 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) if (req.ip != token.ipAddress)
throw new AngelHttpException.Forbidden( throw new AngelHttpException.Forbidden(
@ -147,19 +177,21 @@ class AngelAuth extends AngelPlugin {
if (token.lifeSpan > -1) { if (token.lifeSpan > -1) {
if (debug) { 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)); token.issuedAt.add(new Duration(milliseconds: token.lifeSpan));
if (!token.issuedAt.isAfter(new DateTime.now())) { 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 // Extend its lifespan by changing iat
token.issuedAt = new DateTime.now(); token.issuedAt = new DateTime.now();
} else if (debug) { } else if (debug) {
print('Token has not expired yet.'); print('Token has not expired yet.');
} }
} else if(debug) { } else if (debug) {
print('This token never expires, so it is still valid.'); print('This token never expires, so it is still valid.');
} }
@ -193,9 +225,12 @@ class AngelAuth extends AngelPlugin {
var userId = await serializer(result); var userId = await serializer(result);
// Create JWT // Create JWT
var jwt = new AuthToken(userId: userId, lifeSpan: _jwtLifeSpan) var jwt = new AuthToken(
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip)
.serialize(_hs256); .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 && if (req.headers.value("accept") != null &&
(req.headers.value("accept").contains("application/json") || (req.headers.value("accept").contains("application/json") ||

View file

@ -21,8 +21,8 @@ class LocalAuthStrategy extends AuthStrategy {
String usernameField; String usernameField;
String passwordField; String passwordField;
String invalidMessage; String invalidMessage;
bool allowBasic; final bool allowBasic;
bool forceBasic; final bool forceBasic;
String realm; String realm;
LocalAuthStrategy(LocalAuthVerifier this.verifier, LocalAuthStrategy(LocalAuthVerifier this.verifier,
@ -32,7 +32,8 @@ class LocalAuthStrategy extends AuthStrategy {
'Please provide a valid username and password.', 'Please provide a valid username and password.',
bool this.allowBasic: true, bool this.allowBasic: true,
bool this.forceBasic: false, bool this.forceBasic: false,
String this.realm: 'Authentication is required.'}) {} String this.realm: 'Authentication is required.'}) {
}
@override @override
Future<bool> canLogout(RequestContext req, ResponseContext res) async { Future<bool> canLogout(RequestContext req, ResponseContext res) async {
@ -47,6 +48,7 @@ class LocalAuthStrategy extends AuthStrategy {
if (allowBasic) { if (allowBasic) {
String authHeader = req.headers.value(HttpHeaders.AUTHORIZATION) ?? ""; String authHeader = req.headers.value(HttpHeaders.AUTHORIZATION) ?? "";
if (_rgxBasic.hasMatch(authHeader)) { if (_rgxBasic.hasMatch(authHeader)) {
String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1); String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1);
String authString = String authString =

View file

@ -1,6 +1,6 @@
name: angel_auth name: angel_auth
description: A complete authentication plugin for Angel. description: A complete authentication plugin for Angel.
version: 1.0.0-dev+9 version: 1.0.0-dev+10
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_auth homepage: https://github.com/angel-dart/angel_auth
dependencies: dependencies:

View file

@ -3,8 +3,8 @@ import 'package:angel_framework/angel_framework.dart';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel_auth/angel_auth.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'auth_token.dart' as authToken; import 'auth_token_test.dart' as authToken;
import 'local.dart' as local; import 'local_test.dart' as local;
wireAuth(Angel app) async { wireAuth(Angel app) async {

View file

@ -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"'));
});
});
}

111
test/local_test.dart Normal file
View file

@ -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"'));
});
}

View file

@ -1 +0,0 @@
../packages