This commit is contained in:
Tobe O 2019-01-05 18:54:48 -05:00
parent b660eef2db
commit 46318e5fa6
10 changed files with 57 additions and 33 deletions

View file

@ -1,3 +1,6 @@
# 2.1.1
* Added `scopes` to `ExternalAuthOptions`.
# 2.1.0 # 2.1.0
* Added `ExternalAuthOptions`. * Added `ExternalAuthOptions`.

View file

@ -36,10 +36,10 @@ class AuthToken {
AuthToken( AuthToken(
{this.ipAddress, {this.ipAddress,
this.lifeSpan: -1, this.lifeSpan = -1,
this.userId, this.userId,
DateTime issuedAt, DateTime issuedAt,
Map payload: const {}}) { Map payload = const {}}) {
this.issuedAt = issuedAt ?? new DateTime.now(); this.issuedAt = issuedAt ?? new DateTime.now();
this.payload.addAll( this.payload.addAll(
payload?.keys?.fold({}, (out, k) => out..[k.toString()] = payload[k]) ?? payload?.keys?.fold({}, (out, k) => out..[k.toString()] = payload[k]) ??

View file

@ -1,4 +1,5 @@
import 'package:charcode/ascii.dart'; import 'package:charcode/ascii.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:quiver_hashcode/hashcode.dart'; import 'package:quiver_hashcode/hashcode.dart';
@ -13,7 +14,11 @@ class ExternalAuthOptions {
/// The user's redirect URI. /// The user's redirect URI.
final Uri redirectUri; final Uri redirectUri;
ExternalAuthOptions._(this.clientId, this.clientSecret, this.redirectUri) { /// The scopes to be passed to the external server.
final Set<String> scopes;
ExternalAuthOptions._(
this.clientId, this.clientSecret, this.redirectUri, this.scopes) {
if (clientId == null) { if (clientId == null) {
throw new ArgumentError.notNull('clientId'); throw new ArgumentError.notNull('clientId');
} else if (clientSecret == null) { } else if (clientSecret == null) {
@ -24,12 +29,14 @@ class ExternalAuthOptions {
factory ExternalAuthOptions( factory ExternalAuthOptions(
{@required String clientId, {@required String clientId,
@required String clientSecret, @required String clientSecret,
@required redirectUri}) { @required redirectUri,
Iterable<String> scopes = const []}) {
if (redirectUri is String) { if (redirectUri is String) {
return new ExternalAuthOptions._( return new ExternalAuthOptions._(
clientId, clientSecret, Uri.parse(redirectUri)); clientId, clientSecret, Uri.parse(redirectUri), scopes.toSet());
} else if (redirectUri is Uri) { } else if (redirectUri is Uri) {
return new ExternalAuthOptions._(clientId, clientSecret, redirectUri); return new ExternalAuthOptions._(
clientId, clientSecret, redirectUri, scopes.toSet());
} else { } else {
throw new ArgumentError.value( throw new ArgumentError.value(
redirectUri, 'redirectUri', 'must be a String or Uri'); redirectUri, 'redirectUri', 'must be a String or Uri');
@ -47,26 +54,34 @@ class ExternalAuthOptions {
clientId: map['client_id'] as String, clientId: map['client_id'] as String,
clientSecret: map['client_secret'] as String, clientSecret: map['client_secret'] as String,
redirectUri: map['redirect_uri'], redirectUri: map['redirect_uri'],
scopes: map['scopes'] is Iterable
? ((map['scopes'] as Iterable).map((x) => x.toString()))
: <String>[],
); );
} }
@override @override
int get hashCode => hash3(clientId, clientSecret, redirectUri); int get hashCode => hash4(clientId, clientSecret, redirectUri, scopes);
@override @override
bool operator ==(other) => bool operator ==(other) =>
other is ExternalAuthOptions && other is ExternalAuthOptions &&
other.clientId == clientId && other.clientId == clientId &&
other.clientSecret == other.clientSecret && other.clientSecret == other.clientSecret &&
other.redirectUri == other.redirectUri; other.redirectUri == other.redirectUri &&
const SetEquality<String>().equals(other.scopes, scopes);
/// Creates a copy of this object, with the specified changes. /// Creates a copy of this object, with the specified changes.
ExternalAuthOptions copyWith( ExternalAuthOptions copyWith(
{String clientId, String clientSecret, redirectUri}) { {String clientId,
String clientSecret,
redirectUri,
Iterable<String> scopes}) {
return new ExternalAuthOptions( return new ExternalAuthOptions(
clientId: clientId ?? this.clientId, clientId: clientId ?? this.clientId,
clientSecret: clientSecret ?? this.clientSecret, clientSecret: clientSecret ?? this.clientSecret,
redirectUri: redirectUri ?? this.redirectUri, redirectUri: redirectUri ?? this.redirectUri,
scopes: (scopes ??= []).followedBy(this.scopes),
); );
} }
@ -79,11 +94,12 @@ class ExternalAuthOptions {
/// ///
/// If [obscureSecret] is `true` (default), then the [clientSecret] will /// If [obscureSecret] is `true` (default), then the [clientSecret] will
/// be replaced by the string `<redacted>`. /// be replaced by the string `<redacted>`.
Map<String, String> toJson({bool obscureSecret = true}) { Map<String, dynamic> toJson({bool obscureSecret = true}) {
return { return {
'client_id': clientId, 'client_id': clientId,
'client_secret': obscureSecret ? '<redacted>' : clientSecret, 'client_secret': obscureSecret ? '<redacted>' : clientSecret,
'redirect_uri': redirectUri.toString(), 'redirect_uri': redirectUri.toString(),
'scopes': scopes.toList(),
}; };
} }
@ -110,6 +126,7 @@ class ExternalAuthOptions {
b.write('clientId=$clientId'); b.write('clientId=$clientId');
b.write(', clientSecret=$secret'); b.write(', clientSecret=$secret');
b.write(', redirectUri=$redirectUri'); b.write(', redirectUri=$redirectUri');
b.write(', scopes=${scopes.toList()}');
b.write(')'); b.write(')');
return b.toString(); return b.toString();
} }

View file

@ -14,7 +14,7 @@ RequestHandler forceBasicAuth<User>({String realm}) {
/// Restricts access to a resource via authentication. /// Restricts access to a resource via authentication.
RequestHandler requireAuthentication<User>() { RequestHandler requireAuthentication<User>() {
return (RequestContext req, ResponseContext res, return (RequestContext req, ResponseContext res,
{bool throwError: true}) async { {bool throwError = true}) async {
bool _reject(ResponseContext res) { bool _reject(ResponseContext res) {
if (throwError) { if (throwError) {
res.statusCode = 403; res.statusCode = 403;

View file

@ -24,7 +24,7 @@ class AngelAuthOptions<User> {
AngelAuthOptions( AngelAuthOptions(
{this.callback, {this.callback,
this.tokenCallback, this.tokenCallback,
this.canRespondWithJson: true, this.canRespondWithJson = true,
this.successRedirect, this.successRedirect,
String this.failureRedirect}); String this.failureRedirect});
} }

View file

@ -63,8 +63,8 @@ class AngelAuth<User> {
Hmac get hmac => _hs256; Hmac get hmac => _hs256;
String _randomString( String _randomString(
{int length: 32, {int length = 32,
String validChars: String validChars =
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) { "ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
var chars = <int>[]; var chars = <int>[];
while (chars.length < length) chars.add(_random.nextInt(validChars.length)); while (chars.length < length) chars.add(_random.nextInt(validChars.length));
@ -77,13 +77,13 @@ class AngelAuth<User> {
this.serializer, this.serializer,
this.deserializer, this.deserializer,
num jwtLifeSpan, num jwtLifeSpan,
this.allowCookie: true, this.allowCookie = true,
this.allowTokenInQuery: true, this.allowTokenInQuery = true,
this.enforceIp: true, this.enforceIp = true,
this.cookieDomain, this.cookieDomain,
this.cookiePath: '/', this.cookiePath = '/',
this.secureCookies: true, this.secureCookies = true,
this.reviveTokenEndpoint: "/auth/token"}) this.reviveTokenEndpoint = "/auth/token"})
: super() { : super() {
_hs256 = new Hmac(sha256, (jwtKey ?? _randomString()).codeUnits); _hs256 = new Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
_jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1; _jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1;

View file

@ -4,7 +4,7 @@ import 'package:http_parser/http_parser.dart';
import 'options.dart'; import 'options.dart';
/// Displays a default callback page to confirm authentication via popups. /// Displays a default callback page to confirm authentication via popups.
AngelAuthCallback confirmPopupAuthentication({String eventName: 'token'}) { AngelAuthCallback confirmPopupAuthentication({String eventName = 'token'}) {
return (req, ResponseContext res, String jwt) { return (req, ResponseContext res, String jwt) {
var evt = json.encode(eventName); var evt = json.encode(eventName);
var detail = json.encode({'detail': jwt}); var detail = json.encode({'detail': jwt});

View file

@ -23,13 +23,13 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
String realm; String realm;
LocalAuthStrategy(this.verifier, LocalAuthStrategy(this.verifier,
{String this.usernameField: 'username', {String this.usernameField = 'username',
String this.passwordField: 'password', String this.passwordField = 'password',
String this.invalidMessage: String this.invalidMessage =
'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.realm: 'Authentication is required.'}) {} String this.realm = 'Authentication is required.'}) {}
@override @override
Future<User> authenticate(RequestContext req, ResponseContext res, Future<User> authenticate(RequestContext req, ResponseContext res,
@ -54,8 +54,8 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
if (verificationResult == false || verificationResult == null) { if (verificationResult == false || verificationResult == null) {
res res
..statusCode = 401 ..statusCode = 401
..headers['www-authenticate'] = 'Basic realm="$realm"' ..headers['www-authenticate'] = 'Basic realm="$realm"';
..close(); await res.close();
return null; return null;
} }

View file

@ -1,6 +1,6 @@
name: angel_auth name: angel_auth
description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more. description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more.
version: 2.1.0 version: 2.1.1
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_auth homepage: https://github.com/angel-dart/angel_auth
environment: environment:

View file

@ -23,11 +23,13 @@ void main() {
clientId: 'hey', clientId: 'hey',
clientSecret: 'hello', clientSecret: 'hello',
redirectUri: 'https://yes.no', redirectUri: 'https://yes.no',
scopes: ['a', 'b'],
), ),
new ExternalAuthOptions( new ExternalAuthOptions(
clientId: 'hey', clientId: 'hey',
clientSecret: 'hello', clientSecret: 'hello',
redirectUri: 'https://yes.no', redirectUri: 'https://yes.no',
scopes: ['a', 'b'],
), ),
); );
}); });
@ -113,21 +115,21 @@ void main() {
test('produces correct string', () { test('produces correct string', () {
expect( expect(
options.toString(obscureSecret: false), options.toString(obscureSecret: false),
'ExternalAuthOptions(clientId=foo, clientSecret=bar, redirectUri=http://example.com)', 'ExternalAuthOptions(clientId=foo, clientSecret=bar, redirectUri=http://example.com, scopes=[])',
); );
}); });
test('obscures secret', () { test('obscures secret', () {
expect( expect(
options.toString(), options.toString(),
'ExternalAuthOptions(clientId=foo, clientSecret=***, redirectUri=http://example.com)', 'ExternalAuthOptions(clientId=foo, clientSecret=***, redirectUri=http://example.com, scopes=[])',
); );
}); });
test('asteriskCount', () { test('asteriskCount', () {
expect( expect(
options.toString(asteriskCount: 7), options.toString(asteriskCount: 7),
'ExternalAuthOptions(clientId=foo, clientSecret=*******, redirectUri=http://example.com)', 'ExternalAuthOptions(clientId=foo, clientSecret=*******, redirectUri=http://example.com, scopes=[])',
); );
}); });
}); });
@ -140,6 +142,7 @@ void main() {
'client_id': 'foo', 'client_id': 'foo',
'client_secret': '<redacted>', 'client_secret': '<redacted>',
'redirect_uri': 'http://example.com', 'redirect_uri': 'http://example.com',
'scopes': [],
}, },
); );
}); });
@ -151,6 +154,7 @@ void main() {
'client_id': 'foo', 'client_id': 'foo',
'client_secret': 'bar', 'client_secret': 'bar',
'redirect_uri': 'http://example.com', 'redirect_uri': 'http://example.com',
'scopes': [],
}, },
); );
}); });