Local done, OAuth2 started

This commit is contained in:
regiostech 2016-05-09 16:47:28 -04:00
parent dc9c39fc89
commit 5faaceefa4
8 changed files with 138 additions and 22 deletions

View file

@ -4,6 +4,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:crypto/crypto.dart' as Crypto;
import 'package:oauth2/oauth2.dart' as Oauth2;
part 'strategy.dart'; part 'strategy.dart';
@ -13,10 +15,15 @@ part 'middleware/serialization.dart';
part 'strategies/local.dart'; part 'strategies/local.dart';
part 'strategies/oauth2.dart';
_validateString(String str) { _validateString(String str) {
return str != null && str.isNotEmpty; return str != null && str.isNotEmpty;
} }
const String FAILURE_REDIRECT = 'failureRedirect';
const String SUCCESS_REDIRECT = 'successRedirect';
class Auth { class Auth {
static List<AuthStrategy> strategies = []; static List<AuthStrategy> strategies = [];
static UserSerializer serializer; static UserSerializer serializer;
@ -27,16 +34,14 @@ class Auth {
app.before.add(_serializationMiddleware); app.before.add(_serializationMiddleware);
} }
static authenticate(String type, [Map options]) { static authenticate(String type, [AngelAuthOptions options]) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
AuthStrategy strategy = AuthStrategy strategy =
strategies.firstWhere((AuthStrategy x) => x.name == type); strategies.firstWhere((AuthStrategy x) => x.name == type);
var result = await strategy.authenticate( var result = await strategy.authenticate(req, res, options);
req, res, options: options ?? {});
print("${req.path} -> $result");
if (result == true) if (result == true)
return result; return result;
else if(result != false) { else if (result != false) {
req.session['userId'] = await serializer(result); req.session['userId'] = await serializer(result);
return true; return true;
} else { } else {
@ -44,6 +49,39 @@ class Auth {
} }
}; };
} }
static logout([AngelAuthOptions options]) {
return (RequestContext req, ResponseContext res) async {
for (AuthStrategy strategy in Auth.strategies) {
if (!(await strategy.canLogout(req, res))) {
if (options != null &&
options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) {
return res.redirect(options.failureRedirect);
}
return false;
}
}
req.session.remove('userId');
if (options != null &&
options.successRedirect != null &&
options.successRedirect.isNotEmpty) {
return res.redirect(options.successRedirect);
}
return true;
};
}
}
class AngelAuthOptions {
String successRedirect;
String failureRedirect;
AngelAuthOptions({String this.successRedirect, String this.failureRedirect});
} }
/// Configures an app to use angel_auth. :) /// Configures an app to use angel_auth. :)

View file

@ -5,6 +5,9 @@ Future<bool> requireAuth(RequestContext req, ResponseContext res,
{bool throws: true}) async { {bool throws: true}) async {
if (req.session.containsKey('userId')) if (req.session.containsKey('userId'))
return true; return true;
else if (throws) throw new AngelHttpException.NotAuthenticated(); else if (throws) {
res.status(HttpStatus.UNAUTHORIZED);
throw new AngelHttpException.NotAuthenticated();
}
else return false; else return false;
} }

View file

