Updated auth

This commit is contained in:
thomashii 2021-07-08 09:20:21 +08:00
parent aa3ce41a9a
commit 0d1601980a
11 changed files with 171 additions and 84 deletions

View file

@ -15,7 +15,7 @@
* Added merge_map and migrated to 2.0.0 (6/6 tests passed)
* Added mock_request and migrated to 2.0.0 (5/5 tests)
* Migrated angel_framework to 4.0.0 (149/150 tests passed)
* Migrated angel_auth to 4.0.0 (29/30 tests passed)
* Migrated angel_auth to 4.0.0 (31/31 tests passed)
* Migrated angel_configuration to 4.0.0 (6/8 testspassed)
* Migrated angel_validate to 4.0.0 (6/7 tests passed)
* Migrated json_god to 4.0.0 (13/13 tests passed)

View file

@ -1,102 +1,135 @@
# 4.0.4
* Changed serializer and deserializer to be required
* Fixed "allow basic" test case
# Change Log
# 4.0.3
* Fixed "failureRedirect" test case
## 4.0.4
# 4.0.2
* Added MirrorsReflector to test cases
* Changed `serializer` and `deserializer` parameters to be required
* Fixed HTTP basic authentication
* Passed all 51 unit tests
## 4.0.3
* Fixed "failureRedirect" unit test
## 4.0.2
* Added MirrorsReflector to unit test
## 4.0.1
# 4.0.1
* Updated README
# 4.0.0
## 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0
## 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.1.5+1
## 2.1.5+1
* Fix error in popup page.
# 2.1.5
## 2.1.5
* Modify `_apply` to honor an existing `User` over `Future<User>`.
# 2.1.4
## 2.1.4
* Deprecate `decodeJwt`, in favor of asynchronous injections.
# 2.1.3
## 2.1.3
* Use `await` on redirects, etc.
# 2.1.2
## 2.1.2
* Change empty cookie string to have double quotes (thanks @korsvanloon).
# 2.1.1
## 2.1.1
* Added `scopes` to `ExternalAuthOptions`.
# 2.1.0
## 2.1.0
* Added `ExternalAuthOptions`.
# 2.0.4
## 2.0.4
* `successRedirect` was previously explicitly returning a `200`; remove this and allow the default `302`.
# 2.0.3
## 2.0.3
* Updates for streaming parse of request bodies.
# 2.0.2
## 2.0.2
* Handle `null` return in `authenticate` + `failureRedirect`.
# 2.0.1
## 2.0.1
* Add generic parameter to `options` on `AuthStrategy.authenticate`.
# 2.0.0+1
## 2.0.0+1
* Meta update to improve Pub score.
# 2.0.0
## 2.0.0
* Made `AuthStrategy` generic.
* `AngelAuth.strategies` is now a `Map<String, AuthStrategy<User>>`.
* Removed `AuthStrategy.canLogout`.
* Made `AngelAuthTokenCallback` generic.
# 2.0.0-alpha
## 2.0.0-alpha
* Depend on Dart 2 and Angel 2.
* Remove `dart2_constant`.
* Remove `requireAuth`.
* Remove `userKey`, instead favoring generic parameters.
# 1.2.0
## 1.2.0
* Deprecate `requireAuth`, in favor of `requireAuthentication`.
* Allow configuring of the `userKey`.
* Deprecate `middlewareName`.
# 1.1.1+6
## 1.1.1+6
* Fix a small logic bug that prevented `LocalAuthStrategy`
from correctly propagating the authenticated user when
using `Basic` auth.
# 1.1.1+5
## 1.1.1+5
* Prevent duplication of cookies.
* Regenerate the JWT if `tokenCallback` is called.
# 1.1.1+4
## 1.1.1+4
* Patched `logout` to properly erase cookies
* Fixed checking of expired tokens.
# 1.1.1+3
## 1.1.1+3
* `authenticate` returns the current user, if one is present.
# 1.1.1+2
## 1.1.1+2
* `_apply` now always sends a `token` cookie.
# 1.1.1+1
## 1.1.1+1
* Update `protectCookie` to only send `maxAge` when it is not `-1`.
# 1.1.1
## 1.1.1
* Added `protectCookie`, to better protect data sent in cookies.
# 1.1.0+2
## 1.1.0+2
* `LocalAuthStrategy` returns `true` on `Basic` authentication.
# 1.1.0+1
## 1.1.0+1
* Modified `LocalAuthStrategy`'s handling of `Basic` authentication.

