Skip to main content

Provide sign-in

The auth role splits sign-in between its provider and its template. A provider keeps the accounts of the users and knows who is signed in on the device, such as a backend with authentication. The template of the role owns what is the same with every provider: the session of the app, the mode, and what happens to an anonymous user who gets an account. Sign-in describes what the app has.

The service​

The template generates lib/core/auth/auth_service.dart with what a provider implements and what its calls fail with:

In the fileWhat it is
AuthServiceThe interface that a provider implements.
AuthUserA user as the provider knows them: a uid, which stays the same for as long as the user exists, isAnonymous, and an email or null.
AuthFailureThe failure of a call, the only thing that such a call throws: a reason and, for the developer of the app, a developerHint or null.
AuthFailureReasonWhy a call failed.

This is the interface, without its comments:

lib/core/auth/auth_service.dart
abstract interface class AuthService {
AuthUser? get currentUser;

Stream<AuthUser?> get userChanges;

Future<void> signIn({required String email, required String password});

Future<void> signUp({required String email, required String password});

Future<void> linkPassword({required String email, required String password});

Future<void> signInAnonymously();

Future<void> sendPasswordReset(String email);

Future<void> signOut();

Future<void> deleteAccount();
}

The code of the app does not call it. It signs in through appSession, which calls this service.

The module​

A provider contributes its implementation as data of the role, as the providers of the other service roles do. This is the built-in firebase_auth:

  
ModuleDescriptor get descriptor => const ModuleDescriptor(
id: id,
description: 'Firebase Authentication with firebase_auth',
kind: ModuleKinds.infrastructure,
dependsOn: {FirebaseCoreModule.id},
uses: {localizationRole},
providers: [RoleProvider.plain(authRole)],
);


List<Contribution> contribute(ModuleContext context) => [
BrickContribution(
firebaseAuthBundle,
vars: {
'email_language': const RoleVar(
localizationRole,
present: _emailLanguage,
absent: '',
),
// For the script that enables the ways to sign in: the file
// that it reads the mode of the app from, how the auth role
// says to read it, the mode with anonymous users, and the
// Firebase CLI that it asks for.
'session_file': AuthRole.sessionFile,
'mode_declaration': modeDeclarationCode,
'anonymous_mode': AuthMode.anonymous.name,
'minimum_firebase_cli': firstFirebaseCliWithSignIn,
},
),
const PubspecContribution.hosted('firebase_auth', '^6.7.0'),
authRole.data(
const RoleImplementation(
type: TypeRef('FirebaseAuthService', import: _file),
create: FactoryRef('createFirebaseAuthService', import: _file),
),
),
const Preflight([firebaseCliWithSignIn]),
enableSignIn,
AppEntryRole.readmeSections.entry(readmeHeading, readmeSection),
AppEntryRole.agentSections.entry(
authRole.description,
AgentNote(agentNote),
),
];
  • A provider contributes one implementation. Its function is in the file of the implementation and takes no arguments. An implementation that is created asynchronously uses RoleImplementation.async, and initAuth() of the role awaits it.
  • What the service keeps to is the same in every mode of the app. A module whose brick alone implements it, with the same code for every mode, is a plain provider, as firebase_auth is. A provider that renders something by the mode reads the mode in its render hook, with authRole.modeIn(input). A tool of the app that follows the mode reads it from the app; see a tool that reads the mode.
  • The brick of firebase_auth reads email_language, a variable that depends on a role. In an app with the localization role it is the code that gives Firebase the language of the app, and elsewhere it is empty.
  • The Preflight and enableSignIn are the check and the step with which the module enables the sign-in methods in the Firebase project after generation; see a step with a notice.
  • A provider tells in a README section of its own what its side needs for a mode, such as how anonymous users are enabled. The role writes the section "Sign-in", whose heading is AuthRole.readmeHeading, and the section of a provider can refer to it.
  • The note of the provider goes under authRole.description in the guide for coding agents, after the note of the role; see Notes for coding agents.

The role uses the router role, and a provider uses what its role uses. So the hooks of a provider may read the routes and the guards of the app, and the contract harness builds the apps of a provider with each router of its registry and without one. Put a router into the registry of the tests of a provider.

A tool that reads the mode​

A provider whose side has to be set up for the mode may give the app a tool for that, a script that dart runs. firebase_auth generates one, tool/enable_firebase_sign_in.dart, which enables anonymous users in the Firebase project only in the mode anonymous.

Such a tool cannot take the mode from the render hook of the provider: that is the mode that the app was generated in, and it is stale from the first time the developer of the app changes authMode. It cannot import the file of the session either, which imports Flutter. So it reads the declaration from the text of that file, and the role says how:

AuthRole.modeDeclarationThe source of a regular expression for a search over the lines of the file, as RegExp(AuthRole.modeDeclaration, multiLine: true). Each match is a comment of the file, or a declaration of authMode that starts its line and has a mode as its value. Only a declaration has the first group, the name of its mode.
authRole.modeWrittenIn(source)The mode that the text of the file declares, or null when the text does not tell it: it has no declaration that the expression takes, or more than one.

The file tells the mode when exactly one match has the first group. A tool stops otherwise, and does not guess.

  • The expression takes the declaration as the template writes it, const AuthMode authMode = AuthMode.required;, and as a developer may leave it: without the type, with final, with its value on the next line, and with a comment after it.
  • It takes no value that is not the name of an AuthMode, no computed value, no declaration within a comment, and none that does not start its line.
  • The comment of authMode in the app tells the developer to keep the declaration in that form.

A script of the app has only the libraries of Dart, so the expression goes into it as code. firebase_auth gives its brick the expression as a variable, and the files of the module stay the same in every mode.

