Problem
On one of our recent projects we decided to use Flutter for the web version to leverage the same codebase for both web and mobile. Before we could deploy the web version, we needed to add password protection to the site so that only authorized users could access it (us).
The solution shouldn’t be too complex and time consuming, something that can be plugged in easily from a point without touching the other parts of the app. And I found something! The solution protects all the pages of the site (even if user tries to access the page directly by typing the URL in the browser) without touching them at all on the code side, by just inserting a few lines of code into the App
widget.
Here is the video of how it looks like:
Solution
To add password protection to a Flutter web project, you can create a new widget that overlays the app’s content and prompts the user to enter a password. If the user enters the correct password, the overlay will disappear, and they’ll be able to access the rest of the site.
NOTE: This solution uses shared_preferences
package to store the password in the browser’s local storage and not ask again. You can use any other storage method you prefer.
Implement UI
First, create a new file password_protection_overlay.dart
and start by adding a const
variable that stores the password:
const String _appPassword = 'flutterweb';
Then create a new class PasswordProtectionOverlay
that extends StatefulWidget
and let’s add the basic structure of the widget (we’ll implement the logic):
import 'package:flutter/material.dart';
const String _appPassword = 'flutterweb';
class PasswordProtectionOverlay extends StatefulWidget {
const PasswordProtectionOverlay({super.key});
@override
State<PasswordProtectionOverlay> createState() =>
_PasswordProtectionOverlayState();
}
class _PasswordProtectionOverlayState extends State<PasswordProtectionOverlay> {
// Whether the user has already entered the password correctly
bool? _authenticated;
// Whether the user entered an incorrect password (to show an error message)
bool _invalidPassword_ = false;
late final TextEditingController _passwordController;
@override
void initState() {
super.initState();
_passwordController = TextEditingController();
// Check if the user has already entered the password from a previous session
_checkPassword();
}
@override
void dispose() {
super.dispose();
_passwordController.dispose();
}
@override
Widget build(BuildContext context) {
// Show a loading spinner while we're checking the cached password
if (_authenticated == null) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
// If the user has already entered the password correctly, hide the overlay
if (_authenticated!) {
return const SizedBox.shrink();
}
// If the user hasn't entered the password yet, show the password input form
return Scaffold(
body: Center(
child: SizedBox(
width: 320.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Enter password to continue',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 32.0),
// Collect the password from the user
TextField(
controller: _passwordController,
onSubmitted: (_) {
_submitPassword();
},
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
suffixIcon: Icon(Icons.lock),
),
),
const SizedBox(height: 32.0),
// Button to submit the password
FilledButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
_invalidPassword_ ? Colors.red : Colors.blue,
),
),
onPressed: () {
_submitPassword();
},
child: Text(
_invalidPassword_ ? 'Wrong Password' : 'Submit',
),
),
],
),
),
),
);
}
Future<void> _checkPassword() async {
// We'll implement this later
}
void _submitPassword() async {
// We'll implement this later
}
}
Here is what the initial implementation should look like:
Implement Logic
Now, let’s implement the logic for checking the password and submitting the user’s input.
In the _checkPassword
method, we’ll check if the user has already entered the password correctly in a previous session. If they have, we’ll set _authenticated
to true
and hide the overlay. If they haven’t, we’ll show the password input form.
Future<void> _checkPassword() async {
// Check the cached password
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String? cached = prefs.getString('password');
// If the cached password matches the app password
final bool valid = cached == _appPassword;
// Hide the overlay if the user is already authenticated
setState(() {
_authenticated = valid;
});
}
In the _submitPassword
method, we’ll check if the user’s input matches the password we’ve set. If it does, we’ll set _authenticated
to true
and hide the overlay. If it doesn’t, we’ll show an error message.
void _submitPassword() async {
// Check if the user entered the correct password
if (_passwordController.text == _appPassword) {
// Hide the overlay
setState(() {
_authenticated = true;
});
// Cache the password in the browser's local storage
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('password', _appPassword);
} else {
// Show an error message
setState(() {
_invalidPassword_ = true;
});
// Hide the error message after a short delay
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_invalidPassword_ = false;
});
}
}
Integrate with the App
The last step is to add the PasswordProtectionOverlay
widget to the App
(or let’s say MaterialApp
) widget’s builder method. This will ensure that the overlay is displayed on top of the app’s content until the user enters the correct password. Here’s how you can do it:
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
return Stack(
children: [
child!,
// Show the overlay on top of the app when running on the web
if (kIsWeb)
const Positioned(
child: PasswordProtectionOverlay(),
),
],
);
},
home: const HomePage(),
);
}
}
That’s it! Now, when you run your Flutter web project, you should see the password input form overlayed on top of your app’s content. If you enter the correct password, the overlay will disappear, and you’ll be able to access the rest of the site.
I hope this solution works for you as well as it did for me. If you would like to reach out to me, you can find me on X/Twitter or LinkedIn.
Get the gist of the code from here.