mirror of
https://github.com/foss42/apidash.git
synced 2025-12-10 15:23:19 +08:00
Simplify security implementation using modern 2025 patterns - integrate security directly with zero abstraction
Co-authored-by: animator <615622+animator@users.noreply.github.com>
This commit is contained in:
@@ -44,34 +44,22 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
|
|||||||
late final JavascriptRuntime _runtime;
|
late final JavascriptRuntime _runtime;
|
||||||
String? _currentRequestId;
|
String? _currentRequestId;
|
||||||
|
|
||||||
// Security: Maximum script length to prevent DoS attacks
|
// Modern 2025 security: Simple pattern-based validation
|
||||||
static const int _maxScriptLength = 50000; // 50KB
|
static const _maxScriptSize = 50000; // 50KB limit
|
||||||
|
static final _dangerousPatterns = RegExp(
|
||||||
|
r'eval\s*\(|Function\s*\(|constructor\s*\[|__proto__',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
// Security: Dangerous JavaScript patterns that could lead to code injection
|
/// Validate script before execution (zero-trust approach)
|
||||||
static const List<String> _dangerousPatterns = [
|
|
||||||
r'eval\s*\(',
|
|
||||||
r'Function\s*\(',
|
|
||||||
r'constructor\s*\[',
|
|
||||||
r'__proto__',
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Validates user script for basic security checks
|
|
||||||
/// Returns null if valid, error message if invalid
|
|
||||||
String? _validateScript(String script) {
|
String? _validateScript(String script) {
|
||||||
// Check script length to prevent DoS
|
if (script.length > _maxScriptSize) {
|
||||||
if (script.length > _maxScriptLength) {
|
return 'Script too large (max 50KB)';
|
||||||
return 'Script exceeds maximum length of $_maxScriptLength characters';
|
|
||||||
}
|
}
|
||||||
|
if (_dangerousPatterns.hasMatch(script)) {
|
||||||
// Check for dangerous patterns
|
return 'Script contains unsafe patterns';
|
||||||
for (final pattern in _dangerousPatterns) {
|
|
||||||
final regex = RegExp(pattern, caseSensitive: false);
|
|
||||||
if (regex.hasMatch(script)) {
|
|
||||||
return 'Script contains potentially dangerous pattern: ${pattern.replaceAll(r'\s*\(', '(').replaceAll(r'\s*\[', '[')}';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return null; // Valid
|
||||||
return null; // Script is valid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initialize() {
|
void _initialize() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||||
import 'secure_credential_storage.dart';
|
import 'secure_storage.dart';
|
||||||
|
|
||||||
enum HiveBoxType { normal, lazy }
|
enum HiveBoxType { normal, lazy }
|
||||||
|
|
||||||
@@ -153,9 +153,9 @@ class HiveHandler {
|
|||||||
|
|
||||||
// Store secret in secure storage
|
// Store secret in secure storage
|
||||||
try {
|
try {
|
||||||
await SecureCredentialStorage.storeEnvironmentSecret(
|
await SecureStorage.storeSecret(
|
||||||
environmentId: id,
|
environmentId: id,
|
||||||
variableKey: variable['key'] ?? 'unknown_$i',
|
key: variable['key'] ?? 'unknown_$i',
|
||||||
value: variable['value'].toString(),
|
value: variable['value'].toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -197,9 +197,9 @@ class HiveHandler {
|
|||||||
|
|
||||||
// Retrieve secret from secure storage
|
// Retrieve secret from secure storage
|
||||||
try {
|
try {
|
||||||
final decryptedValue = await SecureCredentialStorage.retrieveEnvironmentSecret(
|
final decryptedValue = await SecureStorage.retrieveSecret(
|
||||||
environmentId: id,
|
environmentId: id,
|
||||||
variableKey: variable['key'] ?? 'unknown_$i',
|
key: variable['key'] ?? 'unknown_$i',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (decryptedValue != null) {
|
if (decryptedValue != null) {
|
||||||
@@ -224,11 +224,9 @@ class HiveHandler {
|
|||||||
Future<void> deleteEnvironment(String id) async {
|
Future<void> deleteEnvironment(String id) async {
|
||||||
// Clean up secure storage for this environment
|
// Clean up secure storage for this environment
|
||||||
try {
|
try {
|
||||||
await SecureCredentialStorage.clearEnvironmentSecrets(
|
await SecureStorage.deleteEnvironmentSecrets(id);
|
||||||
environmentId: id,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Log error but continue with deletion
|
// Graceful failure
|
||||||
}
|
}
|
||||||
return environmentBox.delete(id);
|
return environmentBox.delete(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
|
|
||||||
/// Service for securely storing and retrieving OAuth2 credentials
|
|
||||||
/// Uses flutter_secure_storage for encryption keys and encrypted values
|
|
||||||
class SecureCredentialStorage {
|
|
||||||
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
|
|
||||||
aOptions: AndroidOptions(
|
|
||||||
encryptedSharedPreferences: true,
|
|
||||||
),
|
|
||||||
iOptions: IOSOptions(
|
|
||||||
accessibility: KeychainAccessibility.first_unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Generates a storage key from client credentials for OAuth2
|
|
||||||
static String _generateStorageKey(String clientId, String tokenUrl) {
|
|
||||||
final combined = '$clientId:$tokenUrl';
|
|
||||||
final bytes = utf8.encode(combined);
|
|
||||||
final hash = sha256.convert(bytes);
|
|
||||||
return 'oauth2_${hash.toString().substring(0, 16)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store OAuth2 credentials securely
|
|
||||||
static Future<void> storeOAuth2Credentials({
|
|
||||||
required String clientId,
|
|
||||||
required String tokenUrl,
|
|
||||||
required String credentialsJson,
|
|
||||||
}) async {
|
|
||||||
final key = _generateStorageKey(clientId, tokenUrl);
|
|
||||||
await _secureStorage.write(key: key, value: credentialsJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve OAuth2 credentials securely
|
|
||||||
static Future<String?> retrieveOAuth2Credentials({
|
|
||||||
required String clientId,
|
|
||||||
required String tokenUrl,
|
|
||||||
}) async {
|
|
||||||
final key = _generateStorageKey(clientId, tokenUrl);
|
|
||||||
return await _secureStorage.read(key: key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete OAuth2 credentials
|
|
||||||
static Future<void> deleteOAuth2Credentials({
|
|
||||||
required String clientId,
|
|
||||||
required String tokenUrl,
|
|
||||||
}) async {
|
|
||||||
final key = _generateStorageKey(clientId, tokenUrl);
|
|
||||||
await _secureStorage.delete(key: key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all OAuth2 credentials
|
|
||||||
static Future<void> clearAllOAuth2Credentials() async {
|
|
||||||
final allKeys = await _secureStorage.readAll();
|
|
||||||
for (final key in allKeys.keys) {
|
|
||||||
if (key.startsWith('oauth2_')) {
|
|
||||||
await _secureStorage.delete(key: key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store environment variable securely (for secrets)
|
|
||||||
static Future<void> storeEnvironmentSecret({
|
|
||||||
required String environmentId,
|
|
||||||
required String variableKey,
|
|
||||||
required String value,
|
|
||||||
}) async {
|
|
||||||
final key = 'env_${environmentId}_$variableKey';
|
|
||||||
await _secureStorage.write(key: key, value: value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve environment variable secret
|
|
||||||
static Future<String?> retrieveEnvironmentSecret({
|
|
||||||
required String environmentId,
|
|
||||||
required String variableKey,
|
|
||||||
}) async {
|
|
||||||
final key = 'env_${environmentId}_$variableKey';
|
|
||||||
return await _secureStorage.read(key: key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete environment variable secret
|
|
||||||
static Future<void> deleteEnvironmentSecret({
|
|
||||||
required String environmentId,
|
|
||||||
required String variableKey,
|
|
||||||
}) async {
|
|
||||||
final key = 'env_${environmentId}_$variableKey';
|
|
||||||
await _secureStorage.delete(key: key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all environment secrets for a specific environment
|
|
||||||
static Future<void> clearEnvironmentSecrets({
|
|
||||||
required String environmentId,
|
|
||||||
}) async {
|
|
||||||
final allKeys = await _secureStorage.readAll();
|
|
||||||
final prefix = 'env_${environmentId}_';
|
|
||||||
for (final key in allKeys.keys) {
|
|
||||||
if (key.startsWith(prefix)) {
|
|
||||||
await _secureStorage.delete(key: key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if secure storage is available
|
|
||||||
static Future<bool> isSecureStorageAvailable() async {
|
|
||||||
try {
|
|
||||||
await _secureStorage.read(key: '__test__');
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
148
lib/services/secure_storage.dart
Normal file
148
lib/services/secure_storage.dart
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
/// Modern unified secure storage for API Dash (2025 best practices)
|
||||||
|
/// Handles OAuth2 credentials, environment secrets, and rate limiting
|
||||||
|
class SecureStorage {
|
||||||
|
// Platform-specific secure storage
|
||||||
|
static const _storage = FlutterSecureStorage(
|
||||||
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
|
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||||
|
);
|
||||||
|
|
||||||
|
// OAuth2 Rate limiting state
|
||||||
|
static final _rateLimits = <String, _RateLimit>{};
|
||||||
|
static const _maxAttempts = 5;
|
||||||
|
static const _resetMinutes = 30;
|
||||||
|
|
||||||
|
/// Generate secure key using SHA-256
|
||||||
|
static String _hashKey(String input) {
|
||||||
|
return sha256.convert(utf8.encode(input)).toString().substring(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== OAuth2 Methods ====================
|
||||||
|
|
||||||
|
/// Check rate limit for OAuth2
|
||||||
|
static String? checkRateLimit(String clientId, String tokenUrl) {
|
||||||
|
final key = _hashKey('$clientId:$tokenUrl');
|
||||||
|
final limit = _rateLimits[key];
|
||||||
|
|
||||||
|
if (limit == null) return null;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (now.difference(limit.firstAttempt).inMinutes >= _resetMinutes) {
|
||||||
|
_rateLimits.remove(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit.cooldownUntil != null && now.isBefore(limit.cooldownUntil!)) {
|
||||||
|
final seconds = limit.cooldownUntil!.difference(now).inSeconds;
|
||||||
|
return 'Rate limit exceeded. Try again in $seconds seconds.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record failed OAuth2 attempt
|
||||||
|
static void recordFailure(String clientId, String tokenUrl) {
|
||||||
|
final key = _hashKey('$clientId:$tokenUrl');
|
||||||
|
final now = DateTime.now();
|
||||||
|
final limit = _rateLimits[key];
|
||||||
|
|
||||||
|
if (limit == null) {
|
||||||
|
_rateLimits[key] = _RateLimit(now, now, 1, null);
|
||||||
|
} else {
|
||||||
|
final attempts = limit.attempts + 1;
|
||||||
|
final delay = attempts >= _maxAttempts
|
||||||
|
? (2 << (attempts - _maxAttempts)).clamp(2, 300)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
_rateLimits[key] = _RateLimit(
|
||||||
|
limit.firstAttempt,
|
||||||
|
now,
|
||||||
|
attempts,
|
||||||
|
delay > 0 ? now.add(Duration(seconds: delay)) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record successful OAuth2 attempt
|
||||||
|
static void recordSuccess(String clientId, String tokenUrl) {
|
||||||
|
_rateLimits.remove(_hashKey('$clientId:$tokenUrl'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store OAuth2 credentials
|
||||||
|
static Future<void> storeOAuth2({
|
||||||
|
required String clientId,
|
||||||
|
required String tokenUrl,
|
||||||
|
required String credentialsJson,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _storage.write(
|
||||||
|
key: 'oauth2_${_hashKey('$clientId:$tokenUrl')}',
|
||||||
|
value: credentialsJson,
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve OAuth2 credentials
|
||||||
|
static Future<String?> retrieveOAuth2({
|
||||||
|
required String clientId,
|
||||||
|
required String tokenUrl,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await _storage.read(
|
||||||
|
key: 'oauth2_${_hashKey('$clientId:$tokenUrl')}',
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Environment Secret Methods ====================
|
||||||
|
|
||||||
|
/// Store environment secret
|
||||||
|
static Future<void> storeSecret({
|
||||||
|
required String environmentId,
|
||||||
|
required String key,
|
||||||
|
required String value,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _storage.write(key: 'env_${environmentId}_$key', value: value);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve environment secret
|
||||||
|
static Future<String?> retrieveSecret({
|
||||||
|
required String environmentId,
|
||||||
|
required String key,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await _storage.read(key: 'env_${environmentId}_$key');
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all secrets for an environment
|
||||||
|
static Future<void> deleteEnvironmentSecrets(String environmentId) async {
|
||||||
|
try {
|
||||||
|
final all = await _storage.readAll();
|
||||||
|
final prefix = 'env_${environmentId}_';
|
||||||
|
for (final key in all.keys.where((k) => k.startsWith(prefix))) {
|
||||||
|
await _storage.delete(key: key);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal rate limit state
|
||||||
|
class _RateLimit {
|
||||||
|
final DateTime firstAttempt;
|
||||||
|
final DateTime lastAttempt;
|
||||||
|
final int attempts;
|
||||||
|
final DateTime? cooldownUntil;
|
||||||
|
|
||||||
|
_RateLimit(this.firstAttempt, this.lastAttempt, this.attempts, this.cooldownUntil);
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
/// Security utilities for code generation
|
|
||||||
/// Provides sanitization and validation for generated code to prevent injection attacks
|
|
||||||
class SecureCodeGenUtils {
|
|
||||||
/// Maximum length for any user input field
|
|
||||||
static const int _maxFieldLength = 10000;
|
|
||||||
|
|
||||||
/// Validates if a field name is safe (alphanumeric and underscore only)
|
|
||||||
static bool isValidFieldName(String name) {
|
|
||||||
if (name.isEmpty || name.length > 255) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$').hasMatch(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Comprehensive JavaScript string escaping
|
|
||||||
/// Prevents XSS and code injection in generated JavaScript code
|
|
||||||
static String escapeJavaScript(String input) {
|
|
||||||
if (input.length > _maxFieldLength) {
|
|
||||||
throw SecurityException('Input exceeds maximum length');
|
|
||||||
}
|
|
||||||
|
|
||||||
return input
|
|
||||||
.replaceAll('\\', '\\\\') // Backslash
|
|
||||||
.replaceAll('"', '\\"') // Double quote
|
|
||||||
.replaceAll("'", "\\'") // Single quote
|
|
||||||
.replaceAll('\n', '\\n') // Newline
|
|
||||||
.replaceAll('\r', '\\r') // Carriage return
|
|
||||||
.replaceAll('\t', '\\t') // Tab
|
|
||||||
.replaceAll('\b', '\\b') // Backspace
|
|
||||||
.replaceAll('\f', '\\f') // Form feed
|
|
||||||
.replaceAll('<', '\\x3C') // Less than (XSS protection)
|
|
||||||
.replaceAll('>', '\\x3E') // Greater than
|
|
||||||
.replaceAll('&', '\\x26') // Ampersand
|
|
||||||
.replaceAll('/', '\\/') // Forward slash
|
|
||||||
.replaceAll('\u2028', '\\u2028') // Line separator
|
|
||||||
.replaceAll('\u2029', '\\u2029'); // Paragraph separator
|
|
||||||
}
|
|
||||||
|
|
||||||
/// HTML escaping for generated code comments
|
|
||||||
static String escapeHtml(String input) {
|
|
||||||
if (input.length > _maxFieldLength) {
|
|
||||||
throw SecurityException('Input exceeds maximum length');
|
|
||||||
}
|
|
||||||
|
|
||||||
return input
|
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
.replaceAll("'", ''')
|
|
||||||
.replaceAll('/', '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Python string escaping
|
|
||||||
static String escapePython(String input) {
|
|
||||||
if (input.length > _maxFieldLength) {
|
|
||||||
throw SecurityException('Input exceeds maximum length');
|
|
||||||
}
|
|
||||||
|
|
||||||
return input
|
|
||||||
.replaceAll('\\', '\\\\')
|
|
||||||
.replaceAll('"', '\\"')
|
|
||||||
.replaceAll("'", "\\'")
|
|
||||||
.replaceAll('\n', '\\n')
|
|
||||||
.replaceAll('\r', '\\r')
|
|
||||||
.replaceAll('\t', '\\t');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate and sanitize URL
|
|
||||||
/// Returns null if URL is invalid
|
|
||||||
static String? sanitizeUrl(String url) {
|
|
||||||
if (url.length > _maxFieldLength) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
|
|
||||||
// Only allow http and https schemes
|
|
||||||
if (uri.scheme != 'http' && uri.scheme != 'https') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate host
|
|
||||||
if (uri.host.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return uri.toString();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate that input doesn't contain dangerous patterns
|
|
||||||
static bool containsDangerousPattern(String input) {
|
|
||||||
// Check for common injection patterns
|
|
||||||
final dangerousPatterns = [
|
|
||||||
RegExp(r'<script', caseSensitive: false),
|
|
||||||
RegExp(r'javascript:', caseSensitive: false),
|
|
||||||
RegExp(r'onerror\s*=', caseSensitive: false),
|
|
||||||
RegExp(r'onload\s*=', caseSensitive: false),
|
|
||||||
RegExp(r'eval\s*\(', caseSensitive: false),
|
|
||||||
RegExp(r'exec\s*\(', caseSensitive: false),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (final pattern in dangerousPatterns) {
|
|
||||||
if (pattern.hasMatch(input)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanitize multiline string for code generation
|
|
||||||
static String sanitizeMultiline(String input) {
|
|
||||||
if (input.length > _maxFieldLength) {
|
|
||||||
throw SecurityException('Input exceeds maximum length');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any null bytes
|
|
||||||
return input.replaceAll('\x00', '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exception thrown when a security validation fails
|
|
||||||
class SecurityException implements Exception {
|
|
||||||
final String message;
|
|
||||||
SecurityException(this.message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SecurityException: $message';
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Security utility for environment variable substitution
|
|
||||||
/// Protects against ReDoS (Regular Expression Denial of Service) attacks
|
|
||||||
class SecureEnvVarUtils {
|
|
||||||
// Maximum input length to prevent DoS attacks
|
|
||||||
static const int _maxInputLength = 10000;
|
|
||||||
|
|
||||||
// Maximum number of variables before switching to alternative algorithm
|
|
||||||
static const int _maxRegexComplexity = 1000;
|
|
||||||
|
|
||||||
/// Validates if a variable name is safe (alphanumeric, underscore, dash only)
|
|
||||||
static bool isValidVariableName(String name) {
|
|
||||||
if (name.isEmpty || name.length > 100) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return RegExp(r'^[a-zA-Z0-9_-]+$').hasMatch(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Escapes special regex characters in a string
|
|
||||||
static String escapeRegex(String input) {
|
|
||||||
return input.replaceAllMapped(
|
|
||||||
RegExp(r'[.*+?^${}()|[\]\\]'),
|
|
||||||
(match) => '\\${match.group(0)}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Safely substitute environment variables without ReDoS vulnerability
|
|
||||||
///
|
|
||||||
/// Validates input length and complexity before processing
|
|
||||||
/// Uses alternative string matching for large variable sets
|
|
||||||
static String? substituteVariablesSafe(
|
|
||||||
String? input,
|
|
||||||
Map<String, String> envVarMap,
|
|
||||||
) {
|
|
||||||
if (input == null) return null;
|
|
||||||
if (envVarMap.keys.isEmpty) return input;
|
|
||||||
|
|
||||||
// Check input length to prevent DoS
|
|
||||||
if (input.length > _maxInputLength) {
|
|
||||||
throw SecurityException(
|
|
||||||
'Input exceeds maximum length of $_maxInputLength characters'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all variable names before processing
|
|
||||||
final invalidNames = envVarMap.keys.where((key) => !isValidVariableName(key));
|
|
||||||
if (invalidNames.isNotEmpty) {
|
|
||||||
throw SecurityException(
|
|
||||||
'Invalid variable names found: ${invalidNames.join(', ')}'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For large variable sets, use direct string replacement to avoid ReDoS
|
|
||||||
if (envVarMap.keys.length > _maxRegexComplexity) {
|
|
||||||
return _substituteWithoutRegex(input, envVarMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For reasonable sets, use regex with escaped keys
|
|
||||||
try {
|
|
||||||
final escapedKeys = envVarMap.keys.map(escapeRegex).join('|');
|
|
||||||
final regex = RegExp(r'\{\{(' + escapedKeys + r')\}\}');
|
|
||||||
|
|
||||||
return input.replaceAllMapped(regex, (match) {
|
|
||||||
final key = match.group(1)?.trim() ?? '';
|
|
||||||
return envVarMap[key] ?? '{{$key}}';
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback to safe method on any error
|
|
||||||
return _substituteWithoutRegex(input, envVarMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alternative substitution method that doesn't use regex
|
|
||||||
/// Safe for large variable sets
|
|
||||||
static String _substituteWithoutRegex(
|
|
||||||
String input,
|
|
||||||
Map<String, String> envVarMap,
|
|
||||||
) {
|
|
||||||
var result = input;
|
|
||||||
|
|
||||||
// Sort by length descending to handle overlapping keys correctly
|
|
||||||
final sortedEntries = envVarMap.entries.toList()
|
|
||||||
..sort((a, b) => b.key.length.compareTo(a.key.length));
|
|
||||||
|
|
||||||
for (var entry in sortedEntries) {
|
|
||||||
final pattern = '{{${entry.key}}}';
|
|
||||||
result = result.replaceAll(pattern, entry.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exception thrown when a security validation fails
|
|
||||||
class SecurityException implements Exception {
|
|
||||||
final String message;
|
|
||||||
SecurityException(this.message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SecurityException: $message';
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
/// Rate limiter for OAuth2 authentication attempts
|
|
||||||
/// Implements exponential backoff to prevent abuse and brute force attacks
|
|
||||||
class OAuth2RateLimiter {
|
|
||||||
static final Map<String, _RateLimitState> _states = {};
|
|
||||||
|
|
||||||
// Maximum attempts before lockout
|
|
||||||
static const int _maxAttempts = 5;
|
|
||||||
|
|
||||||
// Initial delay after first failure (in seconds)
|
|
||||||
static const int _initialDelay = 2;
|
|
||||||
|
|
||||||
// Maximum delay (in seconds)
|
|
||||||
static const int _maxDelay = 300; // 5 minutes
|
|
||||||
|
|
||||||
// Time window for reset (in minutes)
|
|
||||||
static const int _resetWindow = 30;
|
|
||||||
|
|
||||||
/// Check if an OAuth operation can proceed
|
|
||||||
/// Returns null if allowed, or DateTime when the operation can be retried
|
|
||||||
static DateTime? canProceed(String key) {
|
|
||||||
final state = _states[key];
|
|
||||||
|
|
||||||
if (state == null) {
|
|
||||||
// First attempt, no restrictions
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
// Check if we should reset the counter (time window passed)
|
|
||||||
if (now.difference(state.firstAttempt).inMinutes >= _resetWindow) {
|
|
||||||
_states.remove(key);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're in cooldown period
|
|
||||||
if (state.nextAttemptAt != null && now.isBefore(state.nextAttemptAt!)) {
|
|
||||||
return state.nextAttemptAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if max attempts exceeded
|
|
||||||
if (state.attemptCount >= _maxAttempts) {
|
|
||||||
// Calculate next allowed attempt with exponential backoff
|
|
||||||
final delaySeconds = _calculateDelay(state.attemptCount);
|
|
||||||
final nextAttempt = state.lastAttempt.add(Duration(seconds: delaySeconds));
|
|
||||||
|
|
||||||
if (now.isBefore(nextAttempt)) {
|
|
||||||
return nextAttempt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record a failed authentication attempt
|
|
||||||
static void recordFailure(String key) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final state = _states[key];
|
|
||||||
|
|
||||||
if (state == null) {
|
|
||||||
_states[key] = _RateLimitState(
|
|
||||||
firstAttempt: now,
|
|
||||||
lastAttempt: now,
|
|
||||||
attemptCount: 1,
|
|
||||||
nextAttemptAt: null,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final delaySeconds = _calculateDelay(state.attemptCount + 1);
|
|
||||||
|
|
||||||
_states[key] = _RateLimitState(
|
|
||||||
firstAttempt: state.firstAttempt,
|
|
||||||
lastAttempt: now,
|
|
||||||
attemptCount: state.attemptCount + 1,
|
|
||||||
nextAttemptAt: now.add(Duration(seconds: delaySeconds)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record a successful authentication (clears the rate limit)
|
|
||||||
static void recordSuccess(String key) {
|
|
||||||
_states.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate delay with exponential backoff
|
|
||||||
static int _calculateDelay(int attemptCount) {
|
|
||||||
if (attemptCount <= 1) return 0;
|
|
||||||
|
|
||||||
// Exponential backoff: 2^(n-1) seconds, capped at _maxDelay
|
|
||||||
final delay = _initialDelay * (1 << (attemptCount - 2));
|
|
||||||
return delay > _maxDelay ? _maxDelay : delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate rate limit key from client credentials
|
|
||||||
static String generateKey(String clientId, String tokenUrl) {
|
|
||||||
return '$clientId:$tokenUrl';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get remaining cooldown time in seconds
|
|
||||||
static int? getCooldownSeconds(String key) {
|
|
||||||
final canProceedAt = canProceed(key);
|
|
||||||
if (canProceedAt == null) return null;
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
final diff = canProceedAt.difference(now);
|
|
||||||
return diff.inSeconds > 0 ? diff.inSeconds : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all rate limiting states (for testing or admin purposes)
|
|
||||||
static void clearAll() {
|
|
||||||
_states.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RateLimitState {
|
|
||||||
final DateTime firstAttempt;
|
|
||||||
final DateTime lastAttempt;
|
|
||||||
final int attemptCount;
|
|
||||||
final DateTime? nextAttemptAt;
|
|
||||||
|
|
||||||
_RateLimitState({
|
|
||||||
required this.firstAttempt,
|
|
||||||
required this.lastAttempt,
|
|
||||||
required this.attemptCount,
|
|
||||||
this.nextAttemptAt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -2,79 +2,111 @@ import 'dart:convert';
|
|||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
/// Secure storage service for OAuth2 credentials
|
/// Modern OAuth2 secure storage with built-in rate limiting (2025 best practices)
|
||||||
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android)
|
/// Note: This is a package-local wrapper. The main app uses lib/services/secure_storage.dart
|
||||||
class OAuth2SecureStorage {
|
class OAuth2SecureStorage {
|
||||||
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
|
// Secure storage with platform-specific encryption
|
||||||
aOptions: AndroidOptions(
|
static const _storage = FlutterSecureStorage(
|
||||||
encryptedSharedPreferences: true,
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
),
|
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||||
iOptions: IOSOptions(
|
|
||||||
accessibility: KeychainAccessibility.first_unlock,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Generate a unique storage key from client ID and token URL
|
// Rate limiting state
|
||||||
static String _generateKey(String clientId, String tokenUrl) {
|
static final _rateLimits = <String, _RateLimit>{};
|
||||||
final combined = '$clientId:$tokenUrl';
|
static const _maxAttempts = 5;
|
||||||
final bytes = utf8.encode(combined);
|
static const _resetMinutes = 30;
|
||||||
final hash = sha256.convert(bytes);
|
|
||||||
return 'oauth2_cred_${hash.toString().substring(0, 16)}';
|
/// Generate secure storage key using SHA-256
|
||||||
|
static String _key(String clientId, String tokenUrl) {
|
||||||
|
final hash = sha256.convert(utf8.encode('$clientId:$tokenUrl'));
|
||||||
|
return 'oauth2_${hash.toString().substring(0, 16)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store OAuth2 credentials securely
|
/// Check if authentication can proceed (rate limiting)
|
||||||
static Future<void> storeCredentials({
|
static String? checkRateLimit(String clientId, String tokenUrl) {
|
||||||
|
final key = _key(clientId, tokenUrl);
|
||||||
|
final limit = _rateLimits[key];
|
||||||
|
|
||||||
|
if (limit == null) return null;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// Auto-reset after 30 minutes
|
||||||
|
if (now.difference(limit.firstAttempt).inMinutes >= _resetMinutes) {
|
||||||
|
_rateLimits.remove(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cooldown
|
||||||
|
if (limit.cooldownUntil != null && now.isBefore(limit.cooldownUntil!)) {
|
||||||
|
final seconds = limit.cooldownUntil!.difference(now).inSeconds;
|
||||||
|
return 'Rate limit exceeded. Try again in $seconds seconds.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record failed authentication attempt
|
||||||
|
static void recordFailure(String clientId, String tokenUrl) {
|
||||||
|
final key = _key(clientId, tokenUrl);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final limit = _rateLimits[key];
|
||||||
|
|
||||||
|
if (limit == null) {
|
||||||
|
_rateLimits[key] = _RateLimit(now, now, 1, null);
|
||||||
|
} else {
|
||||||
|
final attempts = limit.attempts + 1;
|
||||||
|
// Exponential backoff: 2, 4, 8, 16... max 300s (5 minutes)
|
||||||
|
final delay = attempts >= _maxAttempts
|
||||||
|
? (2 << (attempts - _maxAttempts)).clamp(2, 300)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
_rateLimits[key] = _RateLimit(
|
||||||
|
limit.firstAttempt,
|
||||||
|
now,
|
||||||
|
attempts,
|
||||||
|
delay > 0 ? now.add(Duration(seconds: delay)) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record successful authentication (clears rate limit)
|
||||||
|
static void recordSuccess(String clientId, String tokenUrl) {
|
||||||
|
_rateLimits.remove(_key(clientId, tokenUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store credentials securely
|
||||||
|
static Future<void> store({
|
||||||
required String clientId,
|
required String clientId,
|
||||||
required String tokenUrl,
|
required String tokenUrl,
|
||||||
required String credentialsJson,
|
required String credentialsJson,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final key = _generateKey(clientId, tokenUrl);
|
await _storage.write(key: _key(clientId, tokenUrl), value: credentialsJson);
|
||||||
await _secureStorage.write(key: key, value: credentialsJson);
|
} catch (_) {
|
||||||
} catch (e) {
|
// Graceful degradation
|
||||||
// Log error but don't fail - fallback to no storage
|
|
||||||
// In production, consider proper logging
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve OAuth2 credentials
|
/// Retrieve credentials
|
||||||
static Future<String?> retrieveCredentials({
|
static Future<String?> retrieve({
|
||||||
required String clientId,
|
required String clientId,
|
||||||
required String tokenUrl,
|
required String tokenUrl,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final key = _generateKey(clientId, tokenUrl);
|
return await _storage.read(key: _key(clientId, tokenUrl));
|
||||||
return await _secureStorage.read(key: key);
|
} catch (_) {
|
||||||
} catch (e) {
|
|
||||||
// Log error but return null - will trigger fresh auth
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Delete OAuth2 credentials
|
|
||||||
static Future<void> deleteCredentials({
|
// Internal rate limit state
|
||||||
required String clientId,
|
class _RateLimit {
|
||||||
required String tokenUrl,
|
final DateTime firstAttempt;
|
||||||
}) async {
|
final DateTime lastAttempt;
|
||||||
try {
|
final int attempts;
|
||||||
final key = _generateKey(clientId, tokenUrl);
|
final DateTime? cooldownUntil;
|
||||||
await _secureStorage.delete(key: key);
|
|
||||||
} catch (e) {
|
_RateLimit(this.firstAttempt, this.lastAttempt, this.attempts, this.cooldownUntil);
|
||||||
// Log error but don't fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all OAuth2 credentials
|
|
||||||
static Future<void> clearAllCredentials() async {
|
|
||||||
try {
|
|
||||||
final allKeys = await _secureStorage.readAll();
|
|
||||||
for (final key in allKeys.keys) {
|
|
||||||
if (key.startsWith('oauth2_cred_')) {
|
|
||||||
await _secureStorage.delete(key: key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Log error but don't fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import '../../models/auth/auth_oauth2_model.dart';
|
|||||||
import '../../services/http_client_manager.dart';
|
import '../../services/http_client_manager.dart';
|
||||||
import '../../services/oauth_callback_server.dart';
|
import '../../services/oauth_callback_server.dart';
|
||||||
import '../../services/oauth2_secure_storage.dart';
|
import '../../services/oauth2_secure_storage.dart';
|
||||||
import '../../services/oauth2_rate_limiter.dart';
|
|
||||||
import '../platform_utils.dart';
|
import '../platform_utils.dart';
|
||||||
|
|
||||||
/// Advanced OAuth2 authorization code grant handler that returns both the client and server
|
/// Advanced OAuth2 authorization code grant handler that returns both the client and server
|
||||||
@@ -25,21 +24,15 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
|||||||
String? state,
|
String? state,
|
||||||
String? scope,
|
String? scope,
|
||||||
}) async {
|
}) async {
|
||||||
// Check rate limiting
|
// Check rate limiting (integrated with OAuth2SecureStorage)
|
||||||
final rateLimitKey = OAuth2RateLimiter.generateKey(identifier, tokenEndpoint.toString());
|
final rateLimitError = OAuth2SecureStorage.checkRateLimit(identifier, tokenEndpoint.toString());
|
||||||
final canProceedAt = OAuth2RateLimiter.canProceed(rateLimitKey);
|
if (rateLimitError != null) {
|
||||||
|
throw Exception(rateLimitError);
|
||||||
if (canProceedAt != null) {
|
|
||||||
final cooldownSeconds = OAuth2RateLimiter.getCooldownSeconds(rateLimitKey);
|
|
||||||
throw Exception(
|
|
||||||
'OAuth2 rate limit exceeded. Please try again in ${cooldownSeconds ?? 0} seconds.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existing credentials first - try secure storage, then file
|
// Try secure storage first (modern approach)
|
||||||
// Try secure storage first (preferred method)
|
|
||||||
try {
|
try {
|
||||||
final secureCredJson = await OAuth2SecureStorage.retrieveCredentials(
|
final secureCredJson = await OAuth2SecureStorage.retrieve(
|
||||||
clientId: identifier,
|
clientId: identifier,
|
||||||
tokenUrl: tokenEndpoint.toString(),
|
tokenUrl: tokenEndpoint.toString(),
|
||||||
);
|
);
|
||||||
@@ -47,8 +40,7 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
|||||||
if (secureCredJson != null) {
|
if (secureCredJson != null) {
|
||||||
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
||||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||||
// Successful retrieval, clear rate limit
|
OAuth2SecureStorage.recordSuccess(identifier, tokenEndpoint.toString());
|
||||||
OAuth2RateLimiter.recordSuccess(rateLimitKey);
|
|
||||||
return (
|
return (
|
||||||
oauth2.Client(credentials, identifier: identifier, secret: secret),
|
oauth2.Client(credentials, identifier: identifier, secret: secret),
|
||||||
null,
|
null,
|
||||||
@@ -56,7 +48,7 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Secure storage failed, try file fallback
|
// Graceful fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to file-based storage for backward compatibility
|
// Fallback to file-based storage for backward compatibility
|
||||||
@@ -68,7 +60,7 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
|||||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||||
// Migrate to secure storage for future use
|
// Migrate to secure storage for future use
|
||||||
try {
|
try {
|
||||||
await OAuth2SecureStorage.storeCredentials(
|
await OAuth2SecureStorage.store(
|
||||||
clientId: identifier,
|
clientId: identifier,
|
||||||
tokenUrl: tokenEndpoint.toString(),
|
tokenUrl: tokenEndpoint.toString(),
|
||||||
credentialsJson: json,
|
credentialsJson: json,
|
||||||
@@ -175,7 +167,7 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
|||||||
|
|
||||||
// Store credentials securely (preferred method)
|
// Store credentials securely (preferred method)
|
||||||
try {
|
try {
|
||||||
await OAuth2SecureStorage.storeCredentials(
|
await OAuth2SecureStorage.store(
|
||||||
clientId: identifier,
|
clientId: identifier,
|
||||||
tokenUrl: tokenEndpoint.toString(),
|
tokenUrl: tokenEndpoint.toString(),
|
||||||
credentialsJson: client.credentials.toJson(),
|
credentialsJson: client.credentials.toJson(),
|
||||||
@@ -192,12 +184,12 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record successful authentication
|
// Record successful authentication
|
||||||
OAuth2RateLimiter.recordSuccess(rateLimitKey);
|
OAuth2SecureStorage.recordSuccess(identifier, tokenEndpoint.toString());
|
||||||
|
|
||||||
return (client, callbackServer);
|
return (client, callbackServer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Record failed authentication attempt
|
// Record failed authentication attempt
|
||||||
OAuth2RateLimiter.recordFailure(rateLimitKey);
|
OAuth2SecureStorage.recordFailure(identifier, tokenEndpoint.toString());
|
||||||
|
|
||||||
// Clean up the callback server immediately on error
|
// Clean up the callback server immediately on error
|
||||||
if (callbackServer != null) {
|
if (callbackServer != null) {
|
||||||
@@ -220,22 +212,18 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
|||||||
required File? credentialsFile,
|
required File? credentialsFile,
|
||||||
}) async {
|
}) async {
|
||||||
// Check rate limiting
|
// Check rate limiting
|
||||||
final rateLimitKey = OAuth2RateLimiter.generateKey(
|
final rateLimitError = OAuth2SecureStorage.checkRateLimit(
|
||||||
oauth2Model.clientId,
|
oauth2Model.clientId,
|
||||||
oauth2Model.accessTokenUrl,
|
oauth2Model.accessTokenUrl,
|
||||||
);
|
);
|
||||||
final canProceedAt = OAuth2RateLimiter.canProceed(rateLimitKey);
|
|
||||||
|
|
||||||
if (canProceedAt != null) {
|
if (rateLimitError != null) {
|
||||||
final cooldownSeconds = OAuth2RateLimiter.getCooldownSeconds(rateLimitKey);
|
throw Exception(rateLimitError);
|
||||||
throw Exception(
|
|
||||||
'OAuth2 rate limit exceeded. Please try again in ${cooldownSeconds ?? 0} seconds.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try secure storage first
|
// Try secure storage first
|
||||||
try {
|
try {
|
||||||
final secureCredJson = await OAuth2SecureStorage.retrieveCredentials(
|
final secureCredJson = await OAuth2SecureStorage.retrieve(
|
||||||
clientId: oauth2Model.clientId,
|
clientId: oauth2Model.clientId,
|
||||||
tokenUrl: oauth2Model.accessTokenUrl,
|
tokenUrl: oauth2Model.accessTokenUrl,
|
||||||
);
|
);
|
||||||
@@ -243,7 +231,7 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
|||||||
if (secureCredJson != null) {
|
if (secureCredJson != null) {
|
||||||
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
||||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||||
OAuth2RateLimiter.recordSuccess(rateLimitKey);
|
OAuth2SecureStorage.recordSuccess(oauth2Model.clientId, oauth2Model.accessTokenUrl);
|
||||||
return oauth2.Client(
|
return oauth2.Client(
|
||||||
credentials,
|
credentials,
|
||||||
identifier: oauth2Model.clientId,
|
identifier: oauth2Model.clientId,
|
||||||
@@ -264,7 +252,7 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
|||||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||||
// Migrate to secure storage
|
// Migrate to secure storage
|
||||||
try {
|
try {
|
||||||
await OAuth2SecureStorage.storeCredentials(
|
await OAuth2SecureStorage.store(
|
||||||
clientId: oauth2Model.clientId,
|
clientId: oauth2Model.clientId,
|
||||||
tokenUrl: oauth2Model.accessTokenUrl,
|
tokenUrl: oauth2Model.accessTokenUrl,
|
||||||
credentialsJson: json,
|
credentialsJson: json,
|
||||||
@@ -303,7 +291,7 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
|||||||
|
|
||||||
// Store credentials securely
|
// Store credentials securely
|
||||||
try {
|
try {
|
||||||
await OAuth2SecureStorage.storeCredentials(
|
await OAuth2SecureStorage.store(
|
||||||
clientId: oauth2Model.clientId,
|
clientId: oauth2Model.clientId,
|
||||||
tokenUrl: oauth2Model.accessTokenUrl,
|
tokenUrl: oauth2Model.accessTokenUrl,
|
||||||
credentialsJson: client.credentials.toJson(),
|
credentialsJson: client.credentials.toJson(),
|
||||||
@@ -320,7 +308,7 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record successful authentication
|
// Record successful authentication
|
||||||
OAuth2RateLimiter.recordSuccess(rateLimitKey);
|
OAuth2SecureStorage.recordSuccess(oauth2Model.clientId, oauth2Model.accessTokenUrl);
|
||||||
|
|
||||||
// Clean up the HTTP client
|
// Clean up the HTTP client
|
||||||
httpClientManager.closeClient(requestId);
|
httpClientManager.closeClient(requestId);
|
||||||
@@ -328,7 +316,7 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
|||||||
return client;
|
return client;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Record failed authentication attempt
|
// Record failed authentication attempt
|
||||||
OAuth2RateLimiter.recordFailure(rateLimitKey);
|
OAuth2SecureStorage.recordFailure(oauth2Model.clientId, oauth2Model.accessTokenUrl);
|
||||||
|
|
||||||
// Clean up the HTTP client on error
|
// Clean up the HTTP client on error
|
||||||
httpClientManager.closeClient(requestId);
|
httpClientManager.closeClient(requestId);
|
||||||
@@ -342,7 +330,7 @@ Future<oauth2.Client> oAuth2ResourceOwnerPasswordGrantHandler({
|
|||||||
}) async {
|
}) async {
|
||||||
// Try secure storage first
|
// Try secure storage first
|
||||||
try {
|
try {
|
||||||
final secureCredJson = await OAuth2SecureStorage.retrieveCredentials(
|
final secureCredJson = await OAuth2SecureStorage.retrieve(
|
||||||
clientId: oauth2Model.clientId,
|
clientId: oauth2Model.clientId,
|
||||||
tokenUrl: oauth2Model.accessTokenUrl,
|
tokenUrl: oauth2Model.accessTokenUrl,
|
||||||
);
|
);
|
||||||
@@ -370,7 +358,7 @@ Future<oauth2.Client> oAuth2ResourceOwnerPasswordGrantHandler({
|
|||||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||||
// Migrate to secure storage
|
// Migrate to secure storage
|
||||||
try {
|
try {
|
||||||
await OAuth2SecureStorage.storeCredentials(
|
await OAuth2SecureStorage.store(
|
||||||
clientId: oauth2Model.clientId,
|
clientId: oauth2Model.clientId,
|
||||||
tokenUrl: oauth2Model.accessTokenUrl,
|
tokenUrl: oauth2Model.accessTokenUrl,
|
||||||
credentialsJson: json,
|
credentialsJson: json,
|
||||||
@@ -415,7 +403,7 @@ Future<oauth2.Client> oAuth2ResourceOwnerPasswordGrantHandler({
|
|||||||
|
|
||||||
// Store credentials securely
|
// Store credentials securely
|
||||||
try {
|
try {
|
||||||
await OAuth2SecureStorage.storeCredentials(
|
await OAuth2SecureStorage.store(
|
||||||
clientId: oauth2Model.clientId,
|
clientId: oauth2Model.clientId,
|
||||||
tokenUrl: oauth2Model.accessTokenUrl,
|
tokenUrl: oauth2Model.accessTokenUrl,
|
||||||
credentialsJson: client.credentials.toJson(),
|
credentialsJson: client.credentials.toJson(),
|
||||||
|
|||||||
Reference in New Issue
Block a user