Migrated angel_auth to NNBD
This commit is contained in:
parent
fdf1532074
commit
5302f635ac
13 changed files with 257 additions and 236 deletions
|
@ -11,7 +11,7 @@
|
||||||
* Added merge_map and updated to 2.0.0
|
* Added merge_map and updated to 2.0.0
|
||||||
* Added mock_request and updated to 2.0.0
|
* Added mock_request and updated to 2.0.0
|
||||||
* Updated angel_framework to 4.0.0 (Revisit TODO)
|
* Updated angel_framework to 4.0.0 (Revisit TODO)
|
||||||
* Updated angel_auth to 4.0.0 (todo)
|
* Updated angel_auth to 4.0.0
|
||||||
* Updated angel_configuration to 4.0.0 (todo)
|
* Updated angel_configuration to 4.0.0 (todo)
|
||||||
|
|
||||||
# 3.0.0 (Non NNBD)
|
# 3.0.0 (Non NNBD)
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:angel_auth/angel_auth.dart';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_framework/http.dart';
|
import 'package:angel_framework/http.dart';
|
||||||
|
|
||||||
main() async {
|
void main() async {
|
||||||
var app = Angel();
|
var app = Angel();
|
||||||
var auth = AngelAuth<User>();
|
var auth = AngelAuth<User?>();
|
||||||
|
|
||||||
auth.serializer = (user) => user.id;
|
auth.serializer = (user) => user!.id;
|
||||||
|
|
||||||
auth.deserializer = (id) => fetchAUserByIdSomehow(id);
|
auth.deserializer = (id) => fetchAUserByIdSomehow(id);
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ main() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
String id, username, password;
|
String? id, username, password;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<User> fetchAUserByIdSomehow(id) async {
|
Future<User> fetchAUserByIdSomehow(id) async {
|
||||||
|
|
|
@ -26,11 +26,11 @@ String decodeBase64(String str) {
|
||||||
|
|
||||||
class AuthToken {
|
class AuthToken {
|
||||||
final SplayTreeMap<String, String> _header =
|
final SplayTreeMap<String, String> _header =
|
||||||
SplayTreeMap.from({"alg": "HS256", "typ": "JWT"});
|
SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});
|
||||||
|
|
||||||
String ipAddress;
|
String? ipAddress;
|
||||||
DateTime issuedAt;
|
late DateTime issuedAt;
|
||||||
num lifeSpan;
|
num? lifeSpan;
|
||||||
var userId;
|
var userId;
|
||||||
Map<String, dynamic> payload = {};
|
Map<String, dynamic> payload = {};
|
||||||
|
|
||||||
|
@ -38,11 +38,14 @@ class AuthToken {
|
||||||
{this.ipAddress,
|
{this.ipAddress,
|
||||||
this.lifeSpan = -1,
|
this.lifeSpan = -1,
|
||||||
this.userId,
|
this.userId,
|
||||||
DateTime issuedAt,
|
DateTime? issuedAt,
|
||||||
Map payload = const {}}) {
|
Map payload = const {}}) {
|
||||||
this.issuedAt = issuedAt ?? DateTime.now();
|
this.issuedAt = issuedAt ?? DateTime.now();
|
||||||
this.payload.addAll(
|
this.payload.addAll(payload?.keys?.fold(
|
||||||
payload?.keys?.fold({}, (out, k) => out..[k.toString()] = payload[k]) ??
|
{},
|
||||||
|
((out, k) => out..[k.toString()] = payload[k])
|
||||||
|
as Map<String, dynamic>? Function(
|
||||||
|
Map<String, dynamic>?, dynamic)) ??
|
||||||
{});
|
{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,37 +54,40 @@ class AuthToken {
|
||||||
|
|
||||||
factory AuthToken.fromMap(Map data) {
|
factory AuthToken.fromMap(Map data) {
|
||||||
return AuthToken(
|
return AuthToken(
|
||||||
ipAddress: data["aud"].toString(),
|
ipAddress: data['aud'].toString(),
|
||||||
lifeSpan: data["exp"] as num,
|
lifeSpan: data['exp'] as num?,
|
||||||
issuedAt: DateTime.parse(data["iat"].toString()),
|
issuedAt: DateTime.parse(data['iat'].toString()),
|
||||||
userId: data["sub"],
|
userId: data['sub'],
|
||||||
payload: data["pld"] as Map ?? {});
|
payload: data['pld'] as Map? ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthToken.parse(String jwt) {
|
factory AuthToken.parse(String jwt) {
|
||||||
var split = jwt.split(".");
|
var split = jwt.split('.');
|
||||||
|
|
||||||
if (split.length != 3)
|
if (split.length != 3) {
|
||||||
throw AngelHttpException.notAuthenticated(message: "Invalid JWT.");
|
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
|
||||||
|
}
|
||||||
|
|
||||||
var payloadString = decodeBase64(split[1]);
|
var payloadString = decodeBase64(split[1]);
|
||||||
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthToken.validate(String jwt, Hmac hmac) {
|
factory AuthToken.validate(String jwt, Hmac hmac) {
|
||||||
var split = jwt.split(".");
|
var split = jwt.split('.');
|
||||||
|
|
||||||
if (split.length != 3)
|
if (split.length != 3) {
|
||||||
throw AngelHttpException.notAuthenticated(message: "Invalid JWT.");
|
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
|
||||||
|
}
|
||||||
|
|
||||||
// var headerString = decodeBase64(split[0]);
|
// var headerString = decodeBase64(split[0]);
|
||||||
var payloadString = decodeBase64(split[1]);
|
var payloadString = decodeBase64(split[1]);
|
||||||
var data = split[0] + "." + split[1];
|
var data = split[0] + '.' + split[1];
|
||||||
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]) {
|
||||||
throw AngelHttpException.notAuthenticated(
|
throw AngelHttpException.notAuthenticated(
|
||||||
message: "JWT payload does not match hashed version.");
|
message: 'JWT payload does not match hashed version.');
|
||||||
|
}
|
||||||
|
|
||||||
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
||||||
}
|
}
|
||||||
|
@ -89,9 +95,9 @@ class AuthToken {
|
||||||
String serialize(Hmac hmac) {
|
String serialize(Hmac hmac) {
|
||||||
var headerString = base64Url.encode(json.encode(_header).codeUnits);
|
var headerString = base64Url.encode(json.encode(_header).codeUnits);
|
||||||
var payloadString = base64Url.encode(json.encode(toJson()).codeUnits);
|
var payloadString = base64Url.encode(json.encode(toJson()).codeUnits);
|
||||||
var data = headerString + "." + payloadString;
|
var data = headerString + '.' + payloadString;
|
||||||
var signature = hmac.convert(data.codeUnits).bytes;
|
var signature = hmac.convert(data.codeUnits).bytes;
|
||||||
return data + "." + base64Url.encode(signature);
|
return data + '.' + base64Url.encode(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map toJson() {
|
Map toJson() {
|
||||||
|
@ -114,11 +120,12 @@ SplayTreeMap _splayify(Map map) {
|
||||||
return SplayTreeMap.from(data);
|
return SplayTreeMap.from(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
_splay(value) {
|
dynamic _splay(value) {
|
||||||
if (value is Iterable) {
|
if (value is Iterable) {
|
||||||
return value.map(_splay).toList();
|
return value.map(_splay).toList();
|
||||||
} else if (value is Map)
|
} else if (value is Map) {
|
||||||
return _splayify(value);
|
return _splayify(value);
|
||||||
else
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import 'package:charcode/ascii.dart';
|
import 'package:charcode/ascii.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:quiver/core.dart';
|
||||||
import 'package:quiver_hashcode/hashcode.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 {
|
||||||
/// 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;
|
||||||
|
|
||||||
/// The user's secret, other known as an "application secret".
|
/// The user's secret, other known as an "application secret".
|
||||||
final String clientSecret;
|
final String? clientSecret;
|
||||||
|
|
||||||
/// The user's redirect URI.
|
/// The user's redirect URI.
|
||||||
final Uri redirectUri;
|
final Uri redirectUri;
|
||||||
|
@ -27,9 +26,9 @@ class ExternalAuthOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ExternalAuthOptions(
|
factory ExternalAuthOptions(
|
||||||
{@required String clientId,
|
{required String? clientId,
|
||||||
@required String clientSecret,
|
required String? clientSecret,
|
||||||
@required redirectUri,
|
required redirectUri,
|
||||||
Iterable<String> scopes = const []}) {
|
Iterable<String> scopes = const []}) {
|
||||||
if (redirectUri is String) {
|
if (redirectUri is String) {
|
||||||
return ExternalAuthOptions._(
|
return ExternalAuthOptions._(
|
||||||
|
@ -51,8 +50,8 @@ class ExternalAuthOptions {
|
||||||
/// * `redirect_uri`
|
/// * `redirect_uri`
|
||||||
factory ExternalAuthOptions.fromMap(Map map) {
|
factory ExternalAuthOptions.fromMap(Map map) {
|
||||||
return ExternalAuthOptions(
|
return ExternalAuthOptions(
|
||||||
clientId: map['client_id'] as String,
|
clientId: map['client_id'] as String?,
|
||||||
clientSecret: map['client_secret'] as String,
|
clientSecret: map['client_secret'] as String?,
|
||||||
redirectUri: map['redirect_uri'],
|
redirectUri: map['redirect_uri'],
|
||||||
scopes: map['scopes'] is Iterable
|
scopes: map['scopes'] is Iterable
|
||||||
? ((map['scopes'] as Iterable).map((x) => x.toString()))
|
? ((map['scopes'] as Iterable).map((x) => x.toString()))
|
||||||
|
@ -73,10 +72,10 @@ class ExternalAuthOptions {
|
||||||
|
|
||||||
/// Creates a copy of this object, with the specified changes.
|
/// Creates a copy of this object, with the specified changes.
|
||||||
ExternalAuthOptions copyWith(
|
ExternalAuthOptions copyWith(
|
||||||
{String clientId,
|
{String? clientId,
|
||||||
String clientSecret,
|
String? clientSecret,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
Iterable<String> scopes}) {
|
Iterable<String>? scopes}) {
|
||||||
return ExternalAuthOptions(
|
return ExternalAuthOptions(
|
||||||
clientId: clientId ?? this.clientId,
|
clientId: clientId ?? this.clientId,
|
||||||
clientSecret: clientSecret ?? this.clientSecret,
|
clientSecret: clientSecret ?? this.clientSecret,
|
||||||
|
@ -111,14 +110,14 @@ class ExternalAuthOptions {
|
||||||
/// If no [asteriskCount] is given, then the number of asterisks will equal the length of
|
/// If no [asteriskCount] is given, then the number of asterisks will equal the length of
|
||||||
/// the actual [clientSecret].
|
/// the actual [clientSecret].
|
||||||
@override
|
@override
|
||||||
String toString({bool obscureSecret = true, int asteriskCount}) {
|
String toString({bool obscureSecret = true, int? asteriskCount}) {
|
||||||
String secret;
|
String? secret;
|
||||||
|
|
||||||
if (!obscureSecret) {
|
if (!obscureSecret) {
|
||||||
secret = clientSecret;
|
secret = clientSecret;
|
||||||
} else {
|
} else {
|
||||||
var codeUnits =
|
var codeUnits =
|
||||||
List<int>.filled(asteriskCount ?? clientSecret.length, $asterisk);
|
List<int>.filled(asteriskCount ?? clientSecret!.length, $asterisk);
|
||||||
secret = String.fromCharCodes(codeUnits);
|
secret = String.fromCharCodes(codeUnits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ import 'package:angel_framework/angel_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 `'angel_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.has<User>())
|
if (req.container!.has<User>()) {
|
||||||
return true;
|
return true;
|
||||||
else if (req.container.has<Future<User>>()) {
|
} else if (req.container!.has<Future<User>>()) {
|
||||||
await req.container.makeAsync<User>();
|
await req.container!.makeAsync<User>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,16 +26,18 @@ RequestHandler requireAuthentication<User>() {
|
||||||
if (throwError) {
|
if (throwError) {
|
||||||
res.statusCode = 403;
|
res.statusCode = 403;
|
||||||
throw AngelHttpException.forbidden();
|
throw AngelHttpException.forbidden();
|
||||||
} else
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (req.container.has<User>() || req.method == 'OPTIONS')
|
if (req.container!.has<User>() || req.method == 'OPTIONS') {
|
||||||
return true;
|
return true;
|
||||||
else if (req.container.has<Future<User>>()) {
|
} else if (req.container!.has<Future<User>>()) {
|
||||||
await req.container.makeAsync<User>();
|
await req.container!.makeAsync<User>();
|
||||||
return true;
|
return true;
|
||||||
} else
|
} else {
|
||||||
return _reject(res);
|
return _reject(res);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,17 @@ import 'dart:async';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'auth_token.dart';
|
import 'auth_token.dart';
|
||||||
|
|
||||||
typedef FutureOr AngelAuthCallback(
|
typedef AngelAuthCallback = FutureOr Function(
|
||||||
RequestContext req, ResponseContext res, String token);
|
RequestContext req, ResponseContext res, String token);
|
||||||
|
|
||||||
typedef FutureOr AngelAuthTokenCallback<User>(
|
typedef AngelAuthTokenCallback<User> = FutureOr Function(
|
||||||
RequestContext req, ResponseContext res, AuthToken token, User user);
|
RequestContext req, ResponseContext res, AuthToken token, User user);
|
||||||
|
|
||||||
class AngelAuthOptions<User> {
|
class AngelAuthOptions<User> {
|
||||||
AngelAuthCallback callback;
|
AngelAuthCallback? callback;
|
||||||
AngelAuthTokenCallback<User> tokenCallback;
|
AngelAuthTokenCallback<User>? tokenCallback;
|
||||||
String successRedirect;
|
String? successRedirect;
|
||||||
String failureRedirect;
|
String? failureRedirect;
|
||||||
|
|
||||||
/// If `false` (default: `true`), then successful authentication will return `true` and allow the
|
/// If `false` (default: `true`), then successful authentication will return `true` and allow the
|
||||||
/// execution of subsequent handlers, just like any other middleware.
|
/// execution of subsequent handlers, just like any other middleware.
|
||||||
|
@ -26,5 +26,5 @@ class AngelAuthOptions<User> {
|
||||||
this.tokenCallback,
|
this.tokenCallback,
|
||||||
this.canRespondWithJson = true,
|
this.canRespondWithJson = true,
|
||||||
this.successRedirect,
|
this.successRedirect,
|
||||||
String this.failureRedirect});
|
this.failureRedirect});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@ import 'strategy.dart';
|
||||||
|
|
||||||
/// Handles authentication within an Angel application.
|
/// Handles authentication within an Angel application.
|
||||||
class AngelAuth<User> {
|
class AngelAuth<User> {
|
||||||
Hmac _hs256;
|
Hmac? _hs256;
|
||||||
int _jwtLifeSpan;
|
int? _jwtLifeSpan;
|
||||||
final StreamController<User> _onLogin = StreamController<User>(),
|
final StreamController<User?> _onLogin = StreamController<User>(),
|
||||||
_onLogout = StreamController<User>();
|
_onLogout = StreamController<User>();
|
||||||
Math.Random _random = Math.Random.secure();
|
final Math.Random _random = Math.Random.secure();
|
||||||
final RegExp _rgxBearer = RegExp(r"^Bearer");
|
final RegExp _rgxBearer = RegExp(r'^Bearer');
|
||||||
|
|
||||||
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
|
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
|
||||||
final bool allowCookie;
|
final bool allowCookie;
|
||||||
|
@ -29,7 +29,7 @@ class AngelAuth<User> {
|
||||||
/// A domain to restrict emitted cookies to.
|
/// A domain to restrict emitted cookies to.
|
||||||
///
|
///
|
||||||
/// Only applies if [allowCookie] is `true`.
|
/// Only applies if [allowCookie] is `true`.
|
||||||
final String cookieDomain;
|
final String? cookieDomain;
|
||||||
|
|
||||||
/// A path to restrict emitted cookies to.
|
/// A path to restrict emitted cookies to.
|
||||||
///
|
///
|
||||||
|
@ -48,19 +48,19 @@ class AngelAuth<User> {
|
||||||
Map<String, AuthStrategy<User>> strategies = {};
|
Map<String, AuthStrategy<User>> strategies = {};
|
||||||
|
|
||||||
/// Serializes a user into a unique identifier associated only with one identity.
|
/// 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.
|
/// 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.
|
/// Fires the result of [deserializer] whenever a user signs in to the application.
|
||||||
Stream<User> get onLogin => _onLogin.stream;
|
Stream<User?> get onLogin => _onLogin.stream;
|
||||||
|
|
||||||
/// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application.
|
/// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application.
|
||||||
Stream<User> get onLogout => _onLogout.stream;
|
Stream<User?> get onLogout => _onLogout.stream;
|
||||||
|
|
||||||
/// The [Hmac] being used to encode JWT's.
|
/// The [Hmac] being used to encode JWT's.
|
||||||
Hmac get hmac => _hs256;
|
Hmac? get hmac => _hs256;
|
||||||
|
|
||||||
String _randomString(
|
String _randomString(
|
||||||
{int length = 32,
|
{int length = 32,
|
||||||
|
@ -73,10 +73,10 @@ class AngelAuth<User> {
|
||||||
|
|
||||||
/// `jwtLifeSpan` - should be in *milliseconds*.
|
/// `jwtLifeSpan` - should be in *milliseconds*.
|
||||||
AngelAuth(
|
AngelAuth(
|
||||||
{String jwtKey,
|
{String? jwtKey,
|
||||||
this.serializer,
|
this.serializer,
|
||||||
this.deserializer,
|
this.deserializer,
|
||||||
num jwtLifeSpan,
|
num? jwtLifeSpan,
|
||||||
this.allowCookie = true,
|
this.allowCookie = true,
|
||||||
this.allowTokenInQuery = true,
|
this.allowTokenInQuery = true,
|
||||||
this.enforceIp = true,
|
this.enforceIp = true,
|
||||||
|
@ -92,21 +92,24 @@ class AngelAuth<User> {
|
||||||
/// Configures an Angel server to decode and validate JSON Web tokens on demand,
|
/// Configures an Angel server to decode and validate JSON Web tokens on demand,
|
||||||
/// whenever an instance of [User] is injected.
|
/// whenever an instance of [User] is injected.
|
||||||
Future<void> configureServer(Angel app) async {
|
Future<void> configureServer(Angel app) async {
|
||||||
if (serializer == null)
|
if (serializer == null) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
|
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
|
||||||
if (deserializer == null)
|
}
|
||||||
|
if (deserializer == null) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
|
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
|
||||||
|
}
|
||||||
|
|
||||||
app.container.registerSingleton(this);
|
app.container!.registerSingleton(this);
|
||||||
if (runtimeType != AngelAuth)
|
if (runtimeType != AngelAuth) {
|
||||||
app.container.registerSingleton(this, as: AngelAuth);
|
app.container!.registerSingleton(this, as: AngelAuth);
|
||||||
|
}
|
||||||
|
|
||||||
if (!app.container.has<_AuthResult<User>>()) {
|
if (!app.container!.has<_AuthResult<User>>()) {
|
||||||
app.container
|
app.container!
|
||||||
.registerLazySingleton<Future<_AuthResult<User>>>((container) async {
|
.registerLazySingleton<Future<_AuthResult<User>>>((container) async {
|
||||||
var req = container.make<RequestContext>();
|
var req = container.make<RequestContext>()!;
|
||||||
var res = container.make<ResponseContext>();
|
var res = container.make<ResponseContext>();
|
||||||
var result = await _decodeJwt(req, res);
|
var result = await _decodeJwt(req, res);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
@ -116,20 +119,19 @@ class AngelAuth<User> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.container.registerLazySingleton<Future<User>>((container) async {
|
app.container!.registerLazySingleton<Future<User>>((container) async {
|
||||||
var result = await container.makeAsync<_AuthResult<User>>();
|
var result = await container.makeAsync<_AuthResult<User>>()!;
|
||||||
return result.user;
|
return result.user;
|
||||||
});
|
});
|
||||||
|
|
||||||
app.container.registerLazySingleton<Future<AuthToken>>((container) async {
|
app.container!
|
||||||
var result = await container.makeAsync<_AuthResult<User>>();
|
.registerLazySingleton<Future<AuthToken>>((container) async {
|
||||||
|
var result = await container.makeAsync<_AuthResult<User>>()!;
|
||||||
return result.token;
|
return result.token;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reviveTokenEndpoint != null) {
|
|
||||||
app.post(reviveTokenEndpoint, reviveJwt);
|
app.post(reviveTokenEndpoint, reviveJwt);
|
||||||
}
|
|
||||||
|
|
||||||
app.shutdownHooks.add((_) {
|
app.shutdownHooks.add((_) {
|
||||||
_onLogin.close();
|
_onLogin.close();
|
||||||
|
@ -137,17 +139,17 @@ 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.has<User>()) {
|
if (!req.container!.has<User>()) {
|
||||||
req.container.registerSingleton<User>(user);
|
req.container!.registerSingleton<User>(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.container.has<AuthToken>()) {
|
if (!req.container!.has<AuthToken>()) {
|
||||||
req.container.registerSingleton<AuthToken>(token);
|
req.container!.registerSingleton<AuthToken>(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowCookie == true) {
|
if (allowCookie == true) {
|
||||||
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
_addProtectedCookie(res!, 'token', token.serialize(_hs256!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +176,7 @@ class AngelAuth<User> {
|
||||||
/// ```
|
/// ```
|
||||||
@deprecated
|
@deprecated
|
||||||
Future decodeJwt(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);
|
||||||
|
@ -182,28 +184,30 @@ class AngelAuth<User> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<_AuthResult<User>> _decodeJwt(
|
Future<_AuthResult<User>?> _decodeJwt(
|
||||||
RequestContext req, ResponseContext res) async {
|
RequestContext req, ResponseContext? res) async {
|
||||||
String jwt = getJwt(req);
|
var jwt = getJwt(req);
|
||||||
|
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
var token = AuthToken.validate(jwt, _hs256);
|
var token = AuthToken.validate(jwt, _hs256!);
|
||||||
|
|
||||||
if (enforceIp) {
|
if (enforceIp) {
|
||||||
if (req.ip != null && req.ip != token.ipAddress)
|
if (req.ip != token.ipAddress) {
|
||||||
throw AngelHttpException.forbidden(
|
throw AngelHttpException.forbidden(
|
||||||
message: "JWT cannot be accessed from this IP address.");
|
message: "JWT cannot be accessed from this IP address.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.lifeSpan > -1) {
|
|
||||||
var expiry =
|
|
||||||
token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt()));
|
|
||||||
|
|
||||||
if (!expiry.isAfter(DateTime.now()))
|
|
||||||
throw AngelHttpException.forbidden(message: "Expired JWT.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await deserializer(token.userId);
|
if (token.lifeSpan! > -1) {
|
||||||
|
var expiry =
|
||||||
|
token.issuedAt.add(Duration(milliseconds: token.lifeSpan!.toInt()));
|
||||||
|
|
||||||
|
if (!expiry.isAfter(DateTime.now())) {
|
||||||
|
throw AngelHttpException.forbidden(message: "Expired JWT.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await deserializer!(token.userId);
|
||||||
_apply(req, res, token, user);
|
_apply(req, res, token, user);
|
||||||
return _AuthResult(user, token);
|
return _AuthResult(user, token);
|
||||||
}
|
}
|
||||||
|
@ -212,19 +216,20 @@ class AngelAuth<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a JWT from a request, if any was sent at all.
|
/// Retrieves a JWT from a request, if any was sent at all.
|
||||||
String getJwt(RequestContext req) {
|
String? getJwt(RequestContext req) {
|
||||||
if (req.headers.value("Authorization") != null) {
|
if (req.headers!.value("Authorization") != null) {
|
||||||
final authHeader = req.headers.value("Authorization");
|
final authHeader = req.headers!.value("Authorization")!;
|
||||||
|
|
||||||
// Allow Basic auth to fall through
|
// Allow Basic auth to fall through
|
||||||
if (_rgxBearer.hasMatch(authHeader))
|
if (_rgxBearer.hasMatch(authHeader)) {
|
||||||
return authHeader.replaceAll(_rgxBearer, "").trim();
|
return authHeader.replaceAll(_rgxBearer, "").trim();
|
||||||
|
}
|
||||||
} 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;
|
||||||
} else if (allowTokenInQuery &&
|
} else if (allowTokenInQuery &&
|
||||||
req.uri.queryParameters['token'] is String) {
|
req.uri!.queryParameters['token'] is String) {
|
||||||
return req.uri.queryParameters['token']?.toString();
|
return req.uri!.queryParameters['token']?.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -243,10 +248,10 @@ class AngelAuth<User> {
|
||||||
cookie.secure = true;
|
cookie.secure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_jwtLifeSpan > 0) {
|
if (_jwtLifeSpan! > 0) {
|
||||||
cookie.maxAge ??= _jwtLifeSpan < 0 ? -1 : _jwtLifeSpan ~/ 1000;
|
cookie.maxAge ??= _jwtLifeSpan! < 0 ? -1 : _jwtLifeSpan! ~/ 1000;
|
||||||
cookie.expires ??=
|
cookie.expires ??=
|
||||||
DateTime.now().add(Duration(milliseconds: _jwtLifeSpan));
|
DateTime.now().add(Duration(milliseconds: _jwtLifeSpan!));
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie.domain ??= cookieDomain;
|
cookie.domain ??= cookieDomain;
|
||||||
|
@ -261,22 +266,23 @@ class AngelAuth<User> {
|
||||||
var jwt = getJwt(req);
|
var jwt = getJwt(req);
|
||||||
|
|
||||||
if (jwt == null) {
|
if (jwt == null) {
|
||||||
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) {
|
||||||
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) {
|
||||||
throw AngelHttpException.forbidden(
|
throw AngelHttpException.forbidden(
|
||||||
message: "JWT cannot be accessed from this IP address.");
|
message: "JWT cannot be accessed from this IP address.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (token.lifeSpan > -1) {
|
if (token.lifeSpan! > -1) {
|
||||||
var expiry = token.issuedAt
|
var expiry = token.issuedAt
|
||||||
.add(Duration(milliseconds: token.lifeSpan.toInt()));
|
.add(Duration(milliseconds: token.lifeSpan!.toInt()));
|
||||||
|
|
||||||
if (!expiry.isAfter(DateTime.now())) {
|
if (!expiry.isAfter(DateTime.now())) {
|
||||||
//print(
|
//print(
|
||||||
|
@ -287,11 +293,11 @@ class AngelAuth<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowCookie) {
|
if (allowCookie) {
|
||||||
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
_addProtectedCookie(res, 'token', token.serialize(_hs256!));
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = await deserializer(token.userId);
|
final data = await deserializer!(token.userId);
|
||||||
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;
|
||||||
|
@ -307,14 +313,14 @@ class AngelAuth<User> {
|
||||||
/// or a `401 Not Authenticated` is thrown, if it is the last one.
|
/// or a `401 Not Authenticated` is thrown, if it is the last one.
|
||||||
///
|
///
|
||||||
/// Any other result is considered an authenticated user, and terminates the loop.
|
/// Any other result is considered an authenticated user, and terminates the loop.
|
||||||
RequestHandler authenticate(type, [AngelAuthOptions<User> options]) {
|
RequestHandler authenticate(type, [AngelAuthOptions<User>? options]) {
|
||||||
return (RequestContext req, ResponseContext res) async {
|
return (RequestContext req, ResponseContext res) async {
|
||||||
List<String> names = [];
|
var names = <String>[];
|
||||||
var arr = type is Iterable
|
var arr = type is Iterable
|
||||||
? type.map((x) => x.toString()).toList()
|
? type.map((x) => x.toString()).toList()
|
||||||
: [type.toString()];
|
: [type.toString()];
|
||||||
|
|
||||||
for (String t in arr) {
|
for (var t in arr) {
|
||||||
var n = t
|
var n = t
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
|
@ -323,34 +329,34 @@ class AngelAuth<User> {
|
||||||
names.addAll(n);
|
names.addAll(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < names.length; i++) {
|
for (var i = 0; i < names.length; i++) {
|
||||||
var name = names[i];
|
var name = names[i];
|
||||||
|
|
||||||
var strategy = strategies[name] ??=
|
var strategy = strategies[name] ??=
|
||||||
throw ArgumentError('No strategy "$name" found.');
|
throw ArgumentError('No strategy "$name" found.');
|
||||||
|
|
||||||
var hasExisting = req.container.has<User>();
|
var hasExisting = req.container!.has<User>();
|
||||||
var result = hasExisting
|
var result = hasExisting
|
||||||
? req.container.make<User>()
|
? req.container!.make<User>()
|
||||||
: await strategy.authenticate(req, res, options);
|
: await strategy.authenticate(req, res, options!);
|
||||||
if (result == true)
|
if (result == true) {
|
||||||
return result;
|
return result;
|
||||||
else if (result != false && result != null) {
|
} else if (result != false && result != null) {
|
||||||
var userId = await serializer(result);
|
var userId = await serializer!(result);
|
||||||
|
|
||||||
// Create JWT
|
// Create JWT
|
||||||
var token = AuthToken(
|
var token = AuthToken(
|
||||||
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?.tokenCallback != null) {
|
||||||
if (!req.container.has<User>()) {
|
if (!req.container!.has<User>()) {
|
||||||
req.container.registerSingleton<User>(result);
|
req.container!.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!);
|
||||||
}
|
}
|
||||||
|
|
||||||
_apply(req, res, token, result);
|
_apply(req, res, token, result);
|
||||||
|
@ -360,17 +366,17 @@ class AngelAuth<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.callback != null) {
|
if (options?.callback != null) {
|
||||||
return await options.callback(req, res, jwt);
|
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 != false &&
|
||||||
req.accepts('application/json')) {
|
req.accepts('application/json')) {
|
||||||
var user = hasExisting
|
var user = hasExisting
|
||||||
? result
|
? result
|
||||||
: await deserializer(await serializer(result));
|
: await deserializer!(await serializer!(result));
|
||||||
_onLogin.add(user);
|
_onLogin.add(user);
|
||||||
return {"data": user, "token": jwt};
|
return {"data": user, "token": jwt};
|
||||||
}
|
}
|
||||||
|
@ -381,47 +387,48 @@ class AngelAuth<User> {
|
||||||
// Check if not redirect
|
// Check if not redirect
|
||||||
if (res.statusCode == 301 ||
|
if (res.statusCode == 301 ||
|
||||||
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?.failureRedirect != null) {
|
||||||
await res.redirect(options.failureRedirect);
|
await res.redirect(options!.failureRedirect);
|
||||||
return false;
|
return false;
|
||||||
} else
|
} else {
|
||||||
throw AngelHttpException.notAuthenticated();
|
throw AngelHttpException.notAuthenticated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log a user in on-demand.
|
/// Log a user in on-demand.
|
||||||
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
|
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
|
||||||
var user = await deserializer(token.userId);
|
var user = await deserializer!(token.userId);
|
||||||
_apply(req, res, token, user);
|
_apply(req, res, token, user);
|
||||||
_onLogin.add(user);
|
_onLogin.add(user);
|
||||||
|
|
||||||
if (allowCookie) {
|
if (allowCookie) {
|
||||||
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
_addProtectedCookie(res, 'token', token.serialize(_hs256!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log a user in on-demand.
|
/// Log a user in on-demand.
|
||||||
Future loginById(userId, RequestContext req, ResponseContext res) async {
|
Future loginById(userId, RequestContext req, ResponseContext res) async {
|
||||||
var user = await deserializer(userId);
|
var user = await deserializer!(userId);
|
||||||
var token =
|
var token =
|
||||||
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
||||||
_apply(req, res, token, user);
|
_apply(req, res, token, user);
|
||||||
_onLogin.add(user);
|
_onLogin.add(user);
|
||||||
|
|
||||||
if (allowCookie) {
|
if (allowCookie) {
|
||||||
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
_addProtectedCookie(res, 'token', token.serialize(_hs256!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log an authenticated user out.
|
/// Log an authenticated user out.
|
||||||
RequestHandler logout([AngelAuthOptions<User> options]) {
|
RequestHandler logout([AngelAuthOptions<User>? options]) {
|
||||||
return (RequestContext req, ResponseContext res) async {
|
return (RequestContext req, ResponseContext res) async {
|
||||||
if (req.container.has<User>()) {
|
if (req.container!.has<User>()) {
|
||||||
var user = req.container.make<User>();
|
var user = req.container!.make<User>();
|
||||||
_onLogout.add(user);
|
_onLogout.add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,7 +439,7 @@ class AngelAuth<User> {
|
||||||
|
|
||||||
if (options != null &&
|
if (options != null &&
|
||||||
options.successRedirect != null &&
|
options.successRedirect != null &&
|
||||||
options.successRedirect.isNotEmpty) {
|
options.successRedirect!.isNotEmpty) {
|
||||||
await res.redirect(options.successRedirect);
|
await res.redirect(options.successRedirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,16 @@ import 'package:angel_framework/angel_framework.dart';
|
||||||
import '../options.dart';
|
import '../options.dart';
|
||||||
import '../strategy.dart';
|
import '../strategy.dart';
|
||||||
|
|
||||||
bool _validateString(String str) => str != null && str.isNotEmpty;
|
bool _validateString(String? str) => str != null && str.isNotEmpty;
|
||||||
|
|
||||||
/// Determines the validity of an incoming username and password.
|
/// Determines the validity of an incoming username and password.
|
||||||
typedef FutureOr<User> LocalAuthVerifier<User>(
|
// typedef FutureOr<User> LocalAuthVerifier<User>(String? username, String? password);
|
||||||
String username, String password);
|
typedef LocalAuthVerifier<User> = FutureOr<User> Function(
|
||||||
|
String? username, String? password);
|
||||||
|
|
||||||
class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
||||||
RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
|
final RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
|
||||||
RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
|
final RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
|
||||||
|
|
||||||
LocalAuthVerifier<User> verifier;
|
LocalAuthVerifier<User> verifier;
|
||||||
String usernameField;
|
String usernameField;
|
||||||
|
@ -23,33 +24,32 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
||||||
String realm;
|
String realm;
|
||||||
|
|
||||||
LocalAuthStrategy(this.verifier,
|
LocalAuthStrategy(this.verifier,
|
||||||
{String this.usernameField = 'username',
|
{this.usernameField = 'username',
|
||||||
String this.passwordField = 'password',
|
this.passwordField = 'password',
|
||||||
String this.invalidMessage =
|
this.invalidMessage = 'Please provide a valid username and password.',
|
||||||
'Please provide a valid username and password.',
|
this.allowBasic = true,
|
||||||
bool this.allowBasic = true,
|
this.forceBasic = false,
|
||||||
bool this.forceBasic = false,
|
this.realm = 'Authentication is required.'});
|
||||||
String this.realm = 'Authentication is required.'});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<User> authenticate(RequestContext req, ResponseContext res,
|
Future<User?> authenticate(RequestContext req, ResponseContext res,
|
||||||
[AngelAuthOptions options_]) async {
|
[AngelAuthOptions? options_]) async {
|
||||||
AngelAuthOptions options = options_ ?? AngelAuthOptions();
|
var options = options_ ?? AngelAuthOptions();
|
||||||
User verificationResult;
|
User? verificationResult;
|
||||||
|
|
||||||
if (allowBasic) {
|
if (allowBasic) {
|
||||||
String authHeader = req.headers.value('authorization') ?? "";
|
var authHeader = req.headers!.value('authorization') ?? '';
|
||||||
|
|
||||||
if (_rgxBasic.hasMatch(authHeader)) {
|
if (_rgxBasic.hasMatch(authHeader)) {
|
||||||
String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1);
|
var base64AuthString = _rgxBasic.firstMatch(authHeader)!.group(1)!;
|
||||||
String authString =
|
var authString = String.fromCharCodes(base64.decode(base64AuthString));
|
||||||
String.fromCharCodes(base64.decode(base64AuthString));
|
|
||||||
if (_rgxUsrPass.hasMatch(authString)) {
|
if (_rgxUsrPass.hasMatch(authString)) {
|
||||||
Match usrPassMatch = _rgxUsrPass.firstMatch(authString);
|
Match usrPassMatch = _rgxUsrPass.firstMatch(authString)!;
|
||||||
verificationResult =
|
verificationResult =
|
||||||
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
|
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
|
||||||
} else
|
} else {
|
||||||
throw AngelHttpException.badRequest(errors: [invalidMessage]);
|
throw AngelHttpException.badRequest(errors: [invalidMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
if (verificationResult == false || verificationResult == null) {
|
if (verificationResult == false || verificationResult == null) {
|
||||||
res
|
res
|
||||||
|
@ -68,27 +68,29 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
||||||
.parseBody()
|
.parseBody()
|
||||||
.then((_) => req.bodyAsMap)
|
.then((_) => req.bodyAsMap)
|
||||||
.catchError((_) => <String, dynamic>{});
|
.catchError((_) => <String, dynamic>{});
|
||||||
|
if (body != null) {
|
||||||
if (_validateString(body[usernameField]?.toString()) &&
|
if (_validateString(body[usernameField]?.toString()) &&
|
||||||
_validateString(body[passwordField]?.toString())) {
|
_validateString(body[passwordField]?.toString())) {
|
||||||
verificationResult = await verifier(
|
verificationResult = await verifier(
|
||||||
body[usernameField]?.toString(), body[passwordField]?.toString());
|
body[usernameField]?.toString(), body[passwordField]?.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (verificationResult == false || verificationResult == null) {
|
if (verificationResult == false || verificationResult == null) {
|
||||||
if (options.failureRedirect != null &&
|
if (options.failureRedirect != null &&
|
||||||
options.failureRedirect.isNotEmpty) {
|
options.failureRedirect!.isNotEmpty) {
|
||||||
await res.redirect(options.failureRedirect, code: 401);
|
await res.redirect(options.failureRedirect, code: 401);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceBasic) {
|
if (forceBasic) {
|
||||||
res.headers['www-authenticate'] = 'Basic realm="$realm"';
|
res.headers['www-authenticate'] = 'Basic realm="$realm"';
|
||||||
throw AngelHttpException.notAuthenticated();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} else if (verificationResult != null && verificationResult != false) {
|
} else if (verificationResult != false) {
|
||||||
return verificationResult;
|
return verificationResult;
|
||||||
} else {
|
} else {
|
||||||
throw AngelHttpException.notAuthenticated();
|
throw AngelHttpException.notAuthenticated();
|
||||||
|
|
|
@ -5,6 +5,6 @@ import 'options.dart';
|
||||||
/// A function that handles login and signup for an Angel application.
|
/// A function that handles login and signup for an Angel application.
|
||||||
abstract class AuthStrategy<User> {
|
abstract class AuthStrategy<User> {
|
||||||
/// Authenticates or rejects an incoming user.
|
/// Authenticates or rejects an incoming user.
|
||||||
FutureOr<User> authenticate(RequestContext req, ResponseContext res,
|
FutureOr<User?> authenticate(RequestContext req, ResponseContext res,
|
||||||
[AngelAuthOptions<User> options]);
|
[AngelAuthOptions<User> options]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
name: angel_auth
|
name: angel_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: 3.0.0
|
version: 4.0.0
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_auth
|
homepage: https://github.com/angel-dart/angel_auth
|
||||||
publish_to: none
|
publish_to: none
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.10.0 <3.0.0"
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework:
|
angel_framework:
|
||||||
git:
|
git:
|
||||||
|
@ -17,7 +17,7 @@ dependencies:
|
||||||
crypto: ^3.0.0
|
crypto: ^3.0.0
|
||||||
http_parser: ^4.0.0
|
http_parser: ^4.0.0
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
quiver_hashcode: ^3.0.0+1
|
quiver: ^3.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
http: ^0.13.1
|
http: ^0.13.1
|
||||||
io: ^1.0.0
|
io: ^1.0.0
|
||||||
|
|
|
@ -3,20 +3,22 @@ import 'package:angel_auth/angel_auth.dart';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_framework/http.dart';
|
import 'package:angel_framework/http.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:collection/collection.dart' show IterableExtension;
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:io/ansi.dart';
|
import 'package:io/ansi.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
class User extends Model {
|
class User extends Model {
|
||||||
String username, password;
|
String? username, password;
|
||||||
|
|
||||||
User({this.username, this.password});
|
User({this.username, this.password});
|
||||||
|
|
||||||
static User parse(Map map) {
|
static User parse(Map map) {
|
||||||
return User(
|
return User(
|
||||||
username: map['username'] as String,
|
username: map['username'] as String?,
|
||||||
password: map['password'] as String,
|
password: map['password'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,27 +33,27 @@ class User extends Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
void main() {
|
||||||
Angel app;
|
Angel? app;
|
||||||
AngelHttp angelHttp;
|
late AngelHttp angelHttp;
|
||||||
AngelAuth<User> auth;
|
AngelAuth<User?> auth;
|
||||||
http.Client client;
|
http.Client? client;
|
||||||
HttpServer server;
|
HttpServer server;
|
||||||
String url;
|
String? url;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
hierarchicalLoggingEnabled = true;
|
hierarchicalLoggingEnabled = true;
|
||||||
app = Angel();
|
app = Angel();
|
||||||
angelHttp = AngelHttp(app);
|
angelHttp = AngelHttp(app);
|
||||||
app.use('/users', MapService());
|
app!.use('/users', MapService());
|
||||||
|
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
app.logger = Logger('angel_auth')
|
app!.logger = Logger('angel_auth')
|
||||||
..level = Level.FINEST
|
..level = Level.FINEST
|
||||||
..onRecord.listen((rec) {
|
..onRecord.listen((rec) {
|
||||||
print(rec);
|
print(rec);
|
||||||
|
@ -65,28 +67,30 @@ 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?>();
|
||||||
auth.serializer = (u) => u.id;
|
auth.serializer = (u) => u!.id;
|
||||||
auth.deserializer =
|
auth.deserializer =
|
||||||
(id) async => await app.findService('users').read(id) as User;
|
(id) async => await app!.findService('users')!.read(id) as User;
|
||||||
|
|
||||||
await app.configure(auth.configureServer);
|
await app!.configure(auth.configureServer);
|
||||||
|
|
||||||
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());
|
||||||
return users.firstWhere(
|
|
||||||
(user) => user.username == username && user.password == password,
|
var result = users.firstWhereOrNull(
|
||||||
orElse: () => null);
|
(user) => user.username == username && user.password == password);
|
||||||
|
|
||||||
|
return Future.value(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post(
|
app!.post(
|
||||||
'/login',
|
'/login',
|
||||||
auth.authenticate('local',
|
auth.authenticate('local',
|
||||||
AngelAuthOptions(callback: (req, res, token) {
|
AngelAuthOptions(callback: (req, res, token) {
|
||||||
|
@ -95,10 +99,10 @@ main() {
|
||||||
..close();
|
..close();
|
||||||
})));
|
})));
|
||||||
|
|
||||||
app.chain([
|
app!.chain([
|
||||||
(req, res) {
|
(req, res) {
|
||||||
if (!req.container.has<User>()) {
|
if (!req.container!.has<User>()) {
|
||||||
req.container.registerSingleton<User>(
|
req.container!.registerSingleton<User>(
|
||||||
User(username: req.params['name']?.toString()));
|
User(username: req.params['name']?.toString()));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -114,7 +118,7 @@ main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
client.close();
|
client!.close();
|
||||||
await angelHttp.close();
|
await angelHttp.close();
|
||||||
app = null;
|
app = null;
|
||||||
client = null;
|
client = null;
|
||||||
|
@ -122,7 +126,7 @@ main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('login', () async {
|
test('login', () async {
|
||||||
final response = await client.post(Uri.parse('$url/login'),
|
final response = await client!.post(Uri.parse('$url/login'),
|
||||||
body: {'username': 'jdoe1', 'password': 'password'});
|
body: {'username': 'jdoe1', 'password': 'password'});
|
||||||
print('Response: ${response.body}');
|
print('Response: ${response.body}');
|
||||||
expect(response.body, equals('Hello!'));
|
expect(response.body, equals('Hello!'));
|
||||||
|
@ -132,7 +136,7 @@ main() {
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
test('preserve existing user', () async {
|
test('preserve existing user', () async {
|
||||||
final response = await client.post(Uri.parse('$url/existing/foo'),
|
final response = await client!.post(Uri.parse('$url/existing/foo'),
|
||||||
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}');
|
||||||
|
|
|
@ -7,17 +7,17 @@ import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:test/test.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>?>();
|
||||||
var headers = <String, String>{'accept': 'application/json'};
|
var headers = <String, String>{'accept': 'application/json'};
|
||||||
var localOpts = AngelAuthOptions<Map<String, String>>(
|
var localOpts = AngelAuthOptions<Map<String, String>>(
|
||||||
failureRedirect: '/failure', successRedirect: '/success');
|
failureRedirect: '/failure', successRedirect: '/success');
|
||||||
Map<String, String> sampleUser = {'hello': 'world'};
|
Map<String, String> sampleUser = {'hello': 'world'};
|
||||||
|
|
||||||
Future<Map<String, String>> verifier(String username, String password) async {
|
Future<Map<String, String>> verifier(String? username, String? password) async {
|
||||||
if (username == 'username' && password == 'password') {
|
if (username == 'username' && password == 'password') {
|
||||||
return sampleUser;
|
return sampleUser;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
throw ArgumentError('Unexpected type for data');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ Future wireAuth(Angel app) async {
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
Angel app;
|
Angel app;
|
||||||
AngelHttp angelHttp;
|
late AngelHttp angelHttp;
|
||||||
http.Client client;
|
http.Client? client;
|
||||||
String url;
|
String? url;
|
||||||
String basicAuthUrl;
|
String? basicAuthUrl;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
client = http.Client();
|
client = http.Client();
|
||||||
|
@ -72,7 +72,7 @@ void main() async {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can use "auth" as middleware', () async {
|
test('can use "auth" as middleware', () async {
|
||||||
var response = await client.get(Uri.parse('$url/success'),
|
var response = await client!.get(Uri.parse('$url/success'),
|
||||||
headers: {'Accept': 'application/json'});
|
headers: {'Accept': 'application/json'});
|
||||||
print(response.body);
|
print(response.body);
|
||||||
expect(response.statusCode, equals(403));
|
expect(response.statusCode, equals(403));
|
||||||
|
@ -80,7 +80,7 @@ void main() async {
|
||||||
|
|
||||||
test('successRedirect', () async {
|
test('successRedirect', () async {
|
||||||
var postData = {'username': 'username', 'password': 'password'};
|
var postData = {'username': 'username', 'password': 'password'};
|
||||||
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'});
|
||||||
expect(response.statusCode, equals(302));
|
expect(response.statusCode, equals(302));
|
||||||
|
@ -89,7 +89,7 @@ void main() async {
|
||||||
|
|
||||||
test('failureRedirect', () async {
|
test('failureRedirect', () async {
|
||||||
var postData = {'username': 'password', 'password': 'username'};
|
var postData = {'username': 'password', 'password': 'username'};
|
||||||
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('Login response: ${response.body}');
|
||||||
|
@ -99,13 +99,13 @@ void main() async {
|
||||||
|
|
||||||
test('allow basic', () async {
|
test('allow basic', () 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'});
|
||||||
expect(response.body, equals('"Woo auth"'));
|
expect(response.body, equals('"Woo auth"'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow basic via URL encoding', () async {
|
test('allow basic via URL encoding', () async {
|
||||||
var response = await client.get(Uri.parse('$basicAuthUrl/hello'));
|
var response = await client!.get(Uri.parse('$basicAuthUrl/hello'));
|
||||||
expect(response.body, equals('"Woo auth"'));
|
expect(response.body, equals('"Woo auth"'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ void main() async {
|
||||||
auth.strategies.clear();
|
auth.strategies.clear();
|
||||||
auth.strategies['local'] =
|
auth.strategies['local'] =
|
||||||
LocalAuthStrategy(verifier, forceBasic: true, realm: 'test');
|
LocalAuthStrategy(verifier, forceBasic: true, realm: 'test');
|
||||||
var response = await client.get(Uri.parse('$url/hello'), headers: {
|
var response = await client!.get(Uri.parse('$url/hello'), headers: {
|
||||||
'accept': 'application/json',
|
'accept': 'application/json',
|
||||||
'content-type': 'application/json'
|
'content-type': 'application/json'
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'package:test/test.dart';
|
||||||
const Duration threeDays = const Duration(days: 3);
|
const Duration threeDays = const Duration(days: 3);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Cookie defaultCookie;
|
late Cookie defaultCookie;
|
||||||
var auth = AngelAuth(
|
var auth = AngelAuth(
|
||||||
secureCookies: true,
|
secureCookies: true,
|
||||||
cookieDomain: 'SECURE',
|
cookieDomain: 'SECURE',
|
||||||
|
@ -21,7 +21,7 @@ void main() {
|
||||||
|
|
||||||
test('sets expires', () {
|
test('sets expires', () {
|
||||||
var now = DateTime.now();
|
var now = DateTime.now();
|
||||||
var expiry = auth.protectCookie(defaultCookie).expires;
|
var expiry = auth.protectCookie(defaultCookie).expires!;
|
||||||
var diff = expiry.difference(now);
|
var diff = expiry.difference(now);
|
||||||
expect(diff.inSeconds, threeDays.inSeconds);
|
expect(diff.inSeconds, threeDays.inSeconds);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue