Sign-in methods

People sign in on one page with an email, a phone number or Google. You choose which of the three are offered; each account can use any of them it has set up.

What it is

The sign-in page is /signup. It asks one question, "Email" (or "Phone number or email" when phone sign-in is on), and shows Continue with Google when Google is on. What happens next depends on what was typed and whether it is registered:

MethodRegisteredNew
EmailPasswordA code by email, then name and password
Phone numberA 6-digit PINA code by SMS, then name and a 6-digit PIN
GoogleOne tapOne tap; name, email and photo come from Google

Whatever the method, a person is one Firebase Auth account plus one users record (see Users and roles). The record is always created by the server, with the site's default role and the arccms_uid claim already in the first ID token.

After signing in, people go back to the page that sent them to sign in (/signup?redirect=/learn), else to their role's home page, which an app sets in src/custom/home.ts (see Set up sign-in).

Someone already signed in who opens /signup never sees the form. Firebase Auth knows from the device that they are signed in long before their users record arrives, so meanwhile the page shows the site's logo, a spinner and "Opening…", then forwards them once AuthState.recordReady() has their record (from the device's copy when there is one, see Add pages and routes). The first frame comes from the signed-in hint this browser keeps (arc:signed-in, see Build your home page), so a signed-out visit shows the form at once with no flash. The form shows when Auth says nobody is signed in, when there is no record for them (a deleted account), or after 20 seconds without one (SIGNED_IN_WAIT_MS).

On a phone the page fits the visible screen (100dvh, 100svh where the browser has no dvh), less the "Built with Arc CMS" line when the site shows it: the logo and the card sit together in the middle and the footer stays at the foot. Only a step taller than the screen, such as the sign-up form, scrolls.

Turn it on or off

Sign-in is core and always on. Email sign-in is always offered. The other two have switches under Settings, User Settings, Sign-in methods:

SwitchDefaultAlso needs
Enable User SignupsOnNothing. Off: only admins can add people (Users, Add User), and existing accounts still sign in.
Phone numberOffThe sms feature (on by default), SMS settings, and one Google Cloud setting (see Google Cloud setup for phone sign-in). The switch checks that setting before it turns on. With sms off in src/custom/features.ts, the switch is hidden, phone sign-in is off whatever it says, and the phone functions are not built.
GoogleOffThe Google provider turned on in the Firebase console.

The Default Role for New Users is set on the same page. The switches take effect at once, with no deploy.

Set it up

  1. Firebase console, Authentication, Sign-in method: Email/Password must be on (email sign-in uses it). Turn on Google if you want Google sign-in. See Configure a project.

  2. Deploy the functions, Firestore rules and indexes (see Deploy).

  3. Phone numbers only: set up SMS under Settings, SMS: the provider and the countries people can sign in from. Keep the Test provider while trying it. See SMS. The project also needs the Google Cloud setting below; the deploy offers to make it.

  4. Settings, User Settings: leave sign-ups on, and turn on Phone number and Google as wanted.

  5. Try each method on /signup in a private window.

Google Cloud setup for phone sign-in

The last step of every phone sign-in creates a custom token, which the functions sign as their own service account (for 2nd gen functions, <project-number>-compute@developer.gserviceaccount.com). A new Firebase project cannot do that until:

Three things make sure nobody finds out the hard way:

  1. The deploy. A deploy that includes functions, with the sms feature on, checks both once per project with gcloud and, in a terminal, offers to set up what is missing. Without gcloud it says so once. It never stops the deploy (see Deploy).
  2. The switch. Turning on Phone number under Settings, User Settings first signs a throwaway token (the checkPhoneSignIn function; it creates no account). If that fails, the switch stays off and the page shows what is missing, a gcloud command for this project to copy, a link to the Google Cloud console, and Check again. With phone sign-in already on, the page runs the same check whenever it opens. If the check itself cannot run (the function is not deployed), the switch turns on anyway, with a warning.
  3. A failed sign-in. If the setting goes missing later, the person signing in sees "Sign-in isn't ready on this site yet. Please contact the site's team.", and every admin gets a notification (admin_sign_in_setup) saying what to fix, at most once a day.

To set it up by hand, with your project id and service account:

