Updated auth
This commit is contained in:
parent
aa3ce41a9a
commit
0d1601980a
11 changed files with 171 additions and 84 deletions
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'});
|
||||||
|
|
Loading…
Reference in a new issue