Updated auth

This commit is contained in:
thomashii 2021-06-07 08:50:39 +08:00
parent 7f9e3c58cc
commit 740b78ba00
11 changed files with 105 additions and 68 deletions

View file

@ -1,3 +1,7 @@
# 4.0.4
* Changed serializer and deserializer to be required
* Fixed "allow basic" test case
# 4.0.3
* Fixed "failureRedirect" test case

View file

@ -1,5 +1,5 @@
# angel3_auth
[![version](https://img.shields.io/badge/pub-v4.0.3-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)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)

View file

@ -5,11 +5,9 @@ import 'package:angel3_framework/http.dart';
void main() async {
var app = Angel();
var auth = AngelAuth<User?>();
auth.serializer = (user) => user!.id;
auth.deserializer = (id) => fetchAUserByIdSomehow(id);
var auth = AngelAuth<User>(
serializer: (user) => user.id,
deserializer: (id) => fetchAUserByIdSomehow(id));
// Middleware to decode JWT's and inject a user object...
await app.configure(auth.configureServer);

View file

@ -3,6 +3,8 @@ import 'dart:io';
import 'dart:math';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:crypto/crypto.dart';
import 'package:logging/logging.dart';
import 'auth_token.dart';
import 'options.dart';
import 'strategy.dart';
@ -41,6 +43,8 @@ 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;
@ -48,10 +52,10 @@ class AngelAuth<User> {
Map<String, AuthStrategy<User>> strategies = {};
/// Serializes a user into a unique identifier associated only with one identity.
FutureOr Function(User)? serializer;
FutureOr Function(User) serializer;
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
FutureOr<User> Function(Object)? deserializer;
FutureOr<User> Function(Object) deserializer;
/// Fires the result of [deserializer] whenever a user signs in to the application.
Stream<User> get onLogin => _onLogin.stream;
@ -76,9 +80,9 @@ class AngelAuth<User> {
/// `jwtLifeSpan` - should be in *milliseconds*.
AngelAuth(
{String? jwtKey,
this.serializer,
this.deserializer,
num? jwtLifeSpan,
required this.serializer,
required this.deserializer,
num jwtLifeSpan = -1,
this.allowCookie = true,
this.allowTokenInQuery = true,
this.enforceIp = true,
@ -88,12 +92,13 @@ class AngelAuth<User> {
this.reviveTokenEndpoint = '/auth/token'})
: super() {
_hs256 = Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
_jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1;
_jwtLifeSpan = jwtLifeSpan.toInt();
}
/// Configures an Angel server to decode and validate JSON Web tokens on demand,
/// whenever an instance of [User] is injected.
Future<void> configureServer(Angel app) async {
/*
if (serializer == null) {
throw StateError(
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
@ -102,17 +107,23 @@ class AngelAuth<User> {
throw StateError(
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
}
*/
app.container!.registerSingleton(this);
var appContainer = app.container!;
appContainer.registerSingleton(this);
if (runtimeType != AngelAuth) {
app.container!.registerSingleton(this, as: AngelAuth);
appContainer.registerSingleton(this, as: AngelAuth);
}
if (!app.container!.has<_AuthResult<User>>()) {
app.container!
if (!appContainer.has<_AuthResult<User>>()) {
appContainer
.registerLazySingleton<Future<_AuthResult<User>>>((container) async {
var req = container.make<RequestContext>()!;
var res = container.make<ResponseContext>()!;
var req = container.make<RequestContext>();
var res = container.make<ResponseContext>();
if (req == null || res == null) {
throw AngelHttpException.forbidden();
}
var result = await _decodeJwt(req, res);
if (result != null) {
return result;
@ -121,15 +132,14 @@ class AngelAuth<User> {
}
});
app.container!.registerLazySingleton<Future<User>>((container) async {
var result = await container.makeAsync<_AuthResult<User>>()!;
return result.user;
appContainer.registerLazySingleton<Future<User>>((container) async {
var result = await container.makeAsync<_AuthResult<User>>();
return result!.user;
});
app.container!
.registerLazySingleton<Future<AuthToken>>((container) async {
var result = await container.makeAsync<_AuthResult<User>>()!;
return result.token;
appContainer.registerLazySingleton<Future<AuthToken>>((container) async {
var result = await container.makeAsync<_AuthResult<User>>();
return result!.token;
});
}
@ -142,12 +152,13 @@ class AngelAuth<User> {
void _apply(
RequestContext req, ResponseContext? res, AuthToken token, User user) {
if (!req.container!.has<User>()) {
req.container!.registerSingleton<User>(user);
var reqContainer = req.container!;
if (!reqContainer.has<User>()) {
reqContainer.registerSingleton<User>(user);
}
if (!req.container!.has<AuthToken>()) {
req.container!.registerSingleton<AuthToken>(token);
if (!reqContainer.has<AuthToken>()) {
reqContainer.registerSingleton<AuthToken>(token);
}
if (allowCookie) {
@ -209,7 +220,7 @@ class AngelAuth<User> {
}
}
var user = await deserializer!(token.userId as Object);
var user = await deserializer(token.userId as Object);
_apply(req, res, token, user);
return _AuthResult(user, token);
}
@ -298,7 +309,7 @@ class AngelAuth<User> {
_addProtectedCookie(res, 'token', token.serialize(_hs256));
}
final data = await deserializer!(token.userId as Object);
final data = await deserializer(token.userId as Object);
return {'data': data, 'token': token.serialize(_hs256)};
}
} catch (e) {
@ -337,14 +348,21 @@ class AngelAuth<User> {
var strategy = strategies[name] ??=
throw ArgumentError('No strategy "$name" found.');
var hasExisting = req.container!.has<User>();
var reqContainer = req.container;
if (reqContainer == null) {
print('req.container is null');
}
var hasExisting = reqContainer?.has<User>() ?? false;
var result = hasExisting
? req.container!.make<User>()
: await strategy.authenticate(req, res, options!);
if (result == true) {
? reqContainer?.make<User>()
: await strategy.authenticate(req, res, options);
if (result != null && result == true) {
return result;
} else if (result != false && result != null) {
var userId = await serializer!(result);
var userId = await serializer(result);
// Create JWT
var token = AuthToken(
@ -352,11 +370,12 @@ class AngelAuth<User> {
var jwt = token.serialize(_hs256);
if (options?.tokenCallback != null) {
if (!req.container!.has<User>()) {
req.container!.registerSingleton<User>(result);
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);
}
@ -368,19 +387,19 @@ class AngelAuth<User> {
}
if (options?.callback != null) {
return await options!.callback!(req, res, jwt);
return await options?.callback!(req, res, jwt);
}
if (options?.successRedirect?.isNotEmpty == true) {
await res.redirect(options!.successRedirect);
await res.redirect(options?.successRedirect);
return false;
} else if (options?.canRespondWithJson != false &&
req.accepts('application/json')) {
var user = hasExisting
? result
: await deserializer!((await serializer!(result)) as Object);
: await deserializer((await serializer(result)) as Object);
_onLogin.add(user);
return {"data": user, "token": jwt};
return {'data': user, 'token': jwt};
}
return true;
@ -404,7 +423,7 @@ class AngelAuth<User> {
/// Log a user in on-demand.
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
var user = await deserializer!(token.userId as Object);
var user = await deserializer(token.userId as Object);
_apply(req, res, token, user);
_onLogin.add(user);
@ -415,7 +434,7 @@ class AngelAuth<User> {
/// Log a user in on-demand.
Future loginById(userId, RequestContext req, ResponseContext res) async {
var user = await deserializer!(userId as Object);
var user = await deserializer(userId as Object);
var token =
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
_apply(req, res, token, user);

View file

@ -6,5 +6,5 @@ import 'options.dart';
abstract class AuthStrategy<User> {
/// Authenticates or rejects an incoming user.
FutureOr<User?> authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions<User> options]);
[AngelAuthOptions<User>? options]);
}

View file

@ -1,13 +1,12 @@
name: angel3_auth
description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more.
version: 4.0.3
version: 4.0.4
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth
#publish_to: none
publish_to: none
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
angel3_framework: ^4.0.0
# path: ../framework
charcode: ^1.2.0
collection: ^1.15.0
crypto: ^3.0.0
@ -21,3 +20,9 @@ dev_dependencies:
logging: ^1.0.0
pedantic: ^1.11.0
test: ^1.17.4
#dependency_overrides:
# angel3_framework:
# path: ../framework
# angel3_container:
# path: ../container/angel_container

View file

@ -18,10 +18,10 @@ void main() async {
test('custom payload', () {
var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe', payload: {
"foo": "bar",
"baz": {
"one": 1,
"franken": ["stein"]
'foo': 'bar',
'baz': {
'one': 1,
'franken': ['stein']
}
});
var jwt = token.serialize(hmac);

View file

@ -72,10 +72,13 @@ void main() {
.findService('users')!
.create({'username': 'jdoe1', 'password': 'password'});
auth = AngelAuth<User>();
auth.serializer = (u) => u.id;
auth.deserializer =
(id) async => await app.findService('users')!.read(id) as User;
auth = AngelAuth<User>(
serializer: (u) => u.id,
deserializer: (id) async =>
await app.findService('users')?.read(id) as User);
//auth.serializer = (u) => u.id;
//auth.deserializer =
// (id) async => await app.findService('users')!.read(id) as User;
await app.configure(auth.configureServer);

View file

@ -7,7 +7,8 @@ import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:test/test.dart';
final AngelAuth<Map<String, String>> auth = AngelAuth<Map<String, String>>();
final AngelAuth<Map<String, String>> auth = AngelAuth<Map<String, String>>(
serializer: (user) async => 1337, deserializer: (id) async => sampleUser);
var headers = <String, String>{'accept': 'application/json'};
var localOpts = AngelAuthOptions<Map<String, String>>(
failureRedirect: '/failure', successRedirect: '/success');
@ -23,8 +24,8 @@ Future<Map<String, String>?> verifier(
}
Future wireAuth(Angel app) async {
auth.serializer = (user) async => 1337;
auth.deserializer = (id) async => sampleUser;
//auth.serializer = (user) async => 1337;
//auth.deserializer = (id) async => sampleUser;
auth.strategies['local'] = LocalAuthStrategy(verifier);
await app.configure(auth.configureServer);
@ -42,8 +43,10 @@ void main() async {
app = Angel();
angelHttp = AngelHttp(app, useZone: false);
await app.configure(wireAuth);
app.get('/hello', (req, res) => 'Woo auth',
middleware: [auth.authenticate('local')]);
app.get('/hello', (req, res) {
// => 'Woo auth'
return 'Woo auth';
}); //, 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: [
@ -102,6 +105,8 @@ void main() async {
var authString = base64.encode('username:password'.runes.toList());
var response = await client.get(Uri.parse('$url/hello'),
headers: {'authorization': 'Basic $authString'});
print(response.statusCode);
print(response.body);
expect(response.body, equals('"Woo auth"'));
});

View file

@ -11,7 +11,8 @@ void main() {
secureCookies: true,
cookieDomain: 'SECURE',
jwtLifeSpan: threeDays.inMilliseconds,
);
serializer: (u) => u,
deserializer: (u) => u);
setUp(() => defaultCookie = Cookie('a', 'b'));

View file

@ -116,6 +116,7 @@ class Angel extends Routable {
///
/// Packages like `package:angel_configuration` populate this map
/// for you.
@override
final Map configuration = {};
/// A function that renders views.
@ -193,6 +194,7 @@ class Angel extends Routable {
/// Shuts down the server, and closes any open [StreamController]s.
///
/// The server will be **COMPLETELY DEFUNCT** after this operation!
@override
Future close() {
Future.forEach(services.values, (Service service) {
service.close();
@ -216,7 +218,7 @@ class Angel extends Routable {
@override
void dumpTree(
{callback(String tree)?,
{Function(String tree)? callback,
String header = 'Dumping route tree:',
String tab = ' ',
bool showMatchers = false}) {