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

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:

Initial Interface

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.



Subscribe to the newsletter
Kamran Bekirov

I'm Kamran Bekirov, a Serial Flutter Developer.
Built over 70 mobile apps for clients and myself.
Currently building UserOrient as a SaaS product.
Personal mobile apps: LibroKit, Beyt
Open-source projects: logarte, versionarte
Reach me on: X, LinkedIn, Instagram, GitHub