gcloud services enable iamcredentials.googleapis.com --project=<project-id>
gcloud iam service-accounts add-iam-policy-binding <service-account> --member=serviceAccount:<service-account> --role=roles/iam.serviceAccountTokenCreator --project=<project-id>

Granting a role needs the Owner or Security Admin role on the project. In the console, the role is added under IAM and admin, IAM.

Use it

The "Phone number or email" field

What a person pastes is cleaned up as it lands, and again on the server: hidden direction marks and zero-width spaces (WhatsApp and contact cards add them), non-breaking spaces, full-width and Devanagari or other Indian digits, a tel: or mailto: link, a name around an address (Asha Rao <asha@example.com>) and the full stop of a sentence. An email is shown in lower case without spaces. A number is read in the country shown beside it (below); an Indian number is shown as 98765 43210, and a number from a country the site does not take is shown in full, such as +447700900123. The cleaning is cleanPasted and cleanEmail in src/shared/utils/identifier.util.ts, mirrored on the server in functions/src/auth/pastedText.ts; both are tested against the same cases.

The country beside a number

As soon as the field starts with a digit, its country appears at the left edge: the flag and the calling code, such as +91. A letter or @ takes it away again, so one field still takes both a number and an email. With one allowed country it is a plain label; with several it is a button that opens the list of allowed countries, the default first (flag, name in the page's language, code), with a search box over eight. The keyboard works too: an arrow opens the list and moves through it, a letter jumps to a name, Enter picks, Escape closes.

The profile's Sign-in methods number box has the same chip, always shown. The chip is the arc-phone-country component and the reading is in src/shared/utils/phone-country.ts; an app can use both for its own number boxes (see Use the custom space, Use core building blocks).

When the field cannot be used, the message under it says what to fix rather than "invalid": a number that is too short or has too many digits, an Indian mobile number that does not start with 6, 7, 8 or 9 (such as 1234567890 beside +91; with several countries it also says to choose the country next to the number), a number from a country the site does not take, a country code that does not look right, an email without a full domain, a word with no @, or a number typed where only email sign-in is on (identifierProblem, messages under member.auth.identifier_error).

Email

A new address gets a 6-digit code by email (template signup_otp_email), valid for 10 minutes, with 5 tries and at least 60 seconds between codes; an address gets at most 5 codes an hour (see Codes, waits and limits). The new account is marked email-verified only when it is created from the browser that entered the code.

Passwords

A new password must be hard to guess. It is refused, with a message that says why, in the member's language, when it is:

That is the strict rule, the default. An app built for casual use can choose simple passwords instead: at least 6 characters, nothing else (see Set up sign-in). The first-run wizard keeps the strict rule for the first admin either way, and the messages say 6 or 8 to match.

The rule is passwordProblem() in src/shared/utils/password-rule.ts, with a mirror for the functions in functions/src/shared/password-rule.ts that a test keeps identical. The page checks it on sign-up, on the profile's change of password, when a phone account adds an email and its first password, on Users, Add User and Edit, and in the first-run wizard; the functions check it again where they set a password themselves (linkEmail, adminCreateUser, adminSetPassword for Users, Edit, and resetPassword after Forgot password) and refuse with the reason weak-password and the problem. Only new passwords are checked: a password set before still signs in, whatever its length. The one place the rule does not reach is Firebase's own reset page, which applies only Firebase's 6-character minimum: a site uses it only while its email engine sends nothing (see Forgot password).

The messages are member.auth.password_error.<problem>; Users, Add User and the wizard say them in English, as the rest of those screens.

Waking the functions early

Each step of a sign-in is its own Cloud Function, and one that has not run for a while takes a few seconds to start. So that the person does not wait for each one in turn, the sign-in page calls the functions its sign-in methods use with { warmUp: true } as it opens, at most once every 5 minutes (SignInService.warmUp(), the list is SIGN_IN_FUNCTIONS). A warm-up call does nothing and returns at once, before any check or rate limit, so it costs a function call and nothing else. The code email is sent by the function that creates the code, not by the email trigger, which would be one more function to start.

The page's settings

What the page offers (phone numbers, Google, the countries, whether sign-ups are open) comes from Settings/users. The page starts from the copy this browser kept from its last visit (arc-sign-in-settings in local storage, written by UserSettingService.readSettings() and by saving Settings, User Settings) and reads the document again in the background, beside the onboarding check rather than after it. A first visit has no copy, and the read can take a few seconds on a cold start. A Continue tapped before the settings are in is held: the button shows as busy, and the step goes on by itself once they arrive. If they cannot be read and there is no copy, or the read takes more than 20 seconds (SETTINGS_WAIT_MS), the page says "We couldn't load the sign-in options. Check your connection and try again." and the next Continue tries again. The server still checks sign-ups and the methods itself, so an old copy can only change what the page shows first.

Phone number

Codes, waits and limits

Messages in the member's language

Every refusal the sign-in functions send a member carries a reason in details.reason, such as wait, too-many-codes or code-wrong, with its numbers beside it (wait in seconds, retryAfter in seconds, remaining tries, the allowed codes). The functions build them with refuse(code, reason, message, extra) in functions/src/auth/refusal.ts. SignInService shows the member string member.auth.server_error.<reason> (dashes become underscores) in the member's language, so every sign-in page and the profile get it through readSignInError(); the server's English shows only for a reason with no string. A test fails when a sign-in function refuses a member without a reason, or when a reason has no English or Hindi string.

Forgot password

Forgot password on the password step works like Forgot PIN, all on the sign-in page, so an installed app never sends the person out to the browser:

  1. The email engine sends a 6-digit reset code (requestPasswordReset, template password_reset_otp_email). It has the same 10 minutes, 5 tries, waits and limits as a sign-up code (see Codes, waits and limits), and it goes through the email engine, so it is in Email Logs and uses the site's sender.
  2. The person enters the code in the code boxes ("Reset your password"). verifySignupOtp checks it with the purpose reset and hands back a ticket: a reset code never passes as a sign-up code, nor the other way round.
  3. They choose a new password ("Choose a new password"). The page checks it against the password rule, and resetPassword checks it again with the person's name and email from their record, sets it with the Admin SDK, uses the code up and signs out every other session. The page then signs in with the new password, as at the password step.

Who can reset here: an account of this site, including a login shared with a host app (authOwner: shared), whose new password then works in that app too. A login the host app owns (authOwner: host), or one with no Arc CMS record, is told "Reset your password in the app you signed up with." (reason host-account). A locked app account and a blocked one are refused as everywhere else.

When the email engine sends nothing (email switched off, no provider, the address suppressed), requestPasswordReset answers sent: false and the page falls back to Firebase's own reset email (sendPasswordResetEmail, in the member's language where Firebase has a template for it). Its link opens Firebase's reset page, which applies only Firebase's 6-character minimum. Set up a real email provider so that every reset goes through Arc CMS.

