Skip to content

🔒 Adding password protection to Flutter web projects

 at 

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:

Initial Interface

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.



Subscribe to the newsletter
Kamran Bekirov

I'm Kamran Bekirov
Built over 70 mobile apps.
Building UserOrient for Flutter
Solo apps: LibroKit, Beyt
Packages: logarte, versionarte
Reach: X, LinkedIn, Instagram, GitHub