Security Features with fasqbloc
fasq_bloc supports all FASQ security features through QueryClient configuration, enabling secure data handling in your Bloc-based applications.
Overview
Section titled “Overview”Security features in fasq_bloc include:
- Secure cache entries with automatic cleanup
- Encrypted persistence for sensitive data
- Input validation preventing injection attacks
- Platform-specific secure key storage
Secure Queries with QueryCubit
Section titled “Secure Queries with QueryCubit”Mark sensitive data to prevent persistence and enable automatic cleanup:
BlocProvider( create: (_) => QueryCubit<String>( key: 'auth-token', queryFn: () => api.getAuthToken(), options: QueryOptions( isSecure: true, // Mark as secure maxAge: Duration(minutes: 15), // Required TTL staleTime: Duration(minutes: 5), ), client: context.queryClient, // Use configured client ), child: BlocBuilder<QueryCubit<String>, QueryState<String>>( builder: (context, state) { // Secure data never persisted, cleared on app background return Text('Token: ${state.data}'); }, ),)Security Benefits
Section titled “Security Benefits”- Never persisted to disk - Secure entries are memory-only
- Automatic cleanup - Cleared on app background/termination
- Strict TTL enforcement - Expired secure entries are immediately removed
- Not exposed in DevTools - Secure data is hidden from debugging tools
Secure Mutations with MutationCubit
Section titled “Secure Mutations with MutationCubit”Handle sensitive mutations with security features:
BlocProvider( create: (_) => MutationCubit<String, String>( mutationFn: (data) => api.secureMutation(data), options: MutationOptions( queueWhenOffline: true, maxRetries: 3, ), client: context.queryClient, // Use configured client ), child: BlocBuilder<MutationCubit<String, String>, MutationState<String>>( builder: (context, state) { return ElevatedButton( onPressed: state.isLoading ? null : () => context.read<MutationCubit<String, String>>().mutate('secure-data'), child: state.isLoading ? CircularProgressIndicator() : Text('Secure Mutation'), ); }, ),)Global Security Configuration
Section titled “Global Security Configuration”Configure security features globally using QueryClientProvider:
final secureClient = QueryClient( config: const CacheConfig( defaultStaleTime: Duration(minutes: 5), defaultCacheTime: Duration(minutes: 10), ), persistenceOptions: const PersistenceOptions(enabled: true),);
QueryClientProvider( client: secureClient, child: const MaterialApp( home: MyApp(), ),);Accessing Configured Client
Section titled “Accessing Configured Client”Use the configured QueryClient in your Bloc providers:
class MyScreen extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (_) => QueryCubit<String>( key: 'secure-data', queryFn: () => fetchSecureData(), options: QueryOptions( isSecure: true, maxAge: Duration(minutes: 30), ), client: context.queryClient, // Use configured client ), ), BlocProvider( create: (_) => MutationCubit<String, String>( mutationFn: (data) => secureMutation(data), client: context.queryClient, // Use configured client ), ), ], child: MyScreenContent(), ); }}Complete Security Example
Section titled “Complete Security Example”Here’s a complete example showing security features in a Bloc-based app:
class SecureApp extends StatelessWidget { @override Widget build(BuildContext context) { final secureClient = QueryClient( config: const CacheConfig( defaultStaleTime: Duration(minutes: 5), defaultCacheTime: Duration(minutes: 10), ), persistenceOptions: const PersistenceOptions(enabled: true), );
return QueryClientProvider( client: secureClient, child: const MaterialApp( home: SecureHomeScreen(), ), ); }}
class SecureHomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Secure App')), body: MultiBlocProvider( providers: [ // Secure authentication token BlocProvider( create: (_) => QueryCubit<String>( key: 'auth-token', queryFn: () => api.getAuthToken(), options: QueryOptions( isSecure: true, maxAge: Duration(minutes: 15), staleTime: Duration(minutes: 5), ), client: context.queryClient, ), ), // Secure user profile BlocProvider( create: (_) => QueryCubit<User>( key: 'user-profile', queryFn: () => api.getUserProfile(), options: QueryOptions( isSecure: true, maxAge: Duration(minutes: 30), staleTime: Duration(minutes: 10), ), client: context.queryClient, ), ), // Secure mutation for updating profile BlocProvider( create: (_) => MutationCubit<User, User>( mutationFn: (user) => api.updateUserProfile(user), options: MutationOptions( queueWhenOffline: true, maxRetries: 3, onSuccess: (user) { // Invalidate user profile query context.queryClient?.invalidateQuery('user-profile'); }, ), client: context.queryClient, ), ), ], child: SecureHomeContent(), ), ); }}
class SecureHomeContent extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // Display auth token BlocBuilder<QueryCubit<String>, QueryState<String>>( builder: (context, state) { if (state.isLoading) return CircularProgressIndicator(); if (state.hasError) return Text('Error: ${state.error}'); return Text('Token: ${state.data}'); }, ),
SizedBox(height: 20),
// Display user profile BlocBuilder<QueryCubit<User>, QueryState<User>>( builder: (context, state) { if (state.isLoading) return CircularProgressIndicator(); if (state.hasError) return Text('Error: ${state.error}'); return Text('User: ${state.data?.name}'); }, ),
SizedBox(height: 20),
// Update profile button BlocBuilder<MutationCubit<User, User>, MutationState<User>>( builder: (context, state) { return ElevatedButton( onPressed: state.isLoading ? null : () => context.read<MutationCubit<User, User>>().mutate( User(name: 'Updated Name'), ), child: state.isLoading ? CircularProgressIndicator() : Text('Update Profile'), ); }, ), ], ); }}Security Best Practices
Section titled “Security Best Practices”1. Always Use Configured Client
Section titled “1. Always Use Configured Client”// Good - Use configured clientQueryCubit<String>( key: 'secure-data', queryFn: () => fetchData(), client: context.queryClient,)
// Bad - Using default client without security configQueryCubit<String>( key: 'secure-data', queryFn: () => fetchData(), // Missing client parameter)2. Mark Sensitive Data as Secure
Section titled “2. Mark Sensitive Data as Secure”// Good - Sensitive data marked as secureQueryOptions( isSecure: true, maxAge: Duration(minutes: 15),)
// Bad - Sensitive data not marked as secureQueryOptions( isSecure: false, // Sensitive data could be persisted)3. Use Appropriate TTL Values
Section titled “3. Use Appropriate TTL Values”// Good - Short TTL for sensitive dataQueryOptions( isSecure: true, maxAge: Duration(minutes: 15), // Short-lived tokens)
// Bad - Too long TTL for sensitive dataQueryOptions( isSecure: true, maxAge: Duration(hours: 24), // Too long for sensitive data)4. Handle Security Errors Gracefully
Section titled “4. Handle Security Errors Gracefully”BlocConsumer<QueryCubit<String>, QueryState<String>>( listener: (context, state) { if (state.hasError) { // Handle security-related errors if (state.error.toString().contains('validation')) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Invalid input detected')), ); } } }, builder: (context, state) { // Build UI },)Migration Guide
Section titled “Migration Guide”Enabling Security in Existing Apps
Section titled “Enabling Security in Existing Apps”- Add the security package using the Flutter CLI:
flutter pub add fasq_security[!TIP] This package integrates with the Fasq core and the bloc adapter.
- Wrap your app with
QueryClientProvider:
QueryClientProvider( config: CacheConfig( defaultStaleTime: Duration(minutes: 5), defaultCacheTime: Duration(minutes: 10), ), persistenceOptions: PersistenceOptions( enabled: true, encryptionKey: 'your-encryption-key', ), child: MaterialApp( home: MyApp(), ),)- Update existing QueryCubit instances:
// BeforeQueryCubit<String>( key: 'auth-token', queryFn: () => api.getAuthToken(),)
// AfterQueryCubit<String>( key: 'auth-token', queryFn: () => api.getAuthToken(), options: QueryOptions( isSecure: true, maxAge: Duration(minutes: 15), ), client: context.queryClient,)- Update existing MutationCubit instances:
// BeforeMutationCubit<String, String>( mutationFn: (data) => api.mutate(data),)
// AfterMutationCubit<String, String>( mutationFn: (data) => api.mutate(data), client: context.queryClient,)Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Client not found:
- Ensure
QueryClientProviderwraps your app - Use
context.queryClientto access the configured client
Security validation errors:
- Check query key format (alphanumeric, colon, hyphen, underscore only)
- Ensure durations are non-negative
- Verify cache data doesn’t contain functions
Performance issues:
- Large data (>50KB) is automatically encrypted in isolates
- Consider reducing cache size for better performance
- Use appropriate TTL values to prevent memory bloat
Error Messages
Section titled “Error Messages”| Error | Cause | Solution |
|---|---|---|
| ”Query key must contain only alphanumeric, colon, hyphen, underscore” | Invalid query key format | Use valid characters only |
| ”Secure queries must specify maxAge for TTL enforcement” | Missing maxAge for secure query | Add maxAge to QueryOptions |
| ”staleTime must be non-negative” | Negative duration | Use positive or zero duration |
| ”Cache data cannot be a function or closure” | Function in cache data | Remove functions from cached data |