To test resets without an inbox, choose the Debug Provider (Log Only) in Settings, Email Settings. The reset code is then in Email Logs, and the page says so. To have the page show it, as it shows a sign-up code, turn on Show password reset codes on screen beside the provider (stored as Settings/email.showResetLinks). The switch is off by default and says why: anyone who knows an email could then reset its password. Leaving the Debug Provider turns it off. Only the server reads it (resetCodesShown in functions/src/auth/signupOtp.ts).

While a step runs

Google

A first-time Google user gets a record straight from their verified Google profile, with no form. If the address already has a password account, the person is asked for the password once and Google is then connected to that account.

Profile, Sign-in methods

On the profile page (/profile, or /user/profile in the member area) a signed-in person can add or change their email or, with phone sign-in on, their phone number (each with a code), change their PIN, and connect Google. A password or PIN is asked for only when the account has none yet.

Moving an email or number to another account

When the email or number someone adds is already on another account, the page says so, and verifying the code moves it. The other account keeps all its data:

Extend it

A new sign-in method (another provider) is a change to core, not something an app adds.

What it stores

WhereWhat
users/{docId}email, emailVerified, phone (E.164) and phoneVerified, and by (how the account was made).
email_lookup/{sha256(email)}Whether an address is registered.
phone_index/{sha256(number)}Which record a number belongs to.
signup_otps, phone_otpsEmail and SMS codes, hashed, with their purpose (signup, reset, link).
auth_pins/{uid}PIN hashes (scrypt with a salt and a server secret kept in _system/pin_pepper), and the lock.
_rate_limitsCounters for the limits above, per caller, address and number.
account_transfersEvery email or number moved between accounts.
Settings/usersisSignupEnabled, defaultRole, phoneSignIn, googleSignIn, and copies of the SMS countries that Settings, SMS writes: phoneCountry (the default, such as IN), phoneCountryCode (its code, 91) and phoneCountries (the allowed ones).

