Security Features with fasq_bloc
fasq_bloc supports all FASQ security features through QueryClient configuration, enabling secure data handling in your Bloc-based applications.
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
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
- 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
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
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
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
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
1. Always Use Configured Client
// Good - Use configured client
QueryCubit<String>(
key: 'secure-data',
queryFn: () => fetchData(),
client: context.queryClient,
)
// Bad - Using default client without security config
QueryCubit<String>(
key: 'secure-data',
queryFn: () => fetchData(),
// Missing client parameter
)2. Mark Sensitive Data as Secure
// Good - Sensitive data marked as secure
QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
)
// Bad - Sensitive data not marked as secure
QueryOptions(
isSecure: false, // Sensitive data could be persisted
)3. Use Appropriate TTL Values
// Good - Short TTL for sensitive data
QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15), // Short-lived tokens
)
// Bad - Too long TTL for sensitive data
QueryOptions(
isSecure: true,
maxAge: Duration(hours: 24), // Too long for sensitive data
)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
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:
// Before
QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
)
// After
QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
options: QueryOptions(
isSecure: true,
maxAge: Duration(minutes: 15),
),
client: context.queryClient,
)- Update existing MutationCubit instances:
// Before
MutationCubit<String, String>(
mutationFn: (data) => api.mutate(data),
)
// After
MutationCubit<String, String>(
mutationFn: (data) => api.mutate(data),
client: context.queryClient,
)Troubleshooting
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
| 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 |
Last updated on