What the role does itself​

The template of the role does the rest, the same with every provider:

  • It generates lib/core/auth/app_session.dart with appSession, the session of the app, the constant authMode, and initAuth(), which creates the service with the function of the provider and starts the session with it. bootstrap() awaits initAuth() in its platform phase, so who is signed in is known before the first frame.
  • The session runs its calls one after another and turns every error that is no AuthFailure into one, with the reason unknown and the error as its developerHint.
  • For an anonymous user, the session calls linkPassword in place of signUp, so that the user keeps the id. Before an anonymous user signs in to an account that exists already, it takes the data of the guest, and it gives that data to the account afterwards, as lib/core/auth/guest_data.dart of the app says.
  • In the mode anonymous, the session calls signInAnonymously when the app starts without a user, waits for it for at most 3 seconds, and tries again each time the user comes back to the app and after each call that leaves nobody signed in.
  • It asks for the mode, writes it into the app, and writes the section "Sign-in" of the README of the app and the note of the role in the guide for coding agents.
  • It publishes AuthRole.account, the condition of a route whose screen is only for the user of an account, and warns when routes ask for it and no guard of the app stands for it.

So a provider keeps no session of its own and declares no guard. A widget of a provider signs in through appSession too.

What every provider keeps to​

The contract of AuthService is in the comments of lib/core/auth/auth_service.dart. It is the same in every mode of the app, whatever the provider renders by the mode:

  • The function of the implementation returns a service whose currentUser is the user of the device already, without a request to the server. It may be called again, and the service that it returns then has the user who is signed in now. A test runs initAuth() again to see what the next launch of the app finds.
  • When a call completes, currentUser is its result. userChanges, a broadcast stream, tells of every change of currentUser, also of one that no call made, and when it tells of a user, currentUser is that user. An error that the stream sends changes nothing: the session reports it with FlutterError.reportError and goes on listening.
  • Every failure is an AuthFailure, and every call ends. When the server does not answer, the call fails with the reason network.
  • signIn is also called while a user is signed in, an anonymous one or the user of another account, and signUp while the user of another account is. The user of the call then replaces that user on the device.
  • A wrong password and an address without an account are both invalidCredentials, and sendPasswordReset completes whether the address has an account or not. So nobody learns from the app which addresses have an account.
  • signUp and linkPassword with an address that has an account fail with emailInUse and change nothing.
  • A call that fails leaves the user who is signed in as that user was. An anonymous user stays signed in when linkPassword fails, and so does whoever is signed in when signIn or signUp fails.
  • signInAnonymously creates a user with an id of its own, and linkPassword keeps the id of the user.
  • deleteAccount deletes the user who is signed in, an anonymous one too, and signs out. It succeeds for a user who has just signed in, and for an anonymous user who was just created. For a user who signed in longer ago, such as one whom the service found on the device, it may fail with recentSignInRequired, and it succeeds once the user has signed in again. The role has no call that asks for the password again.
  • linkPassword and deleteAccount are calls for the user who is signed in. Made when nobody is signed in, such a call fails with recentSignInRequired. Made for a user whose session has ended on the server, it signs that user out on the device first and then fails with the same reason. That is the one case in which a call that fails changes who is signed in.
ReasonWhen a call fails with it
invalidCredentialsThe email address and the password match no account.
emailInUseThe email address has an account already.
weakPasswordThe provider takes no password as weak as this one.
invalidEmailThe text is no email address.
userDisabledThe account is disabled.
tooManyAttemptsThe provider takes no more attempts for a while.
networkThe device did not reach the provider.
recentSignInRequiredThe call needs a user who signed in a short while ago, or the session of the user has ended on the server.
notConfiguredSign-in is not set up at the provider, such as a way to sign in that is not enabled there. The developerHint explains the mistake in the setup.
unknownAnything else. The developerHint has the error.

A package may promise less than the role, and the implementation then makes up the difference. The firebase_auth package keeps a deleted user as its current one until the platform tells it of the sign-out, so FirebaseAuthService signs out after it deleted a user. firebase_auth lists what else it does.

Rules​

The structural rules of the role read what each Dart file of an app imports and calls:

RuleWhat it requires
auth.factory_callsNo file of a module, and none of the template of another role, calls createAuthService(). Of such files, only those of the implementation of the provider, the files of its class and of its function, import or export the file of AuthService.
auth.start_callsOnly the file of bootstrap() calls initAuth(), and no file of a module or of the template of another role calls appSession.start().
auth.implementation_factoriesThe function of the implementation is in its file and takes no arguments.

The module rule services.implementations, which the role shares with the other service roles, checks that a provider, and only a provider, contributes one implementation.

The rules do not see a part file of a library that imports the file of the session, a file that reaches that file through an export of another file of the app, or appSession under another name, such as a variable that it was assigned to.

Tests​

The contract harness checks the rules above, and builds the apps of a provider in each mode of the role; see Test a module. Only a running app shows whether a provider keeps to the contract of the service.

SMF's CI checks that in running apps, in every mode, with every provider of the role that SMF has. The test of the role calls the service of the provider through createAuthService(), takes appSession through a sign-up, a sign-out, a sign-in and the deletion of the account, and runs initAuth() again to see what the next start finds. CI runs the same test on two providers with a known bug, and it must fail on each: one that returns its service before it has read who is signed in on the device, and one that gives an anonymous user a new id in linkPassword(). Contributing lists these tests.

The tests that a provider keeps for what only it does use the implementation of its own module, as those of firebase_auth use createFirebaseAuthService(), and check what reaches its platform side. Sign-in describes what a test of the session can do.