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:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:crypto/crypto.dart' as Crypto;
|
||||
import 'package:oauth2/oauth2.dart' as Oauth2;
|
||||
|
||||
part 'strategy.dart';
|
||||
|
||||
|
@ -13,10 +15,15 @@ part 'middleware/serialization.dart';
|
|||
|
||||
part 'strategies/local.dart';
|
||||
|
||||
part 'strategies/oauth2.dart';
|
||||
|
||||
_validateString(String str) {
|
||||
return str != null && str.isNotEmpty;
|
||||
}
|
||||
|
||||
const String FAILURE_REDIRECT = 'failureRedirect';
|
||||
const String SUCCESS_REDIRECT = 'successRedirect';
|
||||
|
||||
class Auth {
|
||||
static List<AuthStrategy> strategies = [];
|
||||
static UserSerializer serializer;
|
||||
|
@ -27,16 +34,14 @@ class Auth {
|
|||
app.before.add(_serializationMiddleware);
|
||||
}
|
||||
|
||||
static authenticate(String type, [Map options]) {
|
||||
static authenticate(String type, [AngelAuthOptions 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 ?? {});
|
||||
print("${req.path} -> $result");
|
||||
var result = await strategy.authenticate(req, res, options);
|
||||
if (result == true)
|
||||
return result;
|
||||
else if(result != false) {
|
||||
else if (result != false) {
|
||||
req.session['userId'] = await serializer(result);
|
||||
return true;
|
||||
} 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. :)
|
||||
|
|
|
@ -5,6 +5,9 @@ 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 if (throws) {
|
||||
res.status(HttpStatus.UNAUTHORIZED);
|
||||
throw new AngelHttpException.NotAuthenticated();
|
||||
}
|
||||
else return false;
|
||||
}
|
|
@ -15,7 +15,7 @@ class LocalAuthStrategy extends AuthStrategy {
|
|||
String invalidMessage;
|
||||
bool allowBasic;
|
||||
bool forceBasic;
|
||||
String basicRealm;
|
||||
String realm;
|
||||
|
||||
LocalAuthStrategy(LocalAuthVerifier this.verifier,
|
||||
{String this.usernameField: 'username',
|
||||
|
@ -24,7 +24,7 @@ class LocalAuthStrategy extends AuthStrategy {
|
|||
'Please provide a valid username and password.',
|
||||
bool this.allowBasic: true,
|
||||
bool this.forceBasic: false,
|
||||
String this.basicRealm: 'Authentication is required.'}) {}
|
||||
String this.realm: 'Authentication is required.'}) {}
|
||||
|
||||
@override
|
||||
Future<bool> canLogout(RequestContext req, ResponseContext res) async {
|
||||
|
@ -33,7 +33,8 @@ class LocalAuthStrategy extends AuthStrategy {
|
|||
|
||||
@override
|
||||
Future<bool> authenticate(RequestContext req, ResponseContext res,
|
||||
{Map options: const {}}) async {
|
||||
[AngelAuthOptions options_]) async {
|
||||
AngelAuthOptions options = options_ ?? new AngelAuthOptions();
|
||||
var verificationResult;
|
||||
|
||||
if (allowBasic) {
|
||||
|
@ -60,23 +61,24 @@ class LocalAuthStrategy extends AuthStrategy {
|
|||
}
|
||||
|
||||
if (verificationResult == false || verificationResult == null) {
|
||||
if (options.containsKey('failureRedirect')) {
|
||||
if (options.failureRedirect != null &&
|
||||
options.failureRedirect.isNotEmpty) {
|
||||
return res.redirect(
|
||||
options['failureRedirect'], code: HttpStatus.UNAUTHORIZED);
|
||||
options.failureRedirect, code: HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (forceBasic) {
|
||||
res
|
||||
..status(401)
|
||||
..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$basicRealm"')
|
||||
..header(HttpHeaders.WWW_AUTHENTICATE, 'Basic realm="$realm"')
|
||||
..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);
|
||||
if (options.successRedirect != null && options.successRedirect.isNotEmpty) {
|
||||
return res.redirect(options.successRedirect, code: HttpStatus.OK);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// 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.
|
||||
Future<bool> canLogout(RequestContext req, ResponseContext res);
|
||||
|
|
|
@ -5,6 +5,8 @@ author: thosakwe <thosakwe@gmail.com>
|
|||
homepage: https://github.com/angel-dart/angel_auth
|
||||
dependencies:
|
||||
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:
|
||||
http: ">= 0.11.3 < 0.12.0"
|
||||
test: ">= 0.12.13 < 0.13.0"
|
|
@ -48,5 +48,9 @@ main() async {
|
|||
test('force everything', () async {
|
||||
|
||||
});
|
||||
|
||||
test('logout', () async {
|
||||
|
||||
});
|
||||
});
|
||||
}
|
|
@ -7,7 +7,10 @@ import 'package:merge_map/merge_map.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
Map headers = {HttpHeaders.ACCEPT: ContentType.JSON.mimeType};
|
||||
Map localOpts = {'failureRedirect': '/failure'};
|
||||
AngelAuthOptions localOpts = new AngelAuthOptions(
|
||||
failureRedirect: '/failure',
|
||||
successRedirect: '/success'
|
||||
);
|
||||
Map sampleUser = {'hello': 'world'};
|
||||
|
||||
verifier(username, password) async {
|
||||
|
@ -35,7 +38,6 @@ main() async {
|
|||
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',
|
||||
|
@ -58,7 +60,8 @@ main() 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);
|
||||
expect(response.statusCode, equals(401));
|
||||
});
|
||||
|
@ -96,15 +99,14 @@ main() async {
|
|||
});
|
||||
|
||||
test('allow basic via URL encoding', () async {
|
||||
var response = await client.get(
|
||||
basicAuthUrl, headers: headers);
|
||||
var response = await client.get("$basicAuthUrl/hello", headers: headers);
|
||||
expect(response.body, equals('"Woo auth"'));
|
||||
});
|
||||
|
||||
test('force basic', () async {
|
||||
Auth.strategies.clear();
|
||||
Auth.strategies.add(new LocalAuthStrategy(
|
||||
verifier, forceBasic: true, basicRealm: 'test'));
|
||||
verifier, forceBasic: true, realm: 'test'));
|
||||
var response = await client.get("$url/hello", headers: headers);
|
||||
expect(response.headers[HttpHeaders.WWW_AUTHENTICATE],
|
||||
equals('Basic realm="test"'));
|
||||
|
|
Loading…
Reference in a new issue