My work is cut out for me.

This commit is contained in:
regiostech 2016-05-03 00:13:19 -04:00
parent ff1368c789
commit 21f9181dc0
18 changed files with 770 additions and 0 deletions

60
.gitignore vendored
View file

@ -25,3 +25,63 @@ doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
# Directory created by dartdoc
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)

View file

@ -0,0 +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" />
<method />
</configuration>
</component>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

49
lib/angel_auth.dart Normal file
View file

@ -0,0 +1,49 @@
library angel_auth;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
part 'strategy.dart';
part 'middleware/require_auth.dart';
part 'middleware/serialization.dart';
part 'strategies/local.dart';
_validateString(String str) {
return str != null && str.isNotEmpty;
}
class Auth {
static List<AuthStrategy> strategies = [];
static UserSerializer serializer;
static UserDeserializer deserializer;
call(Angel app) async {
app.registerMiddleware('auth', requireAuth);
app.before.add(_serializationMiddleware);
}
static authenticate(String type, [Map 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 ?? {});
if (result is bool)
return result;
else {
req.session['userId'] = await serializer(result);
return true;
}
};
}
}
/// Configures an app to use angel_auth. :)
Future AngelAuth(Angel app) async {
await app.configure(new Auth());
}

View file

@ -0,0 +1,10 @@
part of angel_auth;
/// Restricts access to a resource via authentication.
Future<bool> requireAuth(RequestContext req, ResponseContext res,
{bool throws: true}) async {
if (req.session.containsKey('userId'))
return true;
else if (throws) throw new AngelHttpException.NotAuthenticated();
else return false;
}

View file

@ -0,0 +1,15 @@
part of angel_auth;
/// Serializes a user to the session.
typedef Future UserSerializer(user);
/// Deserializes a user from the session.
typedef Future UserDeserializer(userId);
_serializationMiddleware(RequestContext req, ResponseContext res) async {
if (await requireAuth(req, res, throws: false)) {
req.properties['user'] = await Auth.deserializer(req.session['userId']);
}
return true;
}

84
lib/strategies/local.dart Normal file
View file

@ -0,0 +1,84 @@
part of angel_auth;
/// Determines the validity of an incoming username and password.
typedef Future LocalAuthVerifier(String username, String password);
class LocalAuthStrategy extends AuthStrategy {
RegExp _rgxBasic = new RegExp(r'^Basic (.+)$', caseSensitive: false);
RegExp _rgxUsrPass = new RegExp(r'^([^:]+):(.+)$');
@override
String name = 'local';
LocalAuthVerifier verifier;
String usernameField;
String passwordField;
String invalidMessage;
bool allowBasic;
bool forceBasic;
String basicRealm;
LocalAuthStrategy(LocalAuthVerifier this.verifier,
{String this.usernameField: 'username',
String this.passwordField: 'password',
String this.invalidMessage:
'Please provide a valid username and password.',
bool this.allowBasic: true,
bool this.forceBasic: false,
String this.basicRealm: 'Authentication is required.'}) {}
@override
Future<bool> canLogout(RequestContext req, ResponseContext res) async {
return true;
}
@override
Future<bool> authenticate(RequestContext req, ResponseContext res,
{Map options: const {}}) async {
var verificationResult;
if (allowBasic) {
String authHeader = req.headers.value(HttpHeaders.AUTHORIZATION) ?? "";
if (_rgxBasic.hasMatch(authHeader)) {
String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1);
String authString = new String.fromCharCodes(
BASE64.decode(base64AuthString));
if (_rgxUsrPass.hasMatch(authString)) {
Match usrPassMatch = _rgxUsrPass.firstMatch(authString);
verificationResult =
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
} else throw new AngelHttpException.BadRequest(
errors: [invalidMessage]);
}
}
if (verificationResult == null) {
if (_validateString(req.body[usernameField]) &&
_validateString(req.body[passwordField])) {
verificationResult =
await verifier(req.body[usernameField], req.body[passwordField]);
}
}
if (verificationResult == false || verificationResult == null) {
if (options.containsKey('failureRedirect')) {
return res.redirect(
options['failureRedirect'], code: HttpStatus.UNAUTHORIZED);
}
if (forceBasic) {
res
..status(401)
..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$basicRealm"')
..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);
}
return true;
}
}