View file

@ -1,21 +1,21 @@
# angel3_auth
# Angel3 Anthentication
[![version](https://img.shields.io/badge/pub-v4.0.4-brightgreen)](https://pub.dartlang.org/packages/angel3_auth)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/auth/LICENSE)
A complete authentication plugin for Angel. Inspired by Passport.
A complete authentication plugin for Angel3. Inspired by Passport. More details in the [User Guide](https://angel3-docs.dukefirehawk.com/guides/authentication).
# Wiki
[Click here](https://github.com/angel-dart/auth/wiki).
## Bundled Strategies
# Bundled Strategies
* Local (with and without Basic Auth)
* Find other strategies (Twitter, Google, OAuth2, etc.) on Pub!!!
* Find other strategies (Twitter, Google, OAuth2, etc.) on pub
# Example
Ensure you have read the [wiki](https://github.com/angel-dart/auth/wiki).
## Example
Ensure you have read the [User Guide](https://angel3-docs.dukefirehawk.com/guides/authentication).
```dart
configureServer(Angel app) async {
@ -50,11 +50,11 @@ configureServer(Angel app) async {
}
```
# Default Authentication Callback
## Default Authentication Callback
A frequent use case within SPA's is opening OAuth login endpoints in a separate window.
[`angel_client`](https://github.com/angel-dart/client)
provides a facility for this, which works perfectly with the default callback provided
in this package.
[`angel3_client`](https://github.com/dukefirehawk/angel/tree/angel3/packages/client)
provides a facility for this, which works perfectly with the default callback provided in this package.
```dart
configureServer(Angel app) async {
@ -78,7 +78,7 @@ configureServer(Angel app) async {
```
This renders a simple HTML page that fires the user's JWT as a `token` event in `window.opener`.
`angel_client` [exposes this as a Stream](https://github.com/dukefirehawk/angel/tree/angel3/packages/client#authentication):
`angel3_client` [exposes this as a Stream](https://github.com/dukefirehawk/angel/tree/angel3/packages/client#authentication):
```dart
app.authenticateViaPopup('/auth/google').listen((jwt) {

View file

@ -2,6 +2,7 @@ import 'dart:collection';
import 'package:angel3_framework/angel3_framework.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:logging/logging.dart';
/// Calls [BASE64URL], but also works for strings with lengths
/// that are *not* multiples of 4.
@ -25,6 +26,8 @@ String decodeBase64(String str) {
}
class AuthToken {
static final _log = Logger('AuthToken');
final SplayTreeMap<String, String> _header =
SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});
@ -70,6 +73,7 @@ class AuthToken {
var split = jwt.split('.');
if (split.length != 3) {
_log.severe('Invalid JWT');
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
}
@ -81,6 +85,7 @@ class AuthToken {
var split = jwt.split('.');
if (split.length != 3) {
_log.severe('Invalid JWT');
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
}
@ -90,6 +95,7 @@ class AuthToken {
var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes);
if (signature != split[2]) {
_log.severe('JWT payload does not match hashed version');
throw AngelHttpException.notAuthenticated(
message: 'JWT payload does not match hashed version.');
}

View file

@ -1,9 +1,12 @@
import 'package:charcode/ascii.dart';
import 'package:collection/collection.dart';
import 'package:quiver/core.dart';
import 'package:logging/logging.dart';
/// A common class containing parsing and validation logic for third-party authentication configuration.
class ExternalAuthOptions {
static final _log = Logger('VirtualDirectory');
/// The user's identifier, otherwise known as an "application id".
final String clientId;
@ -31,6 +34,7 @@ class ExternalAuthOptions {
return ExternalAuthOptions._(
clientId, clientSecret, redirectUri, scopes.toSet());
} else {
_log.severe('RedirectUri is not valid');
throw ArgumentError.value(
redirectUri, 'redirectUri', 'must be a String or Uri');
}
@ -46,6 +50,7 @@ class ExternalAuthOptions {
var clientId = map['client_id'];
var clientSecret = map['client_secret'];
if (clientId == null || clientSecret == null) {
_log.severe('clientId or clientSecret is null');
throw ArgumentError('Invalid clientId and/or clientSecret');
}

View file

@ -3,7 +3,7 @@ import 'package:angel3_framework/angel3_framework.dart';
/// Forces Basic authentication over the requested resource, with the given [realm] name, if no JWT is present.
///
/// [realm] defaults to `'angel_auth'`.
/// [realm] defaults to `'angel3_auth'`.
RequestHandler forceBasicAuth<User>({String? realm}) {
return (RequestContext req, ResponseContext res) async {
if (req.container != null) {

View file

@ -11,6 +11,8 @@ import 'strategy.dart';
/// Handles authentication within an Angel application.
class AngelAuth<User> {
final _log = Logger('AngelAuth');
late Hmac _hs256;
late int _jwtLifeSpan;
final StreamController<User> _onLogin = StreamController<User>(),
@ -43,8 +45,6 @@ class AngelAuth<User> {
/// This is a security provision. Even if a user's JWT is stolen, a remote attacker will not be able to impersonate anyone.
final bool enforceIp;
final log = Logger('AngelAuth');
/// The endpoint to mount [reviveJwt] at. If `null`, then no revival route is mounted. Default: `/auth/token`.
String reviveTokenEndpoint;
@ -109,7 +109,7 @@ class AngelAuth<User> {
}
*/
if (app.container == null) {
log.severe('Angel.container is null.');
_log.severe('Angel3 container is null');
throw StateError(
'Angel.container is null. All authentication will fail.');
}
@ -126,6 +126,7 @@ class AngelAuth<User> {
var req = container.make<RequestContext>();
var res = container.make<ResponseContext>();
if (req == null || res == null) {
_log.severe('RequestContext or responseContext is null');
throw AngelHttpException.forbidden();
}
@ -133,6 +134,7 @@ class AngelAuth<User> {
if (result != null) {
return result;
} else {
_log.severe('JWT is null');
throw AngelHttpException.forbidden();
}
});
@ -148,7 +150,7 @@ class AngelAuth<User> {
});
}
app.post(reviveTokenEndpoint, reviveJwt);
app.post(reviveTokenEndpoint, _reviveJwt);
app.shutdownHooks.add((_) {
_onLogin.close();
@ -158,7 +160,7 @@ class AngelAuth<User> {
void _apply(
RequestContext req, ResponseContext res, AuthToken token, User user) {
if (req.container == null) {
log.severe('RequestContext.container is null.');
_log.severe('RequestContext.container is null');
throw StateError(
'RequestContext.container is not set. All authentication will fail.');
}
@ -199,9 +201,9 @@ class AngelAuth<User> {
/// }
/// ```
@deprecated
Future decodeJwtOld(RequestContext req, ResponseContext res) async {
Future decodeJwt(RequestContext req, ResponseContext res) async {
if (req.method == 'POST' && req.path == reviveTokenEndpoint) {
return await reviveJwt(req, res);
return await _reviveJwt(req, res);
} else {
await _decodeJwt(req, res);
return true;
@ -217,6 +219,7 @@ class AngelAuth<User> {
if (enforceIp) {
if (req.ip != token.ipAddress) {
_log.severe('JWT cannot be accessed from this IP address');
throw AngelHttpException.forbidden(
message: 'JWT cannot be accessed from this IP address.');
}
@ -227,6 +230,7 @@ class AngelAuth<User> {
token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt()));
if (!expiry.isAfter(DateTime.now())) {
_log.severe('Expired JWT');
throw AngelHttpException.forbidden(message: 'Expired JWT.');
}
}
@ -250,7 +254,7 @@ class AngelAuth<User> {
}
}
log.info('RequestContext.headers is null');
_log.info('RequestContext.headers is null');
} else if (allowCookie &&
req.cookies.any((cookie) => cookie.name == 'token')) {
return req.cookies.firstWhere((cookie) => cookie.name == 'token').value;
@ -289,7 +293,7 @@ class AngelAuth<User> {
}
/// Attempts to revive an expired (or still alive) JWT.
Future<Map<String, dynamic>> reviveJwt(
Future<Map<String, dynamic>> _reviveJwt(
RequestContext req, ResponseContext res) async {
try {
var jwt = getJwt(req);
@ -298,12 +302,15 @@ class AngelAuth<User> {
var body = await req.parseBody().then((_) => req.bodyAsMap);
jwt = body['token']?.toString();
}
if (jwt == null) {
_log.severe('No JWT provided');
throw AngelHttpException.forbidden(message: 'No JWT provided');
} else {
var token = AuthToken.validate(jwt, _hs256);
if (enforceIp) {
if (req.ip != token.ipAddress) {
_log.severe('WT cannot be accessed from this IP address');
throw AngelHttpException.forbidden(
message: 'JWT cannot be accessed from this IP address.');
}
@ -329,7 +336,10 @@ class AngelAuth<User> {
return {'data': data, 'token': token.serialize(_hs256)};
}
} catch (e) {
if (e is AngelHttpException) rethrow;
if (e is AngelHttpException) {
rethrow;
}
_log.severe('Malformed JWT');
throw AngelHttpException.badRequest(message: 'Malformed JWT');
}
}
@ -385,13 +395,13 @@ class AngelAuth<User> {
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
var jwt = token.serialize(_hs256);
if (options?.tokenCallback != null) {
if (options != null && options.tokenCallback != null) {
var hasUser = reqContainer?.has<User>() ?? false;
if (!hasUser) {
reqContainer?.registerSingleton<User>(result);
}
var r = await options?.tokenCallback!(req, res, token, result);
var r = await options.tokenCallback!(req, res, token, result);
if (r != null) return r;
jwt = token.serialize(_hs256);
}
@ -402,14 +412,16 @@ class AngelAuth<User> {
_addProtectedCookie(res, 'token', jwt);
}
if (options?.callback != null) {
return await options?.callback!(req, res, jwt);
// Options is not null
if (options != null) {
if (options.callback != null) {
return await options.callback!(req, res, jwt);
}
if (options?.successRedirect?.isNotEmpty == true) {
await res.redirect(options?.successRedirect);
if (options.successRedirect?.isNotEmpty == true) {
await res.redirect(options.successRedirect);
return false;
} else if (options?.canRespondWithJson != false &&
} else if (options.canRespondWithJson &&
req.accepts('application/json')) {
var user = hasExisting
? result
@ -417,6 +429,14 @@ class AngelAuth<User> {
_onLogin.add(user);
return {'data': user, 'token': jwt};
}
// Options is null
} else if (hasExisting && req.accepts('application/json')) {
var user = hasExisting
? result
: await deserializer((await serializer(result)) as Object);
_onLogin.add(user);
return {'data': user, 'token': jwt};
}
return true;
} else {
@ -426,8 +446,8 @@ class AngelAuth<User> {
res.statusCode == 302 ||
res.headers.containsKey('location')) {
return false;
} else if (options?.failureRedirect != null) {
await res.redirect(options!.failureRedirect);
} else if (options != null && options.failureRedirect != null) {
await res.redirect(options.failureRedirect);
return false;
} else {
throw AngelHttpException.notAuthenticated();

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:angel3_framework/angel3_framework.dart';
import '../options.dart';
import '../strategy.dart';
@ -12,6 +13,8 @@ typedef LocalAuthVerifier<User> = FutureOr<User?> Function(
String? username, String? password);
class LocalAuthStrategy<User> extends AuthStrategy<User> {
final _log = Logger('LocalAuthStrategy');
final RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
final RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
@ -29,7 +32,9 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
this.invalidMessage = 'Please provide a valid username and password.',
this.allowBasic = true,
this.forceBasic = false,
this.realm = 'Authentication is required.'});
this.realm = 'Authentication is required.'}) {
_log.info('Using LocalAuthStrategy');
}
@override
Future<User?> authenticate(RequestContext req, ResponseContext res,
@ -51,6 +56,7 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
verificationResult =
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
} else {
_log.severe('Bad request: $invalidMessage');
throw AngelHttpException.badRequest(errors: [invalidMessage]);
}
@ -96,6 +102,7 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
} else if (verificationResult != false) {
return verificationResult;
} else {
_log.info('Not authenticated');
throw AngelHttpException.notAuthenticated();
}
}

View file

@ -1,7 +1,8 @@
name: angel3_auth
description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more.
version: 4.0.4
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth
homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
@ -12,11 +13,11 @@ dependencies:
http_parser: ^4.0.0
meta: ^1.3.0
quiver: ^3.0.0
logging: ^1.0.0
dev_dependencies:
angel3_container: ^3.0.0
http: ^0.13.1
io: ^1.0.0
logging: ^1.0.0
pedantic: ^1.11.0
test: ^1.17.4

View file

@ -50,7 +50,7 @@ void main() {
var oldErrorHandler = app.errorHandler;
app.errorHandler = (e, req, res) {
app.logger!.severe(e.message, e, e.stackTrace ?? StackTrace.current);
app.logger?.severe(e.message, e, e.stackTrace ?? StackTrace.current);
return oldErrorHandler(e, req, res);
};
@ -69,8 +69,8 @@ void main() {
});
await app
.findService('users')!
.create({'username': 'jdoe1', 'password': 'password'});
.findService('users')
?.create({'username': 'jdoe1', 'password': 'password'});
auth = AngelAuth<User>(
serializer: (u) => u.id,
@ -84,11 +84,11 @@ void main() {
auth.strategies['local'] = LocalAuthStrategy((username, password) async {
var users = await app
.findService('users')!
.index()
.findService('users')
?.index()
.then((it) => it.map<User>((m) => User.parse(m as Map)).toList());
var result = users.firstWhereOrNull(
var result = users?.firstWhereOrNull(
(user) => user.username == username && user.password == password);
return Future.value(result);
@ -144,6 +144,7 @@ void main() {
body: {'username': 'jdoe1', 'password': 'password'},
headers: {'accept': 'application/json'});
print('Response: ${response.body}');
print(response.headers);
expect(json.decode(response.body)['data']['username'], equals('foo'));
});
}

View file

@ -46,7 +46,7 @@ void main() async {
app.get('/hello', (req, res) {
// => 'Woo auth'
return 'Woo auth';
}); //, middleware: [auth.authenticate('local')]);
}, middleware: [auth.authenticate('local')]);
app.post('/login', (req, res) => 'This should not be shown',
middleware: [auth.authenticate('local', localOpts)]);
app.get('/success', (req, res) => 'yep', middleware: [
@ -54,8 +54,11 @@ void main() async {
]);
app.get('/failure', (req, res) => 'nope');
app.logger = Logger('angel_auth')
app.logger = Logger('local_test')
..onRecord.listen((rec) {
print(
'${rec.time}: ${rec.level.name}: ${rec.loggerName}: ${rec.message}');
if (rec.error != null) {
print(rec.error);
print(rec.stackTrace);
@ -96,12 +99,23 @@ void main() async {
var response = await client.post(Uri.parse('$url/login'),
body: json.encode(postData),
headers: {'content-type': 'application/json'});
print('Login response: ${response.body}');
print('Status Code: ${response.statusCode}');
print(response.headers);
print(response.body);
expect(response.headers['location'], equals('/failure'));
expect(response.statusCode, equals(401));
});
test('allow basic', () async {
test('basic auth without authorization', () async {
var response = await client.get(Uri.parse('$url/hello'));
print('Status Code: ${response.statusCode}');
print(response.headers);
print(response.body);
expect(response.statusCode, equals(401));
});
//test('allow basic', () async {
test('basic auth with authorization', () async {
var authString = base64.encode('username:password'.runes.toList());
var response = await client.get(Uri.parse('$url/hello'),
headers: {'authorization': 'Basic $authString'});