User Journey: Sign in

Table of Contents

This page documents a user journey: A person signs in, chooses the party they work as, and lands.

A user journey describes what one person is trying to do, before anything is built. It is a design document that outlives any sprint, so it names the screens, the entity fields each screen shows, and the operations and messages each step needs.

1. Goal

"I want to get in and start work." The person proves who they are, and the system puts them in the right tenant, at the right party. The journey is done when the workspace opens, the top bar names the party they work as, and no screen shows another tenant's data.

Every other journey runs inside the session this one creates. Signing out is the same journey from the other end: it ends the session and returns the person to the door.

2. Actor and trigger

Any person who holds an account: a member, a tenant administrator, or a super administrator. The trigger is opening the client, or being returned to the door by an expired session. It starts at the sign-in screen and ends when the workspace opens, or when the person signs out.

Three rules by the door:

  • The tenant is resolved before credentials are checked, because the account is looked up inside the tenant. The deployment knows its tenant from the address it is served at, and the server resolves it from the hostname at sign-in.
  • An account must work in at least one party. Zero parties is a misconfiguration and the sign-in is refused with a message that says to contact an administrator.
  • An account that works in more than one party chooses one here, and can change it later. The choice binds the session, not the account.

The very first sign-in of a new installation is not this journey: it is part of First run, where the deployment has no account to sign in with.

3. Steps

  1. Open the door. The person opens the client. The system shows the tenant it is serving, a username field and a password field. The client may first ask whether the deployment is provisioned; a deployment still in bootstrap mode has no account, so it sends the person to first run instead. The submit control stays disabled until both fields have content.
  2. Enter credentials. The person supplies a username and password. The system resolves the tenant from the hostname, finds the account inside that tenant, and verifies the password. Nothing else is typed: the tenant is not a field, because the address already states it.
  3. Wrong password. The system refuses with one sentence, Invalid username or password, and says no more. It counts the failed attempt. The counter is not returned to the client, so the screen cannot warn how many attempts remain. After five failed attempts the server locks the account.
  4. Locked account. A locked account is refused before the password is even checked, with Account is locked due to too many failed attempts. A correct password does not open it. A successful sign-in resets the failed-attempt counter, but only an administrator can clear the lock.
  5. The password must change. An account flagged password_reset_required must set a new password before it reaches any other screen. The account has been issued a password by somebody else and has never chosen one; the flag is set by a password reset and cleared when the member changes the password themselves.
  6. No party. The account works in no party at all. The system refuses the sign-in with Account has no party assignment. Please contact your administrator. This is the state that Sign up leaves behind.
  7. One party. The account works in exactly one party. The system selects it without asking and binds it to the session. The person never sees a picker.
  8. More than one party. The account works in several parties. The system returns the full list with each party's name, category and business centre, plus the account's stored default party when it is in the list. It does not bind a party yet, and the token it issues can do nothing except choose one. The screen names each party and marks the default, so the person recognises where they work rather than reading a UUID.
  9. Choose the party. The person picks a party and confirms. The system checks that the party is one of the account's associations, computes the visible party set and binds the party to the session. A party the account does not work in is refused.
  10. Land. The workspace opens. The session carries the account, the tenant and the party, and every later call is scoped by it. The person sees the party they chose in the top bar.
  11. Sign out. The person chooses Sign out. The system marks the login record offline, writes the end time on the session record, clears the token and returns the person to the door. The journey is over; a later sign-in is a new session, not a resumption of this one.

Steps 4, 5 and 6 end the journey without a session. Steps 7 and 8 both continue into step 9 or step 10. Nothing here waits on the server after a reply, so the journey holds no state of its own beyond the account and party rows it reads.

4. Screens and wireframes

One screen that changes shape. The credentials form, the party picker, the forced password change and the refusal message are panels of the same screen rather than separate screens, because the person is in one conversation with one door.

wireframe_sign_in.png

Figure 1: Sign in: the credentials form, the forced password change, the party picker, and the refusal messages.

The order of the branches is the server's order: the lock is checked before the password, the party count after it, and the password change before the workspace. The screen has to state the tenant, because the person cannot see which tenant the address resolved to until the top bar appears.

5. Entities composed

Entity What it contributes Model
account Username, email, account type and the stored default party ores.iam.account
account_party The parties the account works in — the set the picker offers and the rule the choice is validated against ores.iam.account_party
party Party name, category and business centre, so the picker reads as places ores.refdata.party
login_info Failed attempts, the lock, the last sign-in, and whether the password must change ores.iam.login_info
session The session this journey opens: who, when, from where, and when it ended ores.iam.session
tenant The isolation boundary the hostname resolves to, named on the screen ores.iam.tenant
account_type Whether the account may sign in this way at all: only user accounts may ores.iam.account_type