Cloud Functions (callables, deployed as arccms-<name>):

FunctionDoes
checkEmailAccountIs this email registered, new, or a sign-in with no access to this site?
requestSignupOtp, verifySignupOtpThe email sign-up code; verifySignupOtp also checks reset and link codes.
createAccountRecordCreates the record after an email sign-up.
requestPasswordReset, resetPasswordForgot password: the reset code, then the new password (see Forgot password).
ensureGoogleAccountCreates the record for a first Google sign-in.
checkPhoneAccount, requestPhoneOtp, verifyPhoneOtp, completePhoneSignup, signInWithPin, resetPin, setPinPhone sign-in. Built only with the sms feature.
checkIdentifierForLink, requestEmailLinkOtp, linkEmail, linkPhoneAdding or moving an email or number on the profile page.
refreshMyClaimsPuts the Arc CMS claims back on the caller's token when they are missing.

The onUserCreated trigger fills email_lookup and tells Firebase Auth the email is verified when the sign-up code proved it, so a later Google sign-in keeps the password. See Cloud Functions.

Who can do what

Troubleshooting

SymptomCause and fix
"This way of signing in is not turned on for this site."The Firebase provider is off. Turn on Email/Password or Google in the Firebase console.
"Phone sign-in is not turned on for this site."Turn on Phone number under Settings, User Settings (it needs the sms feature).
"Google sign-in is not turned on for this site."Turn on Google under Settings, User Settings, as well as in the Firebase console.
"New accounts can't be created on this site right now."Sign-ups are off. Turn them on, or add the person under Users.
"This account doesn't have access to this site."The address has a Firebase sign-in but no Arc CMS record, such as another app's user in a shared project. An admin adds them under Users.
A number is refused as not allowed ("This site takes mobile numbers from India only.")Its country is not under Countries in Settings, SMS. Add it there.
"We couldn't load the sign-in options. Check your connection and try again."The page could not read Settings/users and this browser has no copy of it: the device is offline, or the read was refused. Check the connection; if it is fine, check that the rules let anyone read Settings/users. See The page's settings.
Forgot password says "We've emailed a link to reset your password", not a codeThe email engine sent nothing (email off, or no provider), so Firebase sent its own reset email, which never reaches Email Logs. Turn email on with a provider (see Forgot password).
"Reset your password in the app you signed up with."The address is a login of a host app sharing the Firebase project, which owns its password. The person resets it in that app (see Forgot password).
A new password is refused ("That password is one of the most used, so it is easy to guess.")It fails the password rule: too short, one character repeated, a straight run, a common password, or the person's own name or email (see Passwords). Choose another.
No SMS arrivesThe SMS provider is Test, which sends nothing. Look in SMS Logs, or switch the provider (see SMS).
The first sign-in after a quiet spell is slowThe functions were not running and had to start. The page starts them as it opens, so the wait is shortest when the person opens it before typing. A function added to a sign-in step belongs in SIGN_IN_FUNCTIONS in src/app/pages/(auth)/sign-in.service.ts, with if (isWarmUp(request)) return WARM; as the first line of its handler; a test checks both.
"Too many codes. Try again after 5:42 PM."The number or address had 5 codes in the last hour. Wait until that time, or use the last code if it has not expired.
"Too many tries. Reset your PIN with a code."The PIN is locked after 5 wrong tries. Use Forgot PIN.
"Sign-in isn't ready on this site yet. Please contact the site's team."The functions cannot sign the sign-in token: the Token Creator role or the IAM Service Account Credentials API is missing. Open Settings, User Settings: it shows the fix for this project. See Google Cloud setup for phone sign-in.
The Phone number switch will not turn onThe same setup is missing, and the page shows how to fix it. After fixing it, click Check again, then turn the switch on.