Skip to content

M1-36 Identity provider port and provider management

No Date Ruling Reason
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.