Updated auth
This commit is contained in:
parent
7f9e3c58cc
commit
740b78ba00
11 changed files with 105 additions and 68 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"'));
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ void main() {
|
|||
secureCookies: true,
|
||||
cookieDomain: 'SECURE',
|
||||
jwtLifeSpan: threeDays.inMilliseconds,
|
||||
);
|
||||
serializer: (u) => u,
|
||||
deserializer: (u) => u);
|
||||
|
||||
setUp(() => defaultCookie = Cookie('a', 'b'));
|
||||
|
||||
|
|
|
@ -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}) {
|
||||
|
|
Loading…
Reference in a new issue