Chapters

Hide chapters

Real-World Flutter by Tutorials

First Edition · Flutter 3.3 · Dart 2.18 · Android Studio or VS Code

Real-World Flutter by Tutorials

Section 1: 16 chapters
Show chapters Hide chapters

6. Authenticating Users
Written by Edson Bueno

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In Chapter 4, “Validating Forms With Cubits”, you used a signIn() function from a UserRepository class to ultimately send the server what the user entered in the email and password fields:

if (isFormValid) {
  try {
    await userRepository.signIn(
      email.value,
      password.value,
    );

    // ...
  } catch (error) {
    // ...
  }
}

Then, in Chapter 5, “Managing Complex State With Blocs”, your path crossed that UserRepository class again, this time through a getUser() function:

_authChangesSubscription = userRepository.getUser().listen(
  (user) {
    _authenticatedUsername = user?.username;
    add(
      const QuoteListUsernameObtained(),
    );
  },
);

As you can see, getUser() returns a Stream<User?>, which you use to monitor changes to the user’s authentication and refresh the home screen when the user signs in to or out of the app.

At this point, you might’ve noticed a strong connection between these two pieces of code above:

UserRepository. signIn() UserRepository. getUser() ? Calls Emits a new object Something happens SignInCubit QuoteListBloc

This chapter is where you’ll fill in that gap and unravel the mysteries of user authentication. Along the way, you’ll learn:

  • What authentication is.
  • The difference between app authentication and user authentication.
  • How token-based authentication works.
  • How to store sensitive information securely.
  • How to use the flutter_secure_storage package.
  • The difference between ephemeral state and app state.
  • How to use the BehaviorSubject class from the RxDart package.

While going through this chapter, you’ll work on the starter project from this chapter’s assets folder.

Understanding Authentication

Authentication is how you identify yourself and prove your identity to a server. That can be both at the app level and at the user level.

In Chapter 1, “Setting up Your Environment”, you created an account at FavQs.com — the API server behind WonderWords — to generate something called the API key. You then learned how to configure compile-time variables in Dart to safely inject that key into your code, which you then set up to include that key in the headers of all HTTP requests. That was app-level authentication. You’re passing the API key in your requests to prove to FavQs.com that you’re not a random — or even malicious — app.

App-level authentication is sufficient for operations that aren’t tied to a particular user, like getting a list of quotes. But how about favoriting a quote?

Favoriting, upvoting or downvoting a quote are examples of actions that need a user associated with them. When a user favorites a quote, it doesn’t become a favorite for all users, only for the particular user who executed the action. Now, how does the server know which user it should favorite that quote for? Or, even further, how does the server know the client app is authorized to execute that action on behalf of the user? Here enters user-level authentication.

Understanding Token-based User Authentication

To authenticate your app, all you had to do was generate a key on FavQs.com and include it in all your HTTP requests. But how do you authenticate users?

'Eibkewusoyeav' : 'Xiveh xecoj=6i5s617026ed21m957sd18b44850287fv' { } Pijpif-iad Uqaf’g DKWB Meohup 'Ikah-Xoyov' : 'BWEI0K+LKArQfKceck2RPEeFrVeGSpq8324k8I26rqLfFeT43fpxN3==' 'Iannuqoqedout' : 'Lijuc gugoq=2a5m393418uj32s754nl03h19862209jk' { } Nijyip-ez Ilaj’q SSNJ Zoeveh Ojr corop Usul yoluz Avv viveq

Storing Access Tokens Securely

Once you’ve called the “sign-in” endpoint and gotten the user’s token, your next step is to store that token somewhere. But where? Compile-time variables aren’t an option since you don’t have the user token at compile-time, only at runtime.

Creating a Secure Data Source

Use your IDE of choice to open the starter project. Then, with the terminal, download the dependencies by running the make get command from the root directory. Ignore all the errors in the project for now.

// 1
class UserSecureStorage {
  static const _tokenKey = 'wonder-words-token';
  static const _usernameKey = 'wonder-words-username';
  static const _emailKey = 'wonder-words-email';

  const UserSecureStorage({
    // 2
    FlutterSecureStorage? secureStorage,
  }) : _secureStorage = secureStorage ?? const FlutterSecureStorage();

  final FlutterSecureStorage _secureStorage;

