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).
I thought the solution that I should implement shouldn’t be too complex and time consuming, something that can be plugged in easily without touching the existing codebase. And I found something! And 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, by just inserting a few lines of code to 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. This means that the password will be stored in plain text in the browser’s local storage to not ask the user to enter the password every time they visit the site.
First, create a new file password_protection_overlay.dart
anywhere in your project and start by adding a const
variable to store the password:
const String _appPassword = 'flutterweb';
Then create a new class PasswordProtectionOverlay
that extends StatefulWidget
. Now, let’s add the basic structure of the widget then 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 _invalidPasslock = 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
_checkPasslock();
}
@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: (_) {
_submitPasslock();
},
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>(
_invalidPasslock ? Colors.red : Colors.blue,
),
),
onPressed: () {
_submitPasslock();
},
child: Text(
_invalidPasslock ? 'Wrong Password' : 'Submit',
),
),
],
),
),
),
);
}
Future<void> _checkPasslock() async {
// We'll implement this later
}
void _submitPasslock() async {
// We'll implement this later
}
}
Here is what the initial implementation should look like:
In the _checkPasslock
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> _checkPasslock() 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 _submitPasslock
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 _submitPasslock() 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(() {
_invalidPasslock = true;
});
// Hide the error message after a short delay
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_invalidPasslock = false;
});
}
}
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.