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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Authentication Domain
|
||||
|
||||
Handles user authentication (login and registration) against the configured server endpoint. Provides the login, register, and user-bar UI components.
|
||||
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
|
||||
|
||||
@@ -8,7 +8,11 @@ Handles user authentication (login and registration) against the configured serv
|
||||
authentication/
|
||||
├── application/
|
||||
│ └── services/
|
||||
│ └── authentication.service.ts HTTP login/register against the active server endpoint
|
||||
│ ├── 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/
|
||||
@@ -26,6 +30,8 @@ authentication/
|
||||
|
||||
`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.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Login[LoginComponent]
|
||||
@@ -64,7 +70,7 @@ sequenceDiagram
|
||||
Login->>Auth: login(username, password)
|
||||
Auth->>SD: getApiBaseUrl()
|
||||
SD-->>Auth: https://server/api
|
||||
Auth->>API: POST /api/auth/login
|
||||
Auth->>API: POST /api/users/login
|
||||
API-->>Auth: { userId, displayName }
|
||||
Auth-->>Login: success
|
||||
Login->>Store: UsersActions.authenticateUser
|
||||
@@ -75,7 +81,32 @@ sequenceDiagram
|
||||
|
||||
## Registration flow
|
||||
|
||||
Registration follows the same pattern but posts to `/api/auth/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.
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user