The password material — password_hash, password_salt and totp_secret — never leaves the server, and neither does the token the reply carries.

Two fields the reply exposes are read in the party step and the sign-in summary elsewhere: selected_party_id and available_parties. The session record itself does not carry the party, so the party a session worked as is not written down anywhere the audit can read it. That is gap 3 below.

6. Operations and messages

Step Operation Subject Status
Open the door Ask whether the deployment is provisioned iam.v1.bootstrap.status exists
Enter credentials Verify credentials, resolve the tenant, return the parties iam.v1.auth.login exists
Wrong password Count the failed attempt and lock at five carried by iam.v1.auth.login exists
Locked account Read the lock state and refuse before the password carried by iam.v1.auth.login exists
The password must change Signal that a change is required before anything else login_response.password_reset_required partial
More than one party Offer the parties the account works in iam.v1.auth.login exists
Choose the party Bind the chosen party to the session iam.v1.accounts.select-party exists
Land Keep the session alive while the workspace is used iam.v1.auth.refresh exists
Sign out End the session iam.v1.auth.logout exists

iam.v1.auth.login, iam.v1.auth.logout, iam.v1.auth.refresh and iam.v1.bootstrap.status come from ores.iam.login_messages; iam.v1.accounts.select-party comes from ores.iam.account_messages. All of them are present in the generated web protocol (wire-protocol/src/generated/iam/protocol/login_protocol.ts and its neighbours).

partial on password_reset_required is exact: the field is declared on login_response and carried over the wire, but the server never sets it during sign-in. It is set on the account when an administrator resets the password, and it is cleared when the member changes the password, but the login handler does not read it and put it in the reply. So the branch cannot fire today.

Lock, unlock, reset and change-password are separate administrative or self-service subjects — iam.v1.accounts.lock, iam.v1.accounts.unlock, iam.v1.accounts.reset-password and iam.v1.accounts.change-password — and belong to Rescue access and Protect my account. This journey only meets their after-effects; it never calls them.

7. What is missing

  1. No self-service password recovery from the door. There is no subject for a person who has forgotten a password to obtain a new one. Everything that exists either needs the current password (iam.v1.accounts.change-password) or an administrator (iam.v1.accounts.reset-password). No reset e-mail or reset-token path exists, so the candidate is a iam.v1.auth.request-password-reset subject plus a delivery mechanism outside IAM. Until it exists the screen offers no Forgot password control and the person must ask an administrator.
  2. The forced change never fires. login_response.password_reset_required is declared but never set, so an account whose password was reset signs in normally and keeps the password somebody else chose. The candidate is for the login handler to copy login_info.password_reset_required into the response and for the workspace to refuse to open until the member changes the password.
  3. The session does not record the party. The sign-in handler builds a session record but its party_id is left unset, and the sessions table has no column for it: party_id and visible_party_ids exist only on session_view in ores.iam.session_messages. So "who signed in, as which party" is not answerable from the session store, which is exactly the question a multi-party tenant has to answer. The candidate is a party_id column on the sessions table.
  4. No way out of a lock without an administrator. iam.v1.accounts.unlock is an administrative operation, so a locked member cannot help themselves and the screen can only tell them to ask. The candidate is a delay-based self-unlock, or a stated cooling-off period, so the lock does not become a support ticket every time.
  5. A refusal cannot say which refusal it is. Every sign-in failure returns login_response.success = false with a sentence in error_message. There is no code field, so the client distinguishes a locked account from a wrong password only by matching the text. The candidate is an error code on the response, as the sign-up path already has on the server side.
  6. The multi-party token expires silently. The token issued at step 8 can do nothing but choose a party, and it has its own short lifetime. The screen has nothing to show when it lapses, so a person who leaves the picker open is returned to the door with a generic failure rather than being asked to sign in again.
  7. The web door drops two reply fields. SignInPage.tsx shows the form and the picker, but not party_setup_required or party_setup_warning, so a member whose party wizard has not finished is not told. The candidate is a panel on the same screen that names the incomplete setup and links to First run.

8. Related journeys

  • Choose where I work — this journey's party step continues there: the same list, the default party, and switching party later without signing out.
  • Protect my account — where a signed-in member changes the password deliberately, and reads the sign-in state this journey just wrote.
  • Rescue access — where a member who cannot get in is unlocked and given a new password, which is where every refusal on this screen ends.
  • Sign up — the door for a person who has no account, when the tenant allows self-registration.
  • First run — where a deployment goes when it has no account to sign in with, and where a party setup left incomplete is finished.

9. Related knowledge

10. See also

Emacs 29.3 (Org mode 9.6.15)