138 lines
4.5 KiB
Dart
138 lines
4.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:angel3_framework/angel3_framework.dart';
|
|
import '../options.dart';
|
|
import '../strategy.dart';
|
|
|
|
/// Determines the validity of an incoming username and password.
|
|
// typedef FutureOr<User> LocalAuthVerifier<User>(String? username, String? password);
|
|
typedef LocalAuthVerifier<User> = FutureOr<User?> Function(
|
|
String? username, String? password);
|
|
|
|
class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
|
final _log = Logger('LocalAuthStrategy');
|
|
|
|
final RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
|
|
final RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
|
|
|
|
LocalAuthVerifier<User> verifier;
|
|
String usernameField;
|
|
String passwordField;
|
|
String invalidMessage;
|
|
final bool allowBasic;
|
|
final bool forceBasic;
|
|
String realm;
|
|
|
|
LocalAuthStrategy(this.verifier,
|
|
{this.usernameField = 'username',
|
|
this.passwordField = 'password',
|
|
this.invalidMessage = 'Please provide a valid username and password.',
|
|
this.allowBasic = false,
|
|
this.forceBasic = false,
|
|
this.realm = 'Authentication is required.'}) {
|
|
_log.info('Using LocalAuthStrategy');
|
|
}
|
|
|
|
@override
|
|
Future<User?> authenticate(RequestContext req, ResponseContext res,
|
|
[AngelAuthOptions? options]) async {
|
|
var localOptions = options ?? AngelAuthOptions();
|
|
User? verificationResult;
|
|
|
|
if (allowBasic) {
|
|
var authHeader = req.headers?.value('authorization') ?? '';
|
|
|
|
if (_rgxBasic.hasMatch(authHeader)) {
|
|
var base64AuthString = _rgxBasic.firstMatch(authHeader)?.group(1);
|
|
if (base64AuthString == null) {
|
|
return null;
|
|
}
|
|
var authString = String.fromCharCodes(base64.decode(base64AuthString));
|
|
if (_rgxUsrPass.hasMatch(authString)) {
|
|
Match usrPassMatch = _rgxUsrPass.firstMatch(authString)!;
|
|
verificationResult =
|
|
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
|
|
} else {
|
|
_log.warning('Bad request: $invalidMessage');
|
|
throw ProtevusHttpException.badRequest(errors: [invalidMessage]);
|
|
}
|
|
|
|
if (verificationResult == null) {
|
|
res
|
|
..statusCode = 401
|
|
..headers['www-authenticate'] = 'Basic realm="$realm"';
|
|
await res.close();
|
|
return null;
|
|
}
|
|
|
|
//Allow non-null to pass through
|
|
//return verificationResult;
|
|
}
|
|
} else {
|
|
var body = await req
|
|
.parseBody()
|
|
.then((_) => req.bodyAsMap)
|
|
.catchError((_) => <String, dynamic>{});
|
|
if (_validateString(body[usernameField]?.toString()) &&
|
|
_validateString(body[passwordField]?.toString())) {
|
|
verificationResult = await verifier(
|
|
body[usernameField]?.toString(), body[passwordField]?.toString());
|
|
}
|
|
}
|
|
|
|
// User authentication succeeded can return Map(one element), User(non null) or true
|
|
if (verificationResult != null && verificationResult != false) {
|
|
if (verificationResult is Map && verificationResult.isNotEmpty) {
|
|
return verificationResult;
|
|
} else if (verificationResult is! Map) {
|
|
return verificationResult;
|
|
}
|
|
}
|
|
|
|
// Force basic if set
|
|
if (forceBasic) {
|
|
//res.headers['www-authenticate'] = 'Basic realm="$realm"';
|
|
res
|
|
..statusCode = 401
|
|
..headers['www-authenticate'] = 'Basic realm="$realm"';
|
|
await res.close();
|
|
return null;
|
|
}
|
|
|
|
// Redirect failed authentication
|
|
if (localOptions.failureRedirect != null &&
|
|
localOptions.failureRedirect!.isNotEmpty) {
|
|
await res.redirect(localOptions.failureRedirect, code: 401);
|
|
return null;
|
|
}
|
|
|
|
_log.info('Not authenticated');
|
|
throw ProtevusHttpException.notAuthenticated();
|
|
|
|
/*
|
|
if (verificationResult is Map && verificationResult.isEmpty) {
|
|
if (localOptions.failureRedirect != null &&
|
|
localOptions.failureRedirect!.isNotEmpty) {
|
|
await res.redirect(localOptions.failureRedirect, code: 401);
|
|
return null;
|
|
}
|
|
|
|
if (forceBasic) {
|
|
res.headers['www-authenticate'] = 'Basic realm="$realm"';
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
} else if (verificationResult != false ||
|
|
(verificationResult is Map && verificationResult.isNotEmpty)) {
|
|
return verificationResult;
|
|
} else {
|
|
_log.info('Not authenticated');
|
|
throw AngelHttpException.notAuthenticated();
|
|
}
|
|
*/
|
|
}
|
|
|
|
bool _validateString(String? str) => str != null && str.isNotEmpty;
|
|
}
|