Skip to Content
DocumentationBloc AdapterSecurity Features

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

  1. 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.

  1. 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(), ), )
  1. 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, )
  1. 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 QueryClientProvider wraps your app
  • Use context.queryClient to 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

ErrorCauseSolution
”Query key must contain only alphanumeric, colon, hyphen, underscore”Invalid query key formatUse valid characters only
”Secure queries must specify maxAge for TTL enforcement”Missing maxAge for secure queryAdd maxAge to QueryOptions
”staleTime must be non-negative”Negative durationUse positive or zero duration
”Cache data cannot be a function or closure”Function in cache dataRemove functions from cached data
Last updated on