| M1-36/1 |
2026-10-13 |
The ticket was built on an integration base and ported onto main as its own pull request; text this ticket adds is English. |
Instructions of the launcher, which override the brief's merge step (as M1-35/1). |
| M1-36/2 |
2026-10-13 |
The app extension point per credential kind is LoginMethodSource (Contribute, Remaining), not CredentialSource: app.CredentialSource already implements authz.CredentialSource (M1-33). The sources of identities and active devices are always present; M1-39 and M1-40 add theirs through IdentitiesDeps.Sources. |
Two types of one name in one package do not compile; renaming the authz source would touch M1-33 and M1-34 code outside the scope. |
| M1-36/3 |
2026-10-13 |
Device pairing counts as a remaining sign-in method for identity.unlink only for an active device whose platform is not cli (08 section 3.3, section 5.3). |
A pairing is approved with step-up on the active device (08 section 5.3), and a cli device has no step-up (ID-T29); counting it would let a person unlink the last way in. |
| M1-36/4 |
2026-10-13 |
A provider name is trimmed, not empty, passes the display-text check (SEC-IN-4) and has at most principal.display_name_max_chars characters; no new configuration key. |
The brief names no length; an unbounded display text is refused everywhere else, and the display-name limit is the named key for display texts. |
| M1-36/5 |
2026-10-13 |
The schemas of 08 section 2.6 are enforced with these refinements, written into 2.6: an issuer with a path segment common, organizations or consumers is refused (use {tenantid} with allowed_tenant_ids); {tenantid} appears at most once and only in the path; allowed_tenant_ids is refused without the placeholder, each id is ^[A-Za-z0-9][A-Za-z0-9._-]*$ and unique; client_key_alg, client_key_id and client_cert_x5t_s256 belong to private_key_jwt only, client_key_alg is normalized to PS256 when absent, the thumbprint is 32 bytes as base64url without padding; app_redirect_uris entries are https with a host or a private-use scheme in reverse domain form, without user information or fragment, unique; header names are stored in canonical form and name_header differs from login_header; secret references must parse with platform/secrets (file: or env:), azkv: and awssm: are refused while they have no driver. |
Brief test list ("/organizations/ issuer", inline secret, invalid reference) and fail closed: a configuration that can never sign in, or that names a tenant check that never runs, is refused at write time. |
| M1-36/6 |
2026-10-13 |
deleteIdentityProvider answers 409 conflict with reason provider_in_use and hint "disable the provider instead" while an identity (also an unlinked one) or a claim group references the provider; the restrict violation (SQLSTATE 23001) is mapped without the constraint name. |
03 groups.provider_id is ON DELETE RESTRICT as well, so a provider with claim groups cannot be deleted either; one answer for both keeps the hint true. |
| M1-36/7 |
2026-10-13 |
Providers.Enabled (and Identities.Resolve through it) takes a share lock on the provider row inside a unit of work, and Resolve a share lock on the identity row; disabling and unlinking take the row lock before they read the live sessions. |
A login that resolved an identity commits its session before a concurrent disable or unlink reads the sessions it must end, and a login after the disable sees the provider disabled; without the locks a session could start through a path that was just closed (R-ID-12). |
| M1-36/8 |
2026-10-13 |
identity.read and identity.unlink answer 404 not_found for another person when the caller is no owner (authz reason not_self), the same as for an unknown id. |
Brief "Credential overview" and "Unlink" (others 404), as listUserSessions does (M1-34). |
| M1-36/9 |
2026-10-13 |
Link stores email_at_link only when EmailHint is one plain address (domain.CheckEmail), issuer only for oidc (required there); the subject must be non-empty valid UTF-8 without NUL. |
The schema Identity.email_at_link has format: email; a hint of another form would break the response. Issuer and subject rules follow 03 user_identities. |
| M1-36/10 |
2026-10-13 |
An unlink of an identity that is already unlinked answers 204 and records the allow with no_change; a provider update without effect records no_change and keeps updated_at. Audit details: identity.unlink names identity_id, provider_id, provider_kind and sessions_revoked; identity_provider.manage names the provider as resource, provider_kind, changed_fields and on a disable sessions_revoked, never configuration values or the name. |
06 section 7 (DELETE) and section 2 "No effect" as in M1-31; SEC-LOG-4 keeps values out of the audit. |
| M1-36/11 |
2026-10-13 |
Resolve compares the issuer only for oidc (08 section 3.1 step 1) and answers unmatched for an empty subject; it never reads EmailHint, DisplayName, users.email or email_at_link (ID-T04). |
R-ID-02. |
| M1-36/12 |
2026-10-13 |
The migration 00013_identity_providers.sql adds the deferred foreign keys auth_sessions.identity_id (M1-34/10) and groups.provider_id (M1-27); the schema tests of those migrations now expect them, and the claim group fixture of authz/app TestGroups inserts its provider first. bothy_app gets SELECT, INSERT, UPDATE, DELETE on identity_providers and SELECT, INSERT, UPDATE on user_identities. |
The schema tests hold the latest schema; a provider without identities is deleted for good (08 section 3.3), an identity ends by unlinked_at. |
| M1-36/13 |
2026-10-13 |
identity.unlink locks only the person's active identities (08 section 3.3), not the methods of other kinds it counts. Every later writer that removes a counted method (device revocation and expiry, M1-45; passkey revocation, M1-40; removal of local credentials, M1-39) locks the person's active identities in ascending id before it changes that method, so an unlink cannot pass on a count that a concurrent removal has already made stale. The removal itself stays allowed to take the last method: a lost device must always be revocable (08 section 10), only identity.unlink refuses with no_login_method_left. |
Red team M1-36 (Minor 2 and its re-review): none of these writers exists on this base, so the race cannot occur yet; the rule belongs to the tickets that add them, and it orders the two writers without adding a last-method check to revocation. |
| M1-36/14 |
2026-10-13 |
Secret references in provider configurations are checked for syntax only (brief, 19 section 10); any file: or env: reference is accepted. Before the OIDC driver (M1-41) resolves a reference it must confine provider references to a place no server key uses: a subdirectory of <secrets.dir> reserved for identity providers or an env: prefix reserved for them (not BOTHY_SECRET_), and refuse a reference equal to any configured server *_ref value. Otherwise an owner could send the token pepper or another server secret to an issuer as client secret. |
Red team M1-36 (Minor 3 and its re-review): nothing is resolved before M1-41, so the risk starts there; the server's own secrets live below <secrets.dir> and use BOTHY_SECRET_ (docs/operations/configuration.md), so a plain directory or prefix rule would not protect them. |