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:
copilot-swe-agent[bot]
2025-12-04 19:59:29 +00:00
parent 8f5c387018
commit ba46cb9cfa
9 changed files with 278 additions and 600 deletions

View File

@@ -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() {

View File

@@ -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);
} }

View File

@@ -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;
}
}
}

View 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);
}

View File

@@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#x27;')
.replaceAll('/', '&#x2F;');
}
/// 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';
}

View File

@@ -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';
}

View File

@@ -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,
});
}

View File

@@ -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
}
}
} }

View File

@@ -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(),