Files
Toju/toju-app/src/app/domains/authentication/README.md
T
myxelium f9e8538c80 feat(auth): recover cross-signal authorization with provision secrets
A client that could not authorize against a foreign signal server was
redirected into a dead end with no way to retry, so servers joined from
another signal route became unreachable.

The home server now stores a per-user provision secret, clients keep it in
their own store, and a recovery service records why authorization failed per
server URL. Invite, server browser, and chat room surface that reason and
offer a retry instead of silently redirecting.
2026-08-14 03:19:29 +02:00

6.2 KiB

Authentication Domain

Handles the durable home session plus per-signal-server credentials used for cross-server identity. Provides login, registration, silent foreign provisioning, contextual recovery, and user-bar UI.

Module map

authentication/
├── application/
│   └── services/
│       ├── authentication.service.ts              HTTP login/register against the active endpoint
│       ├── signal-server-auth.service.ts          Home migration and silent foreign provisioning
│       ├── signal-server-authorize.service.ts     Explicit authorization and credential checks
│       ├── signal-server-auth-recovery.service.ts Contextual per-server recovery state
│       └── home-provision-secret.service.ts       Account-wide secret issued by the home server
│
├── domain/
│   └── models/
│       └── authentication.model.ts      LoginResponse interface
│
├── feature/
│   ├── login/                       Login form (`<form ngSubmit>`; autofocus + select-on-focus via shared directives)
│   ├── register/                    Registration form (same form-field UX as login)
│   └── user-bar/                    Displays current user or login/register links
│
└── index.ts                         Barrel exports

Service overview

AuthenticationService resolves the API base URL from ServerDirectoryFacade, then makes POST requests for login and registration. It does not hold session state itself; after a successful login the calling component dispatches UsersActions.authenticateUser, and the users effects prepare the local persistence boundary before exposing the new user in the NgRx store.

SignalServerAuthService keeps one credential per normalized signal-server URL. Provision failures never expire the valid home session or automatically redirect to generic login. They publish a contextual issue rendered on server/join surfaces with Retry.

graph TD
    Login[LoginComponent]
    Register[RegisterComponent]
    UserBar[UserBarComponent]
    Auth[AuthenticationService]
    SD[ServerDirectoryFacade]
    Store[NgRx Store]

    Login --> Auth
    Register --> Auth
    UserBar --> Store
    Auth --> SD
    Login --> Store

    click Auth "application/services/authentication.service.ts" "HTTP login/register" _blank
    click Login "feature/login/" "Login form" _blank
    click Register "feature/register/" "Registration form" _blank
    click UserBar "feature/user-bar/" "Current user display" _blank
    click SD "../server-directory/application/server-directory.facade.ts" "Resolves API URL" _blank

Login flow

sequenceDiagram
    participant User
    participant Login as LoginComponent
    participant Auth as AuthenticationService
    participant SD as ServerDirectoryFacade
    participant API as Server API
    participant Store as NgRx Store
    participant Effects as UsersEffects

    User->>Login: Submit credentials
    Login->>Auth: login(username, password)
    Auth->>SD: getApiBaseUrl()
    SD-->>Auth: https://server/api
    Auth->>API: POST /api/users/login
    API-->>Auth: { userId, displayName }
    Auth-->>Login: success
    Login->>Store: UsersActions.authenticateUser
    Store->>Effects: prepare persisted user scope
    Effects->>Store: reset stale room/user/message state
    Effects->>Store: UsersActions.setCurrentUser

Registration flow

Registration follows the same pattern but posts to /api/users/register with an additional displayName field. On success the user is treated as logged in and the same authenticated-user transition runs, switching the browser persistence layer to that user's local scope before the app reloads rooms and user state.

One human, one account per signal server

A linked account on a foreign signal server is an ordinary account whose password is the user's provision secret. That secret is issued and stored by the home signal server (GET /api/users/me/provision-secret, created on first use), so it is identical on every device the person signs in from. HomeProvisionSecretService fetches it with the home session token and caches it in memory only.

This matters because the secret decides identity. Older builds generated a secret per device; the second device could not sign in to the account the first one had created, fell through to the username-<shortHomeId> candidate, and registered a second account with the same display name. Everyone else then saw that person twice, DM threads forked, and a 1:1 call looked like a group call.

buildProvisionPlan therefore orders attempts so a duplicate cannot happen by accident:

  1. register the preferred username;
  2. on conflict, sign in with the canonical secret — this is another device of ours;
  3. then sign in with the legacy device-local secret — an account this device made before canonical secrets existed, which is immediately rotated onto the canonical secret via POST /api/users/me/password;
  4. only once the preferred name is proven to belong to somebody else, repeat for username-<shortHomeId>.

Registering the suffixed name requires a canonical secret. Without one the client cannot distinguish "another human owns this name" from "our own account whose secret this device never had", so it raises a contextual recovery issue instead of guessing.

Restore and foreign-server recovery

  1. Restore validates the home session token and migrates the home credential.
  2. Active foreign endpoints call ensureProvisioned.
  3. Provisioning resolves the canonical secret from the home server, then follows the plan above.
  4. Successful provisioning stores the foreign actor credential; room connection then identifies and joins with that actor id.
  5. Rejected credentials or an unavailable endpoint publish per-server recovery state. The home session remains active; Retry re-runs provisioning and reconnects the current room after success.

Diagnostics record only home user id, foreign actor id, normalized server URL, and outcome. Tokens, passwords, provision secrets, SDP, and message contents must never be logged.

User bar

UserBarComponent reads the current user from the NgRx store. When logged in it shows the user's display name; when not logged in it shows links to the login and register views.