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 merge_map and migrated to 2.0.0 (6/6 tests passed)
* Added mock_request and migrated to 2.0.0 (5/5 tests) * 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_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_configuration to 4.0.0 (6/8 testspassed)
* Migrated angel_validate to 4.0.0 (6/7 tests passed) * Migrated angel_validate to 4.0.0 (6/7 tests passed)
* Migrated json_god to 4.0.0 (13/13 tests passed) * Migrated json_god to 4.0.0 (13/13 tests passed)

View file

@ -1,102 +1,135 @@
# 4.0.4 # Change Log
* Changed serializer and deserializer to be required
* Fixed "allow basic" test case
# 4.0.3 ## 4.0.4
* Fixed "failureRedirect" test case
# 4.0.2 * Changed `serializer` and `deserializer` parameters to be required
* Added MirrorsReflector to test cases * 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 * Updated README
# 4.0.0 ## 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD * 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 * Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.1.5+1 ## 2.1.5+1
* Fix error in popup page. * Fix error in popup page.
# 2.1.5 ## 2.1.5
* Modify `_apply` to honor an existing `User` over `Future<User>`. * Modify `_apply` to honor an existing `User` over `Future<User>`.
# 2.1.4 ## 2.1.4
* Deprecate `decodeJwt`, in favor of asynchronous injections. * Deprecate `decodeJwt`, in favor of asynchronous injections.
# 2.1.3 ## 2.1.3
* Use `await` on redirects, etc. * Use `await` on redirects, etc.
# 2.1.2 ## 2.1.2
* Change empty cookie string to have double quotes (thanks @korsvanloon). * Change empty cookie string to have double quotes (thanks @korsvanloon).
# 2.1.1 ## 2.1.1
* Added `scopes` to `ExternalAuthOptions`. * Added `scopes` to `ExternalAuthOptions`.
# 2.1.0 ## 2.1.0
* Added `ExternalAuthOptions`. * Added `ExternalAuthOptions`.
# 2.0.4 ## 2.0.4
* `successRedirect` was previously explicitly returning a `200`; remove this and allow the default `302`. * `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. * Updates for streaming parse of request bodies.
# 2.0.2 ## 2.0.2
* Handle `null` return in `authenticate` + `failureRedirect`. * Handle `null` return in `authenticate` + `failureRedirect`.
# 2.0.1 ## 2.0.1
* Add generic parameter to `options` on `AuthStrategy.authenticate`. * Add generic parameter to `options` on `AuthStrategy.authenticate`.
# 2.0.0+1 ## 2.0.0+1
* Meta update to improve Pub score. * Meta update to improve Pub score.
# 2.0.0 ## 2.0.0
* Made `AuthStrategy` generic. * Made `AuthStrategy` generic.
* `AngelAuth.strategies` is now a `Map<String, AuthStrategy<User>>`. * `AngelAuth.strategies` is now a `Map<String, AuthStrategy<User>>`.
* Removed `AuthStrategy.canLogout`. * Removed `AuthStrategy.canLogout`.
* Made `AngelAuthTokenCallback` generic. * Made `AngelAuthTokenCallback` generic.
# 2.0.0-alpha ## 2.0.0-alpha
* Depend on Dart 2 and Angel 2. * Depend on Dart 2 and Angel 2.
* Remove `dart2_constant`. * Remove `dart2_constant`.
* Remove `requireAuth`. * Remove `requireAuth`.
* Remove `userKey`, instead favoring generic parameters. * Remove `userKey`, instead favoring generic parameters.
# 1.2.0 ## 1.2.0
* Deprecate `requireAuth`, in favor of `requireAuthentication`. * Deprecate `requireAuth`, in favor of `requireAuthentication`.
* Allow configuring of the `userKey`. * Allow configuring of the `userKey`.
* Deprecate `middlewareName`. * Deprecate `middlewareName`.
# 1.1.1+6 ## 1.1.1+6
* Fix a small logic bug that prevented `LocalAuthStrategy` * Fix a small logic bug that prevented `LocalAuthStrategy`
from correctly propagating the authenticated user when from correctly propagating the authenticated user when
using `Basic` auth. using `Basic` auth.
# 1.1.1+5 ## 1.1.1+5
* Prevent duplication of cookies. * Prevent duplication of cookies.
* Regenerate the JWT if `tokenCallback` is called. * Regenerate the JWT if `tokenCallback` is called.
# 1.1.1+4 ## 1.1.1+4
* Patched `logout` to properly erase cookies * Patched `logout` to properly erase cookies
* Fixed checking of expired tokens. * Fixed checking of expired tokens.
# 1.1.1+3 ## 1.1.1+3
* `authenticate` returns the current user, if one is present. * `authenticate` returns the current user, if one is present.
# 1.1.1+2 ## 1.1.1+2
* `_apply` now always sends a `token` cookie. * `_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`. * 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. * Added `protectCookie`, to better protect data sent in cookies.
# 1.1.0+2 ## 1.1.0+2
* `LocalAuthStrategy` returns `true` on `Basic` authentication. * `LocalAuthStrategy` returns `true` on `Basic` authentication.
# 1.1.0+1 ## 1.1.0+1
* Modified `LocalAuthStrategy`'s handling of `Basic` authentication. * 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) [![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) [![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) [![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) [![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 ## Bundled Strategies
[Click here](https://github.com/angel-dart/auth/wiki).
# Bundled Strategies
* Local (with and without Basic Auth) * 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 ## Example
Ensure you have read the [wiki](https://github.com/angel-dart/auth/wiki).
Ensure you have read the [User Guide](https://angel3-docs.dukefirehawk.com/guides/authentication).
```dart ```dart
configureServer(Angel app) async { 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. A frequent use case within SPA's is opening OAuth login endpoints in a separate window.
[`angel_client`](https://github.com/angel-dart/client) [`angel3_client`](https://github.com/dukefirehawk/angel/tree/angel3/packages/client)
provides a facility for this, which works perfectly with the default callback provided provides a facility for this, which works perfectly with the default callback provided in this package.
in this package.
```dart ```dart
configureServer(Angel app) async { 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`. 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 ```dart
app.authenticateViaPopup('/auth/google').listen((jwt) { app.authenticateViaPopup('/auth/google').listen((jwt) {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
name: angel3_auth name: angel3_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: 4.0.4 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: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
@ -12,11 +13,11 @@ dependencies:
http_parser: ^4.0.0 http_parser: ^4.0.0
meta: ^1.3.0 meta: ^1.3.0
quiver: ^3.0.0 quiver: ^3.0.0
logging: ^1.0.0
dev_dependencies: dev_dependencies:
angel3_container: ^3.0.0 angel3_container: ^3.0.0
http: ^0.13.1 http: ^0.13.1
io: ^1.0.0 io: ^1.0.0
logging: ^1.0.0
pedantic: ^1.11.0 pedantic: ^1.11.0
test: ^1.17.4 test: ^1.17.4

View file

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

View file

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