12
lib/strategy.dart Normal file
View file

@ -0,0 +1,12 @@
part of angel_auth;
/// A function that handles login and signup for an Angel application.
abstract class AuthStrategy {
String name;
/// Authenticates or rejects an incoming user.
Future authenticate(RequestContext req, ResponseContext res, {Map options: const {}});
/// Determines whether a signed-in user can log out or not.
Future<bool> canLogout(RequestContext req, ResponseContext res);
}

10
pubspec.yaml Normal file
View file

@ -0,0 +1,10 @@
name: angel_auth
description: A complete authentication plugin for Angel.
version: 1.0.0-dev
author: thosakwe <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_auth
dependencies:
angel_framework: ">=0.0.0-dev < 0.1.0"
dev_dependencies:
http: ">= 0.11.3 < 0.12.0"
test: ">= 0.12.13 < 0.13.0"

52
test/digest.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('digest', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow digest', () async {
});
test('force digest', () async {
});
});
}

52
test/everything.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('everything', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow everything', () async {
});
test('force everything', () async {
});
});
}

102
test/local.dart Normal file
View file

@ -0,0 +1,102 @@
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';
Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType};
Map localOpts = {'failureRedirect': '/failure'};
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(AngelAuth);
}
main() async {
group
('local', () {
Angel app;
http.Client client;
String url;
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: []);
app.get('/failure', "nope");
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
var response = await client.get("$url/success");
expect(response.statusCode, equals(401));
});
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});
expect(response.statusCode, equals(401));
expect(response.headers[HttpHeaders.LOCATION], equals('/failure'));
});
test('allow basic', () async {
String authString = BASE64.encode("username:password".runes.toList());
Map auth = {HttpHeaders.AUTHORIZATION: 'Basic $authString'};
var response = await client.get(
"$url/hello", headers: mergeMap([auth, headers]));
expect(response.body, equals('"Woo auth"'));
});
test('force basic', () async {
Auth.strategies.clear();
Auth.strategies.add(new LocalAuthStrategy(
verifier, forceBasic: true, basicRealm: 'test'));
var response = await client.get("$url/hello", headers: headers);
expect(response.headers[HttpHeaders.WWW_AUTHENTICATE],
equals('Basic realm="test"'));
});
});
}

52
test/oauth1.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('oauth1', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow oauth1', () async {
});
test('force oauth1', () async {
});
});
}

52
test/oauth2.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('oauth2', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow oauth2', () async {
});
test('force oauth2', () async {
});
});
}

52
test/oauth_in_house.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('oauth2 in-house', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow oauth2 in-house', () async {
});
test('force oauth2 in-house', () async {
});
});
}

52
test/openid.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('openid', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow openid', () async {
});
test('force openid', () async {
});
});
}

52
test/token.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('token', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow token', () async {
});
test('force token', () async {
});
});
}

52
test/websocket.dart Normal file
View file

@ -0,0 +1,52 @@
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:test/test.dart';
wireAuth(Angel app) async {
}
main() async {
group('websocket', () {
Angel app;
http.Client client;
String url;
setUp(() async {
client = new http.Client();
app = new Angel();
await app.configure(wireAuth);
HttpServer server = await app.startServer(
InternetAddress.LOOPBACK_IP_V4, 0);
url = "http://${server.address.host}:${server.port}";
});
tearDown(() async {
await app.httpServer.close(force: true);
client = null;
url = null;
});
test('can use login as middleware', () async {
});
test('successRedirect', () async {
});
test('failureRedirect', () async {
});
test('allow websocket', () async {
});
test('force websocket', () async {
});
});
}