Local done, OAuth2 started
This commit is contained in:
parent
dc9c39fc89
commit
5faaceefa4
8 changed files with 138 additions and 22 deletions
|
@ -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. :)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
65
lib/strategies/oauth2.dart
Normal file
65
lib/strategies/oauth2.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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"
|
|
@ -48,5 +48,9 @@ main() async {
|
||||||
test('force everything', () async {
|
test('force everything', () async {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logout', () async {
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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"'));
|
||||||
|
|
Loading…
Reference in a new issue