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 file | What it is |
|---|---|
AuthService | The interface that a provider implements. |
AuthUser | A 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. |
AuthFailure | The 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. |
AuthFailureReason | Why a call failed. |
This is the interface, without its comments:
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, andinitAuth()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_authis. A provider that renders something by the mode reads the mode in its render hook, withauthRole.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_authreadsemail_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
PreflightandenableSignInare 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.descriptionin 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.modeDeclaration | The 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, withfinal, 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
authModein 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.dartwithappSession, the session of the app, the constantauthMode, andinitAuth(), which creates the service with the function of the provider and starts the session with it.bootstrap()awaitsinitAuth()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
AuthFailureinto one, with the reasonunknownand the error as itsdeveloperHint. - For an anonymous user, the session calls
linkPasswordin place ofsignUp, 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, aslib/core/auth/guest_data.dartof the app says. - In the mode
anonymous, the session callssignInAnonymouslywhen 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
currentUseris 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 runsinitAuth()again to see what the next launch of the app finds. - When a call completes,
currentUseris its result.userChanges, a broadcast stream, tells of every change ofcurrentUser, also of one that no call made, and when it tells of a user,currentUseris that user. An error that the stream sends changes nothing: the session reports it withFlutterError.reportErrorand goes on listening. - Every failure is an
AuthFailure, and every call ends. When the server does not answer, the call fails with the reasonnetwork. signInis also called while a user is signed in, an anonymous one or the user of another account, andsignUpwhile 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, andsendPasswordResetcompletes whether the address has an account or not. So nobody learns from the app which addresses have an account. signUpandlinkPasswordwith an address that has an account fail withemailInUseand change nothing.- A call that fails leaves the user who is signed in as that user was. An anonymous user stays signed in when
linkPasswordfails, and so does whoever is signed in whensignInorsignUpfails. signInAnonymouslycreates a user with an id of its own, andlinkPasswordkeeps the id of the user.deleteAccountdeletes 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 withrecentSignInRequired, and it succeeds once the user has signed in again. The role has no call that asks for the password again.linkPasswordanddeleteAccountare calls for the user who is signed in. Made when nobody is signed in, such a call fails withrecentSignInRequired. 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.
| Reason | When a call fails with it |
|---|---|
invalidCredentials | The email address and the password match no account. |
emailInUse | The email address has an account already. |
weakPassword | The provider takes no password as weak as this one. |
invalidEmail | The text is no email address. |
userDisabled | The account is disabled. |
tooManyAttempts | The provider takes no more attempts for a while. |
network | The device did not reach the provider. |
recentSignInRequired | The call needs a user who signed in a short while ago, or the session of the user has ended on the server. |
notConfigured | Sign-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. |
unknown | Anything 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:
| Rule | What it requires |
|---|---|
auth.factory_calls | No 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_calls | Only the file of bootstrap() calls initAuth(), and no file of a module or of the template of another role calls appSession.start(). |
auth.implementation_factories | The 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.