Skip to content

RP subsystems

Multiple subsystems under a single RP. The RP is registered by an admin only; subsystems are auto-registered by trusting the RP (find-or-create). On the phone's consent screen the subsystem name is shown in LARGE text, with the parent RP name below in SMALL text.

Wire (the core idea)

No new field is added to the RP call — the relyingPartyName in the body is reused as the subsystem name. For a registered RP this field was previously overwritten by rp.Name and rendered insignificant (an impersonation block). Now:

POST /v3/auth/... { "relyingPartyUUID": "<RP UUID>", "relyingPartyName": "Хаан Банк Зээл" }

Server (newSession, store.go): 1. Resolves the RP from the secret (Bearer) (CreatorRP) — it does not trust the UUID in the body. 2. Resolves relyingPartyName as a subsystem within that RP via resolveSubsystem (find-or-create). Key = lower(trim(name)) (name_key), unique per RP. 3. In the session: RPName = rp.Name (authoritative, small text), SubsystemName (large text), SubsystemID.

If relyingPartyName is empty or sent as the same name as the RP → the default subsystem (same name as the RP). Legacy RPs (e.g. the web demo "Demo Bank") send their own name, so they map directly to the default — backward-compatible.

First-party (operator) / dev ("") callers have no subsystem (RP not registered) — SubsystemName is empty and the phone shows only RPName on its own.

Registration and protection

  • When an RP is registered, a default subsystem with the same name as itself is always created (Register).
  • Cap: each RP may have SMARTID_RP_SUBSYSTEM_MAX (default 50) subsystems. If exceeded, new auto-creation is refused (ErrSubsystemCap → HTTP 429). Existing subsystems keep working.
  • The admin can only edit subsystems: rename / merge / deactivate. No manual creation (the RP auto-creates them).

Counters

On the subsystem row, auth_count / sign_count / last_used_at are incremented atomically each time a session ends successfully (OK) (recordSessionActivity, crypto_wired.go). The RP's total = SUM of its subsystems. The admin dashboard shows "Subsystems (active/total)" + per-subsystem counters within the RP list.

Endpoints (admin, /v3/admin/*)

Method Path Description
GET relying-parties/{id}/subsystems RP's subsystems + counters (rp:read)
PATCH relying-parties/{id}/subsystems/{sid} rename (rp:write)
POST relying-parties/{id}/subsystems/{sid}/deactivate | /activate active state
POST relying-parties/{id}/subsystems/{sid}/merge {targetId} merge src→dst counters

Files

  • DB: server/migrations/V30__rp_subsystems.sql (+ default backfill for legacy RPs).
  • Model/repo: persistence/models.go (Subsystem, SubsystemID/Name on session), repo/repo.go (Subsystems), repo/inmem, repo/pg, postgres/subsystem_repo.go.
  • Logic: service/memory/store.go (resolveSubsystem, admin ops), crypto_wired.go (counters).
  • Phone: iOS SessionInfo.subsystemName + ContentView/RequestView; Android SessionInfo + RequestView.kt — subsystem LARGE, RP below in SMALL (if the names are the same, only the RP).
  • Admin: admin/src/app/(app)/rps/page.tsx (subsystem expansion + management), page.tsx (dashboard).

Security note

The subsystem name is free-text supplied by the RP (an RP could give a name like "Google"). Protections: (a) the subsystem is scoped to the RP; (b) the authoritative RP name is always shown below in SMALL text; (c) cap + admin merge/rename/deactivate. This is consistent with the "trust the RP" model.

Push notifications display the subsystem name as the primary one (<subsystem> (<RP>) — when the name differs from the RP); if empty, the RP. apns.go body + payload includes subsystemName. When the phone opens, from sessionInfo the consent screen confirms the subsystem in LARGE and the RP below in SMALL.