@ -15,7 +15,7 @@ class LocalAuthStrategy extends AuthStrategy {
String invalidMessage; String invalidMessage;
bool allowBasic; bool allowBasic;
bool forceBasic; bool forceBasic;
String basicRealm; String realm;
LocalAuthStrategy(LocalAuthVerifier this.verifier, LocalAuthStrategy(LocalAuthVerifier this.verifier,
{String this.usernameField: 'username', {String this.usernameField: 'username',
@ -24,7 +24,7 @@ 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.basicRealm: '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 {
@ -33,7 +33,8 @@ class LocalAuthStrategy extends AuthStrategy {
@override @override
Future<bool> authenticate(RequestContext req, ResponseContext res, Future<bool> authenticate(RequestContext req, ResponseContext res,
{Map options: const {}}) async { [AngelAuthOptions options_]) async {
AngelAuthOptions options = options_ ?? new AngelAuthOptions();
var verificationResult; var verificationResult;
if (allowBasic) { if (allowBasic) {
@ -60,23 +61,24 @@ class LocalAuthStrategy extends AuthStrategy {
} }
if (verificationResult == false || verificationResult == null) { if (verificationResult == false || verificationResult == null) {
if (options.containsKey('failureRedirect')) { if (options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) {
return res.redirect( return res.redirect(
options['failureRedirect'], code: HttpStatus.UNAUTHORIZED); options.failureRedirect, code: HttpStatus.UNAUTHORIZED);
} }
if (forceBasic) { if (forceBasic) {
res res
..status(401) ..status(401)
..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$basicRealm"') ..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$realm"')
..end(); ..end();
return false; return false;
} else throw new AngelHttpException.NotAuthenticated(); } else throw new AngelHttpException.NotAuthenticated();
} }
req.session['user'] = await Auth.serializer(verificationResult); req.session['user'] = await Auth.serializer(verificationResult);
if (options.containsKey('successRedirect')) { if (options.successRedirect != null && options.successRedirect.isNotEmpty) {
return res.redirect(options['successRedirect'], code: HttpStatus.OK); return res.redirect(options.successRedirect, code: HttpStatus.OK);
} }
return true; return true;

View file

@ -0,0 +1,65 @@
part of angel_auth;
/// Logs a user in based on an incoming OAuth access and refresh token.
typedef Future OAuth2AuthVerifier(String accessToken, String refreshToken,
Map profile);
class OAuth2AuthStrategy extends AuthStrategy {
@override
String name = "oauth2";
OAuth2AuthVerifier verifier;
Uri authEndPoint;
Uri tokenEndPoint;
String clientId;
String clientSecret;
Uri callbackUri;
List<String> scopes;
@override
Future authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options_]) async {
Oauth2.Client client = await makeGrant().handleAuthorizationResponse(req.query);
// Remember: Do stuff
}
@override
Future<bool> canLogout(RequestContext req, ResponseContext res) async {
return true;
}
OAuth2AuthStrategy(String this.name, OAuth2AuthVerifier this.verifier,
{Uri this.authEndPoint,
Uri this.tokenEndPoint,
String this.clientId,
String this.clientSecret,
Uri this.callbackUri,
List<String> this.scopes: const[]}) {
if (this.authEndPoint == null)
throw new ArgumentError.notNull('authEndPoint');
if (this.tokenEndPoint == null)
throw new ArgumentError.notNull('tokenEndPoint');
if (this.clientId == null || this.clientId.isEmpty)
throw new ArgumentError.notNull('clientId');
}
call(RequestContext req, ResponseContext res) async {
var grant = makeGrant();
Uri to = grant.getAuthorizationUrl(callbackUri, scopes: scopes);
return res.redirect(to.path);
}
Oauth2.AuthorizationCodeGrant makeGrant() {
return new Oauth2.AuthorizationCodeGrant(
clientId, authEndPoint, tokenEndPoint, secret: clientSecret);
}
}
class OAuth2AuthorizationError extends AngelHttpException {
OAuth2AuthorizationError({String message: "OAuth2 Authorization Error",
List<String> errors: const []})
: super.NotAuthenticated(message: message) {
this.errors = errors;
}
}

View file

@ -5,7 +5,7 @@ abstract class AuthStrategy {
String name; String name;
/// Authenticates or rejects an incoming user. /// Authenticates or rejects an incoming user.
Future authenticate(RequestContext req, ResponseContext res, {Map options: const {}}); Future authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options]);
/// Determines whether a signed-in user can log out or not. /// Determines whether a signed-in user can log out or not.
Future<bool> canLogout(RequestContext req, ResponseContext res); Future<bool> canLogout(RequestContext req, ResponseContext res);

View file

@ -5,6 +5,8 @@ author: thosakwe <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_auth homepage: https://github.com/angel-dart/angel_auth
dependencies: dependencies:
angel_framework: ">=0.0.0-dev < 0.1.0" angel_framework: ">=0.0.0-dev < 0.1.0"
crypto: ">= 1.1.1 < 2.0.0"
oauth2: ">= 1.0.2 < 2.0.0"
dev_dependencies: dev_dependencies:
http: ">= 0.11.3 < 0.12.0" http: ">= 0.11.3 < 0.12.0"
test: ">= 0.12.13 < 0.13.0" test: ">= 0.12.13 < 0.13.0"

View file

@ -48,5 +48,9 @@ main() async {
test('force everything', () async { test('force everything', () async {
}); });
test('logout', () async {
});
}); });
} }

View file

@ -7,7 +7,10 @@ import 'package:merge_map/merge_map.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType}; Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType};
Map localOpts = {'failureRedirect': '/failure'}; AngelAuthOptions localOpts = new AngelAuthOptions(
failureRedirect: '/failure',
successRedirect: '/success'
);
Map sampleUser = {'hello': 'world'}; Map sampleUser = {'hello': 'world'};
verifier(username, password) async { verifier(username, password) async {
@ -35,7 +38,6 @@ main() async {
setUp(() async { setUp(() async {
client = new http.Client(); client = new http.Client();
app = new Angel(); app = new Angel();
await app.configure(wireAuth); 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', app.post('/login', 'This should not be shown',
@ -58,7 +60,8 @@ main() async {
}); });
test('can use login as middleware', () async { test('can use login as middleware', () async {
var response = await client.get("$url/success", headers: {'Accept': 'application/json'}); var response = await client.get(
"$url/success", headers: {'Accept': 'application/json'});
print(response.body); print(response.body);
expect(response.statusCode, equals(401)); expect(response.statusCode, equals(401));
}); });
@ -96,15 +99,14 @@ main() async {
}); });
test('allow basic via URL encoding', () async { test('allow basic via URL encoding', () async {
var response = await client.get( var response = await client.get("$basicAuthUrl/hello", headers: headers);
basicAuthUrl, headers: headers);
expect(response.body, equals('"Woo auth"')); expect(response.body, equals('"Woo auth"'));
}); });
test('force basic', () async { test('force basic', () async {
Auth.strategies.clear(); Auth.strategies.clear();
Auth.strategies.add(new LocalAuthStrategy( Auth.strategies.add(new LocalAuthStrategy(
verifier, forceBasic: true, basicRealm: 'test')); verifier, forceBasic: true, realm: 'test'));
var response = await client.get("$url/hello", headers: headers); var response = await client.get("$url/hello", headers: headers);
expect(response.headers[HttpHeaders.WWW_AUTHENTICATE], expect(response.headers[HttpHeaders.WWW_AUTHENTICATE],
equals('Basic realm="test"')); equals('Basic realm="test"'));