跳转至

6 Authenticating Users

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:

img

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?

There are some different approaches, but the one used by FavQs.com — and most servers out there — is token-based authentication. It works like this:

  1. The client app — WonderWords, in your case — prompts the user with a Sign In screen. Here, they can enter an email — to identify the user — and a password — to prove they own that user.
  2. Once the user fills in that information and taps submit, the app sends a request to a “sign-in” endpoint on the server. The server then uses that email and password to generate a random-like String, the user token.
  3. From then on, all the app has to do is include that user token — or access token — along with the app token — or API key — in the headers of the HTTP requests.

img

Note

Some servers might generate user tokens that expire after a certain time, but FavQs.com doesn’t. In those cases, the client application doesn’t have to know what the token’s duration is. When a token expires, and you send it to the server, the server lets you know by returning a specific error — often one with the 401 status code. Then, all you have to do is either prompt the user for their credentials again or start a background process known as token refresh. It depends on how sophisticated the API is.

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.

Storing the token in a simple variable also wouldn’t do because you need to keep the user signed in, even when they restart the app. Your next guess might be storing it in a local database, which wouldn’t be entirely wrong… It just can’t be any database.

User tokens, and any personally identifiable information (PII) such as emails and usernames, are extremely sensitive and shouldn’t be stored in regular databases. The flutter_secure_storage package is your friend here.

flutter_secure_storage provides you with a simple key-value interface that, under the hood, leverages the most recommended secret storage on each platform: Apple’s Keychain on iOS and Google’s Keystore on Android. Time to get your hands dirty.

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.

Wait for the command to finish executing, then look for the user_repository package and open the user_secure_storage.dart file inside lib/src.

img

Kick things off by replacing // TODO: Create a secure Data Source. with:

// 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,
      );
}

Here’s what’s going on:

  1. If you recall from Chapter 2, “Mastering the Repository Pattern”, data sourcesare classes your repositories use to interact with external sources, like databases and the network. This UserSecureStorage class you’re creating here will act as one of the data sources of your UserRepository class. Its role is to expose UserRepository to functions that help maintain the authenticated user’s information.
  2. The FlutterSecureStorage class comes from this flutter_secure_storagepackage you were reading about. Look at the functions’ implementation in this file to see how easy it is to work with the package.
  3. If you haven’t seen this word before, upsert is a common neologism in software development that combines update and insert. In other words, it stands for: Update the registry if one already exists or insert if it doesn’t.
  4. This Future.wait function combines multiple Futures into one, allowing you to execute them simultaneously. This is useful when the Future calls aren’t dependent on each other, that is, when you don’t have to wait for one Futureto complete to execute the next.

Done! That’s all there is to “storing sensitive data” in Flutter and the flutter_secure_storage package. Easy, right?

You now have everything you need to jump to the UserRepository class and start connecting the dots.

Signing in Users

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

img

Note

Notice there are two user_repository.dart files, make sure you open the inner one within the src folder.

Scroll down until you find the signIn() function. This is the function that runs when the user taps the “Sign In” button on the Sign In screen.

img

Replace // TODO: Sign in the user by coordinating the Data Sources. with:

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();
}

This is an excellent refresher on Chapter 2, “Mastering the Repository Pattern”. Here, you:

  1. Called the “sign-in” endpoint on the server using the remoteApi property, which is of type FavQsApi. If the request succeeds, you get a UserRM object back from the server and assign it to the apiUser property. The UserRMclass holds the recently signed-in user’s token, email and username.
  2. Used the upsertUserInfo() function you just created in the UserSecureStorage class.
  3. Captured any InvalidCredentialsFavQsExceptions and converted them to InvalidCredentialsExceptions. Doing so is important because InvalidCredentialsFavQsException is only known by packages importing the fav_qs_api internal package, which won’t be the case for users of this UserRepository class. InvalidCredentialsException, on the other hand, is part of the domain_models package and, therefore, is known to all features, making it possible for them to handle the exception properly.

The code you just wrote perfectly portrays the role of a repository: orchestrating different data sources. As you can see from this signIn() function, UserRepository is juggling between two data sources:

  1. remoteApi of type FavQsApi, which talks to your remote API.
  2. _secureStorage of type UserSecureStorage, which you created in the last section, uses the flutter_secure_storage package.