  // 3
  Future<void> upsertUserInfo({
    required String username,
    required String email,
    String? token,
  }) =>
      // 4
      Future.wait([
        _secureStorage.write(
          key: _emailKey,
          value: email,
        ),
        _secureStorage.write(
          key: _usernameKey,
          value: username,
        ),
        if (token != null)
          _secureStorage.write(
            key: _tokenKey,
            value: token,
          )
      ]);

  Future<void> deleteUserInfo() => Future.wait([
        _secureStorage.delete(
          key: _tokenKey,
        ),
        _secureStorage.delete(
          key: _usernameKey,
        ),
        _secureStorage.delete(
          key: _emailKey,
        ),
      ]);

  Future<String?> getUserToken() => _secureStorage.read(
        key: _tokenKey,
      );

  Future<String?> getUserEmail() => _secureStorage.read(
        key: _emailKey,
      );

  Future<String?> getUsername() => _secureStorage.read(
        key: _usernameKey,
      );
}

Signing in Users

Continuing in the same directory of the file you were working on before, open user_repository.dart this time.

try {
  // 1
  final apiUser = await remoteApi.signIn(
    email,
    password,
  );

  // 2
  await _secureStorage.upsertUserInfo(
    username: apiUser.username,
    email: apiUser.email,
    token: apiUser.token,
  );

  // TODO: Propagate changes to the signed in user.
} on InvalidCredentialsFavQsException catch (_) {
  // 3
  throw InvalidCredentialsException();
}

Differentiating Between Ephemeral State and App State

You already know what state is: the conjunction of variables that describe what changed in your app since the user opened it.

Suni Nde muuqn eg? Mola goymarj Qiwpxi viflom Afwetociuq kgife Butd pohkajr Ukp sqeco

Managing App State With BehaviorSubject

Still in user_repository.dart, find // TODO: Create a listenable property. and replace it with:

final BehaviorSubject<User?> _userSubject = BehaviorSubject();

Notifying Changes in the User State

Scroll down back to the signIn() function and replace // TODO: Propagate changes to the signed in user. with:

// 1
final domainUser = apiUser.toDomainModel();

// 2
_userSubject.add(
  domainUser,
);

Providing a Way to Listen to Changes in the User State

You’ve seen how to add a value to a BehaviorSubject, and now’s the time to see how to listen to changes in it.

// 1
if (!_userSubject.hasValue) {
  final userInfo = await Future.wait([
    _secureStorage.getUserEmail(),
    _secureStorage.getUsername(),
  ]);

  final email = userInfo[0];
  final username = userInfo[1];

  if (email != null && username != null) {
    _userSubject.add(
      User(
        email: email,
        username: username,
      ),
    );
  } else {
    _userSubject.add(
      null,
    );
  }
}

// 2
yield* _userSubject.stream;

Supplying the Access Token

Continuing on user_repository.dart, scroll up to // TODO: Provide the user token. and replace it with:

return _secureStorage.getUserToken();

Key Points

  • App authentication is how your app proves to the server there’s a legit client app behind the requests.
  • App authentication is usually accomplished by attaching a static long-formed String — the app token — to the headers of the requests. That token is the same across all installations of the app. You went over this process in Chapter 1, “Setting up Your Environment”.
  • On the other hand, user authentication is how your app proves to the server there’s a known user behind the app. This is only required to access and generate user-specific data within the server, such as reading and marking favorites.
  • User authentication is usually accomplished similarly to app authentication, where you attach a long-formed String — the user token, in contrast to the app token — to the header of your requests. The difference here is that the server generates this token on the fly, for every new sign-in request.
  • Token-based authentication works like this: The client app makes a request to the server to exchange the user’s email and password for a long-formed String — the access token or the user token. From then on, all the app has to do is attach that user token along with the app token to the headers of all HTTP requests.
  • Don’t store your users’ private data, such as JWTs and PII, in regular databases. An alternative is using the flutter_secure_storage package, which gives you access to Apple’s Keychain on iOS and Google’s Keystore on Android.
  • Ephemeral state is a piece of state associated with a single widget, as opposed to app state, which is related to multiple widgets.
  • The Future.wait function generates a new Future that combines all the other Futures you pass. Use this to execute multiple Futures simultaneously.
  • Using the BehaviorSubject class from the RxDart package is one of the most concise ways to manage app state.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now