Skip to content

plugin-auth SCIM: re-activating a deactivated user (active: true) brings back the API keys and local password from before the deactivation #32115

Open

Description

@os-bob

Edition / version

ObjectOS Enterprise, cloud main de8d979 (framework pin e24136c2, @better-auth/scim 1.7.12), licensed. Local SCIM run for objectstack-ai/cloud#3667, sending Okta's request shapes.

Steps (measured)

  1. A SCIM user has a local password (admin Set Password in the UI) and two API keys created in the UI (Account → Developer → API Keys).
  2. Okta deactivates the user: PATCH /Users/{id} {"op":"replace","value":{"active":false}}. Everything is refused within ~0.2 s, as designed:
    • session 401;
    • API key data door 401;
    • API key MCP door 401;
    • password sign-in 403 BANNED_USER;
    • the group grant is removed.
  3. The key rows stay with revoked = false. They are refused only because sys_user.banned = true.
  4. Okta re-activates the user: PATCH ... {"active":true}. reconcileUser lifts the SCIM ban (by design, auth-manager.ts identity.reconcileUser). Results:
    • the old API key works again on both the data door and the MCP door (200);
    • the old local password signs in (200);
    • the group grant comes back too (expected, since membership is the IdP's).

Why it matters

A re-activation in Okta or Entra is often a rehire months later, or the undo of an accidental deactivation. In both cases, credentials minted before offboarding should not quietly come back to life. A key that sat on a departed employee's laptop, or in a script they left behind, works again without anyone deciding that it should. The admin Set Password dialog already treats this as a deliberate act: "Sign Out Everywhere and Revoke API Keys … Revoked keys stay revoked after the password change".

Proposal (needs a maintainer call)

When SCIM active:false (or DELETE) disables an account, also revoke the user's API keys, the same write the Set Password dialog uses, with revoked_reason naming the SCIM deactivation. The rows stay, so "the key row stays" (ADR-0134 / the leaver e2e) still holds. Optionally, also drop the local credential password of a directory-managed user. Re-activation then restores access through SSO only, and the person mints new keys.

If keeping the current behaviour is intended, it should be written down in ADR-0134 and in cloud's docs/enterprise/scim-okta.md / scim-entra-id.md, which today describe only the deactivation half.

Activity

  1. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 10, 2026
  2. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: security · regraded p2 → p1 · handed to the security lane · 2026-11-10T08:53Z

    ⛔ Not a claim.

    • Per lanes/triage.md, triage sets only security and p1 on a public security card, and judges nothing else. The security lane owns the rest. No mechanism is discussed here.

    Generated by Claude Code

  3. added
    priority:p1High: required for production / M2
    and removed on Nov 10, 2026
  4. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim: PM loop round 1
    Session: session_01L9qibyho66BmvQ2CZsxoWJ (card thread), dispatched by session_01L9qibwMmGJKaQ52CgHqNu2
    Account: os-tesla
    Branch: claude/project-thread-9vbm8g
    Worktree: objectstack-issue-32115
    Domain: domain:server (the security lane holds the card; the fix lands in packages/plugins/plugin-auth)
    Seat: security#2
    File surface: packages/plugins/plugin-auth/src/** (the directory reconcile path, the key-revocation writer, the key restore guard, their tests), root .changeset/
    Container & model: M, mode:thread, model: default tier, effort: xhigh (security card, fleet rule 2)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: platform code in objectstack (plugin-auth's directory reconcile path) | none covers it today | every deployment that provisions users through a directory
    Thread-read: 6763167595
    Premise: holds (the ruling of record, comment 6787890900 on #32004, item "Directory deactivation", is the contract; this PR is Part of #32115, the local-password half is the later step)
    Serial constraints cleared: none (PR #32966 touches runtime only; #32386 touches only manifests and the lockfile)
    Split provenance: security#1 handed this card to security#2 on mailbox PR #33040, comment 6845323973.

  5. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim 6845390227 amended · 2026-11-13T05:57Z — file surface widened on the dev's measurement (a new key-revocation reason word is a closed select option outside plugin-auth):

    File surface: packages/plugins/plugin-auth/src/**, packages/platform-objects/src/** (sys-api-key.object.ts and its generated i18n bundles), packages/core/src/security/api-key.ts and their tests, root .changeset/
    Clause-②: yes (widening)
    Metadata-protocol: none

  6. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibyho66BmvQ2CZsxoWJ
    os-dev-report

    {
      "issue": 32115,
      "status": "done",
      "branch": "claude/project-thread-9vbm8g",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33075",
      "session": "session_01L9qibyho66BmvQ2CZsxoWJ",
      "premise_still_valid": true,
      "summary": "A SCIM deactivation (active: false) and a SCIM delete now revoke every API key the user owns, stamped revoked_reason scim_deactivated (new plugin-auth scim-deactivation-key-revocation.ts, called from AuthManager#reconcileScimUserLifecycle's inactive branch after the directory's ban; it never throws, and an incomplete run logs one error line with the remedy while the deactivation still lands). No one restores such a key from a user context: the restore guard's rule 1 and the AI service account restore-key door refuse it for the owner and a platform administrator, and core's API_KEY_REVOKED text names only the new-key road. sys_api_key.revoked_reason gains the option scim_deactivated in all 11 locales. Administrator ban/unban is unchanged; the local-password half of the ruling is the remainder (PR says Part of #32115). Surface widened to platform-objects and core per the seat's decision A in the seat thread, recorded in the PR's Acceptance notes.",
      "tests": "head fc310c98. plugin-auth: pnpm --filter @objectstack/plugin-auth test on c69a8ef8 -> Test Files 266 passed, Tests 5025 passed | 10 skipped; on fc310c98 the touched and neighbouring files (service-account, auth-route, api-key-restore, scim-deactivation, forced-reset-revoke-reason) -> Test Files 9 passed, Tests 175 passed. core: pnpm --filter @objectstack/core test -> 146 files, 3369 passed. platform-objects: pnpm --filter @objectstack/platform-objects test -> 121 files, 1955 passed. typecheck (incl. test typecheck) exit 0 for all three; pnpm lint exit 0. Ablation 1 (scripts/ablation-replace.mjs, on the commit that added it): the revocation call in auth-manager.ts replaced by a complete no-op outcome -> the 4 deactivation pins in scim-deactivation-reconcile-user.test.ts red, the administrator ban/unban CONTROL green; restored, blob == HEAD, git diff HEAD empty. Ablation 2 (on 30aba24d): ' || storedReason === SCIM_DEACTIVATION_REVOKE_REASON) {' removed from service-account-lifecycle.ts, anchor 1->0, blob 25545604b8b2->a495de8526e2 -> the new restore-key pin red, Tests 1 failed | 28 passed; restored, blob == HEAD, git diff HEAD empty. Subjects imported from source.",
      "gates": "dispatch-gates --commands on fc310c98 derived 109 families; all 109 run with </dev/null, every exit 0, codes on disk before reconciling; dispatch-gates --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. Before that, check:dts-closure and check:dual-build-cjs-loads needed a full declaration build (env -u OS_SKIP_DTS pnpm build under os-verify-lock, 119 tasks) and then passed (311/311 declarations). Earlier reds fixed on the branch: check:tenant-audit-census (new write site, regenerated with tenant-audit-census.mjs --write plus prose counts, regenerated again after the merge of origin/main 64c426ab: 310 sites) and check:query-options-erasure (test casts replaced with Parameters<> spreads; 236 at the ceiling). check-i18n-bundles --filter=platform-objects in sync. origin/main merged once before opening (c69a8ef8). PR check-runs: not yet reported at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "7 — mcp__github__issue_read x3, mcp__github__list_pull_requests x1, mcp__github__pull_request_read x3; 0 writes (the harness's assignee/reviewer step skipped per the frame; the only assignee write is assignees: [os-tesla] on the pr_create)",
      "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + its assignees leg, run 43392641043, read-back identical) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/32115/comments); plus git push of the branch",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }
  7. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    ACCEPT → PR #33075 comment 6848849170 · 2026-11-13T09:12Z · head be3b8623, ready and auto-merge armed. The PR is Part of #32115: it lands the API-key half of the ruling. The card stays open for the local-password half, which is the later step.

  8. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Remainder · 2026-11-13T09:43Z — PR #33075 merged c4196099: a directory deactivation or delete now revokes the user's API keys, and no one restores them. Still open on this card: the local-password half of the ruling (the later step). The seat keeps the claim and orders it when the 18.4.0 freeze lifts.

  9. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    Claim 6845390227 amended · 2026-11-13T12:44Z — step 2 under the same claim, by the maintainer's choice in the seat thread (2026-11-13): a directory deactivation or delete also suspends the AI service accounts the deactivated person is accountable for, as an owner leaving the organization already does; a re-activation does not resume them. The local-password half stays a later step.

    File surface: packages/plugins/plugin-auth/src/** and its tests, content/docs/** (rule-level lines), root .changeset/
    Clause-②: no
    Metadata-protocol: none

  10. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibyho66BmvQ2CZsxoWJ
    os-dev-report

    {
      "issue": 32115,
      "status": "done",
      "branch": "claude/project-thread-9vbm8g",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33109",
      "session": "session_01L9qibyho66BmvQ2CZsxoWJ",
      "premise_still_valid": true,
      "summary": "Step 2. A SCIM deactivation (active: false) or delete now suspends every AI service account whose accountable owner is that person, in every organization, with the ban reason 'Accountable owner deactivated by the directory'. The owner-left writer in service-account-owner-leaves.ts is now one shared function (owner-left keeps its scope and log lines; the new suspendAccountsOfDirectoryDeactivatedOwner calls it with the new reason), called from reconcileScimUserLifecycle's inactive branch after step 1's key revocation; it never throws, and an incomplete pass logs one error with the remedy while the deprovision lands. Nothing is revoked; a re-activation resumes none; the new reason is a family suspension reason (an organization administrator may lift it), and resume answers 409 RESOURCE_CONFLICT while the owner's ban in force carries the directory's deactivation reason. Rule-level docs lines in permissions/authentication.mdx, ai/agents.mdx and permissions/index.mdx; changeset plugin-auth patch, Clause-② no, Metadata-protocol none. Remainders named by the order: an administrator's own ban of the owner (unchanged) and the local-password half.",
      "tests": "head 913efacd: pnpm --filter @objectstack/plugin-auth test under os-verify-lock -> Test Files 266 passed, Tests 5037 passed | 10 skipped. New pins: scim-deactivation-reconcile-user.test.ts describe 'a directory deactivation suspends the AI service accounts the person is accountable for, and a re-activation resumes none' (5 cases, through @better-auth/scim); service-account-lifecycle.test.ts describe 'a directory deactivation of the owner suspends the accounts, and resume waits for the owner' (6 cases). After the merge (8f94ed83): service-account-lifecycle + scim-deactivation-reconcile-user + auth-plugin.test.ts -> 3 files, 193 tests passed. typecheck (incl. test typecheck) exit 0; ESLint on changed .ts files exit 0. Ablations on 913efacd via scripts/ablation-replace.mjs, each restored (blob == HEAD, git diff HEAD empty): (1) the suspension call in auth-manager.ts replaced by an empty outcome, blob 23b9099877af->1a0ce1c29ccc -> 3 of 5 (j) pins red, the kept-reason case and the admin-ban CONTROL green; (2) the resume refusal's condition forced false, blob 0ac37996b51d->3f856b75539c -> 2 red (unit refusal + end-to-end); (3) the new reason dropped from isServiceAccountSuspensionReason, blob 0ac37996b51d->3a1e990423b4 -> 3 red. Subjects imported from source.",
      "gates": "dispatch-gates --commands on 913efacd derived 106 families; all run with </dev/null, codes on disk; spec check:docs (1) and spec check:skill-examples (3) first read spec artifacts older than main's spec src, so env -u OS_SKIP_DTS pnpm build ran under os-verify-lock (119 tasks) and those two plus check:dts-closure and check:dual-build-cjs-loads were rerun: all exit 0. --ran reconciliation: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN. origin/main (64551047, touching plugin-auth auth-plugin.ts and service-automation only) merged once before opening; after it the 33 node-script gates plus check:system-context-census, check:platform-object-tenancy-census and check:changeset-gate-self-tests rerun: 36/36 exit 0. The branch was restarted from origin/main (the step-1 remote branch had been deleted on merge, so the push was a plain new-branch push, no force). PR check-runs: not yet reported at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "1 — mcp__github__issue_read (get_comments on #32115); 0 writes",
      "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + its assignees leg, run 43437839230, read-back identical) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/32115/comments); plus git push of the branch",
      "open_questions": [],
      "security_findings": "withheld",
      "out_of_scope_findings": []
    }
  11. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2

    ACCEPT → PR #33109 comment 6856176450 · 2026-11-13T14:41Z · head 3c1d57b8, ready and auto-merge armed. Step 2 of this card: the AI service accounts of a person the directory deactivates are suspended. The card stays open for the local-password half.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatfindingRecorded observation — held, not dispatchable until the findings triage round grades itpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions