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.
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:
- register the preferred username;
- on conflict, sign in with the canonical secret — this is another device of ours;
- 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; - 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
- Restore validates the home session token and migrates the home credential.
- Active foreign endpoints call
ensureProvisioned. - Provisioning resolves the canonical secret from the home server, then follows the plan above.
- Successful provisioning stores the foreign actor credential; room connection then identifies and joins with that actor id.
- 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.