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$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/test/packages" />

View file

@ -25,14 +25,4 @@
</profile-state>
</entry>
</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>

View file

@ -1,6 +1,7 @@
<component name="ProjectRunConfigurationManager">
<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 />
</configuration>
</component>

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<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 />
</configuration>
</component>

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<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 />
</configuration>
</component>

1
.travis.yml Normal file
View file

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

View file

@ -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)

View file

@ -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);

View file

@ -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") ||

View file

@ -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<bool> 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 =

View file

@ -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 <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_auth
dependencies:

View file

@ -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 {

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