Before you continue knocking out more TODOs in the code, there’s one important concept to clear up: the difference between ephemeral state and app state.

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.

If the app opens on the home screen, and now the user is on the sign-in screen, that’s part of your state. If the fields on a sign-in screen were empty when you opened it, and now they contain input, that’s also part of your state. Another way to think about it is: What information would you need to back up if you had to, from scratch, recreate your UI exactly as it is right now?

Now, some pieces of your state are related to a single widget, such as what’s within an email field. That’s ephemeral state. Others are broader and affect multiple parts of the app, such as “What user is currently signed in?”. Here enters app state.

img

Image recreated from Differentiate between ephemeral state and app state and used under a Creative Commons Attribution 4.0 International License

You won’t find yourself managing app state as frequently as you do for ephemeral state — most of your app’s state is taken care of by external packages or the Flutter framework itself, as it happens for the navigation stack.

In WonderWords, for example, there are only two situations where you’re in charge of managing the app state:

  1. The currently signed-in user’s information.
  2. The dark mode preference the user has selected for that device.

You know this book has a crush on the Bloc library for managing ephemeral state. But how about app state? Well, here, things are grayer, and you need to take the specific situation into account more than ever. Cubits and Blocs can also do a great job here, but for the two situations from WonderWords outlined above, managing them inside the UserRepository itself makes the most sense. You’ll now see how this looks in practice.

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();

BehaviorSubject is a class that:

  1. Holds a value — from the type you specify within the angle brackets <>.
  2. Provides a stream property that you can use to listen for any changes to that value. When a piece of code starts listening to a BehaviorSubject’s stream, it immediately gets the latest value on that property — assuming one has already been added — followed by all the subsequent changes to that value.

What else does one need to manage a piece of state? Think about it for a second: What’s state management if not the art of watching a value and notifying interested parties of changes to that value?

You’ll now see how to use that BehaviorSubject.

Note

The BehaviorSubject class comes from the RxDart package, which this user_repository package depends on.

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,
);

Here, you:

  1. Used a mapper function, toDomainModel(), to convert the apiUser object from the UserRM type to the User type. UserRM is the type your network layer uses — fav_qs_api internal package — while User is the neutral model known by the rest of the codebase.
  2. Replaced — or added, if this is the first sign-in — a new value to your BehaviorSubject.

All errors in your IDE should now be gone. Build and run your app to ensure you’re on the right track, but don’t expect the app to do much yet except for loading indefinitely.

img

Note

If you’re having trouble running the app, it’s because you forgot to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.

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.

Find // TODO: Expose the BehaviorSubject. and replace it with:

// 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;

You’re adding this code to the getUser() function, which exposes a Stream so users of UserRepository can monitor changes to the currently authenticated user. This is the same function you used in the previous chapter to refresh the home screen when the user signs in to or out of the app. In the code above, you:

  1. Check if you’ve already added a value to _userSubject. If not, that means this is the first time the app has called this function. Therefore, you need to set the _userSubject with the values you have in the secure storage — which is what you do inside the if block.
  2. Then, all you have to do is return the stream property of _userSubject. Well, you’re not actually returning the Stream, but that’s just because getUser() is an async* function, which makes it impossible to returnanything. Instead, you use the yield* keyword, which generates a new Stream that just re-emits all the values from _userSubject.stream.

Amazing job! Build and run your app again, and this time you should have no problems loading your home screen:

img

There’s still one issue, though… When a user signs in, the Profile tab reflects that correctly, but the rest of the app doesn’t. For example, the user’s favorites aren’t synced, and if you try to favorite a quote, you’ll still see an error saying you’re not signed in.

img

This is happening because you still have to implement the function that supplies the user token to the places that need it.

Supplying the Access Token

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

return _secureStorage.getUserToken();

And just like that, your code is complete. In the end, the main application package will take care of connecting the fav_qs_api package, which actually injects the token in the headers, to this getUserToken() function you added your code to. More on this in Chapter 7, “Routing & Navigating”.

Build and run your app again, and you won’t be disappointed this time. Play with the user’s authentication for a bit… For example, open the Profile tab while signed out and notice it doesn’t show the username at the top. Sign in, and see how the screen refreshes immediately — due to it listening to your getUser() function.

img

Note

If you don’t have an account, you can create one using the Sign-upfeature within WonderWords itself or use the FavQs.com website.

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.