Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 13 – Enterprise SSO and provisioning (SAML 2.0 and SCIM 2.0)

Deprovisioning that ends access everywhere

Make deactivation end every session, token, key and grant at once, find leavers the provider never reports, recompute roles for movers and stop reused names.

  • Intermediate
  • 40 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free intro, Pro: 7 of 40 minutes free
Free: One switch has to end every credential at once. With a pass: the other 7 sections and 4 interview answers, about 33 minutes.

What you will learn

  • Model the joiner, mover and leaver events of one account across the provider's requests, federated sign-ins, sessions, tokens, keys and jobs
  • Design deactivation so that one per-user write ends sessions, access and refresh tokens, API keys, grants, jobs and invitations at once
  • Choose between just-in-time provisioning, SCIM, events and reconciliation, and detect leavers that a provider never reports
  • Apply group changes to roles and open sessions at once, and remove the grants that no longer match when someone moves team
  • Prevent account resurrection when a name is reused or a person returns, and keep an audit trail without personal data or secrets

Before you start

What you’ll learn · Before you start

What you will learn

  • Model the joiner, mover and leaver events of one account across the provider's requests, federated sign-ins, sessions, tokens, keys and jobs
  • Design deactivation so that one per-user write ends sessions, access and refresh tokens, API keys, grants, jobs and invitations at once
  • Choose between just-in-time provisioning, SCIM, events and reconciliation, and detect leavers that a provider never reports
  • Apply group changes to roles and open sessions at once, and remove the grants that no longer match when someone moves team
  • Prevent account resurrection when a name is reused or a person returns, and keep an audit trail without personal data or secrets

Before you start

On this page

Marking a person inactive in your application stops one thing: a new password sign-in. In this lesson’s first program, nine credentials that the person already held kept working afterwards: a session, an access token, a refresh token, an API key, a grant to another app, a nightly job, an open connection, a sign-in link and an invitation. A list of revocations that a team wrote still left 4 of the 9, and a counter on the user that every check reads left none.

You will make deactivation end every credential at once, learn about leavers when the provider reports nothing, change roles when someone moves, and stop a reused name from inheriting an old account.

One switch has to end every credential at once

deprovisioning: Ending a person's access in an application when they leave or lose the right to it: marking the account inactive, ending its sessions, tokens and keys, and removing what it granted. is the work of ending a person’s access when they leave or lose the right to it, and it is one event in the life of an account: a joiner, mover and leaver: The three life-cycle events of an account. A joiner arrives, a mover changes team or role, and a leaver goes. Each one needs a change in every application the person uses. arrives, a mover changes team and a leaver goes. A SCIM request sets active to false, and RFC 7643 says that the service provider decides what that means. Here it means that nothing the person was given works any more. Step through one deactivation:

Interactive · message by message One deactivation, the switch first and the clean-up after Step through the deactivation of one person. See the single write that ends access, who reads it, and what the clean-up can do later.
One deactivation, the switch first and the clean-up after, step 6 of 6. Messages so far: Identity provider to User record: active is false; Every check to User record: still epoch 7?; User record to Every check: no, it is 8; User record to Every check: delete the rows.Identity providerthe SCIM clientUser recordinactive, epoch 8Every checksession to jobactive is falsestill epoch 7?no, it is 8delete the rowsone line in the audit trail

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The provider says inactive. The identity provider sends one SCIM request that sets active to false for one person. The service takes the tenant from the connection's credential, as the last lesson did, and finds the person inside that tenant.
  2. One write is the switch. In one transaction the service marks the person inactive and raises the epoch from 7 to 8. Nothing else has to succeed for access to end, so a failure later cannot leave a credential working.
  3. Every check reads the switch. A session, an access token, a refresh token, an API key, a grant, a nightly job, an open connection, a sign-in link and an invitation each carry the epoch they were issued under. Each is compared with the person's current one when it is used.
  4. Old credentials stop at once. All nine carry 7 and the person is at 8, so all nine are refused, whether or not anything has been deleted yet. A self-contained token costs one small read of the person's record on each use.
  5. The clean-up follows. A worker deletes what names the person: session rows, refresh tokens, keys, grants, jobs and open connections. It can fail and be run again, because the switch has already done the work that cannot wait.
  6. One audit line. The service writes one line to its audit trail, so that the change can be traced later.

The program deactivates one person under three designs, and then tries a new password sign-in and the nine credentials she held:

Nine credentials after a deactivation, under three designs, and a clean-up that fails JavaScript · offboarding/offboarding.mjs
// Deactivating a person has to end every way their account can still act. Nine credentials, issued before the person was
// deactivated, are tried afterwards under three designs: a flag on the user, a list of revocations that a team wrote, and an
// epoch that every check reads before it believes a credential. A tenth row tries a new password sign-in, the one test that
// teams usually run. The second block makes the clean-up fail halfway.

const NOW = 5000; // minutes on a made-up clock

const KINDS = [ // [what it is, minutes it lasts, how long ago it was issued]
  ['browser session', 480, 120],
  ['access token, self-contained', 15, 5],
  ['refresh token', 43200, 600],
  ['personal API key', Infinity, 20000],
  ['OAuth grant held by another app', Infinity, 9000],
  ['nightly export run as the person', Infinity, 15000],
  ['realtime connection opened earlier', Infinity, 90],
  ['sign-in link e-mailed earlier', 15, 3],
  ['invitation sent by the person', 10080, 2000],
];

function world() {
  const user = { id: 'u-asha', active: true, epoch: 7 };
  const creds = KINDS.map(([kind, lasts, ago]) => ({ kind, epoch: user.epoch, expires: NOW - ago + lasts, revoked: false }));
  return { user, creds };
}

// Each design answers one question: may this credential act now? And it has a deactivate() that may stop halfway.
const REVOKED_BY_THE_LIST = ['browser session', 'refresh token', 'personal API key', 'OAuth grant held by another app', 'nightly export run as the person'];

function cleanup(db, steps, failAt) {
  steps.forEach((step, i) => {
    if (i === failAt) throw new Error(`step ${i + 1} failed: ${step.kind}`);
    step.revoked = true;
  });
}

const DESIGNS = {
  'flag only': {
    deactivate(db) { db.user.active = false; },
    mayAct: (db, c) => !c.revoked && NOW < c.expires, // the credential's own life, and nothing about the user
  },
  'revocation list': {
    deactivate(db, failAt) {
      db.user.active = false;
      cleanup(db, db.creds.filter((c) => REVOKED_BY_THE_LIST.includes(c.kind)), failAt);
    },
    mayAct: (db, c) => !c.revoked && NOW < c.expires,
  },
  'epoch and clean-up': {
    deactivate(db, failAt) {
      if (db.user.active) { // a retry runs only the clean-up again
        db.user.active = false;
        db.user.epoch += 1; // the switch: one write, before anything that can fail
      }
      cleanup(db, db.creds, failAt);
    },
    mayAct: (db, c) => db.user.active && c.epoch === db.user.epoch && !c.revoked && NOW < c.expires, // read on every use
  },
};
const passwordSignIn = (db) => db.user.active;

const state = {};
for (const [name, design] of Object.entries(DESIGNS)) {
  const db = world();
  design.deactivate(db);
  state[name] = db;
}

const names = Object.keys(DESIGNS);
console.log(`${'after the person is deactivated'.padEnd(38)}${names.map((n) => n.padEnd(20)).join('')}`.trimEnd());
console.log(`${'a new password sign-in'.padEnd(38)}${names.map((n) => (passwordSignIn(state[n]) ? 'works' : 'refused').padEnd(20)).join('')}`.trimEnd());
const working = Object.fromEntries(names.map((n) => [n, 0]));
KINDS.forEach(([kind], i) => {
  const cells = names.map((n) => {
    const ok = DESIGNS[n].mayAct(state[n], state[n].creds[i]);
    if (ok) working[n] += 1;
    return (ok ? 'still works' : 'refused').padEnd(20);
  });
  console.log(`${kind.padEnd(38)}${cells.join('')}`.trimEnd());
});
console.log(`${'credentials still working'.padEnd(38)}${names.map((n) => `${working[n]} of ${KINDS.length}`.padEnd(20)).join('')}`.trimEnd());

console.log('\nthe grant service is down, so the clean-up fails at its fourth step, and then a retry runs it again:');
console.log(`${'design'.padEnd(22)}${'clean-up stopped'.padEnd(18)}${'still working'.padEnd(16)}after the retry`);
for (const n of ['revocation list', 'epoch and clean-up']) {
  const db = world();
  let stopped = 'no';
  try { DESIGNS[n].deactivate(db, 3); } catch { stopped = 'yes'; }
  const count = () => db.creds.filter((c) => DESIGNS[n].mayAct(db, c)).length;
  const now = count();
  DESIGNS[n].deactivate(db, -1); // the retry: the same steps, with no failure this time
  console.log(`${n.padEnd(22)}${stopped.padEnd(18)}${`${now} of ${KINDS.length}`.padEnd(16)}${count()} of ${KINDS.length}`);
}

Output

after the person is deactivated       flag only           revocation list     epoch and clean-up
a new password sign-in                refused             refused             refused
browser session                       still works         refused             refused
access token, self-contained          still works         still works         refused
refresh token                         still works         refused             refused
personal API key                      still works         refused             refused
OAuth grant held by another app       still works         refused             refused
nightly export run as the person      still works         refused             refused
realtime connection opened earlier    still works         still works         refused
sign-in link e-mailed earlier         still works         still works         refused
invitation sent by the person         still works         still works         refused
credentials still working             9 of 9              4 of 9              0 of 9

the grant service is down, so the clean-up fails at its fourth step, and then a retry runs it again:
design                clean-up stopped  still working   after the retry
revocation list       yes               6 of 9          4 of 9
epoch and clean-up    yes               0 of 9          0 of 9

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node offboarding.mjs

A flag on the user stopped the new password sign-in and nothing else, so all 9 credentials kept working. The list of revocations covered sessions, refresh tokens, API keys, grants and the nightly job and left 4: the self-contained access token for its last 10 minutes, the open connection, the sign-in link and the invitation. Real products have shipped both mistakes. OrangeHRM did not end sessions when a user was disabled (CVE-2025-66289), and Rocket.Chat did not revoke OAuth tokens on deactivation (CVE-2026-49277). ASVS 7.4.2 asks that all active sessions end when an account is disabled or deleted.

The third design adds a number to the user and to every credential, the session epoch: A number that the server keeps for each user and that a signed cookie carries. When the server raises its number, every cookie that carries an older one is refused. More in “Sessions: IDs, fixation, timeouts and logout” of the sessions lesson. Deactivation marks the person inactive and raises the epoch in one write, and every check compares the epoch of the credential with the person’s current one, so all 9 were refused before anything was deleted. The clean-up that follows can fail. When the grant service was down and it stopped at its fourth step, the list left 6 of 9 working, and 4 after a retry, while the epoch design left 0 both times.

Four more parts follow. Sources of leavers decides how your service learns of a leaver when the provider says nothing. Movers keeps roles in step with groups and applies the change to a session that is open. Return and reuse makes a reactivated person and a reused name start clean. The audit trail records each change without the personal data.

Key idea

Make deactivation one write that every check reads, and run the clean-up after it, so that a failure halfway leaves nothing working.

Checkpoint

Check: One switch for every credential

  1. Question 1 of 2 In the first program a flag on the user was cleared. What did the flag stop?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A new password sign-in, and none of the nine credentials issued before

    This is the pattern behind OrangeHRM (CVE-2025-66289) and Rocket.Chat (CVE-2026-49277): the flag is real and the credentials never look at it.

  2. Question 2 of 2 Why does the epoch design raise the epoch before it starts the clean-up?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that a failure in the clean-up cannot leave a credential working, because every check already refuses the old epoch

    When the grant service was down and the clean-up stopped at its fourth step, the epoch design had 0 of 9 credentials working, and the list of revocations had 6 of 9.

Flashcards · 4 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is deprovisioning, and what does marking a user inactive stop?

    The work of ending a person's access when they leave or lose the right to it, one event in the life of an account: a joiner arrives, a mover changes team and a leaver goes. A SCIM request sets active to false, and RFC 7643 says the service provider decides what that means. In the lesson a flag on the user stopped only the new password sign-in, and all 9 credentials the person already held kept working.

  2. Which credentials can outlive a bare deactivation?

    A session; an API key; an access token and a refresh token; a grant to another app; a nightly job; an open connection; and a sign-in link or an invitation. A list of revocations left 4 of the 9 working: the access token (self-contained, so valid for its last 10 minutes), the open connection, the invitation and the sign-in link. ASVS 7.4.2 asks that all active sessions end when an account is disabled or deleted.

  3. How does an epoch end every credential with one write?

    A number on the user and on every credential: deactivation marks the person inactive and raises the epoch in one write, and every check compares the credential's epoch with the person's current one, so all 9 credentials were refused before anything was deleted. Clean-up after that can fail: when the grant service was down, the list of revocations left 6 of 9 working and the epoch design none.

  4. Which four more parts does the deprovisioning lesson add?

    Sources of leavers: how your service learns of a leaver when the provider reports nothing. Movers: keep roles in step with groups, and apply the change to a session that is open. Return and reuse: a reactivated person and a reused name start clean. And an audit trail that records each change without the personal data.

Learn about leavers from the provider’s pushes, its list and its silence Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Movers: recompute roles from the groups, and apply them to open sessions
  • A person who returns, and a name that is reused, must start clean
  • Record each change, and leave the personal data out
  • Practise: offboarding, role sync and reconciliation
    • Deactivation that ends every credential
    • Roles that follow the groups when someone moves
    • Reconcile accounts with the provider's list
  • Interview questions
    • A customer tells you that a former employee, deactivated in their identity provider three days ago, exported data from your product this morning. Your logs show a valid session. Explain how that can happen, how you identify which credential was used, and what you change so that it cannot happen again.
    • You are asked to add a nightly job that deactivates the accounts which the customer's identity provider no longer lists. What can go wrong, and what does the job check before it changes anything?
    • An audit finds that an engineer who moved from finance to engineering a year ago can still approve invoices. How did that happen, and what do you change so that a move removes what the old team granted without removing the roles that the application's own administrators gave?
    • An employee leaves, and a year later the company hires a different person with the same name and e-mail address. In another case the same person is rehired. Describe what your service does in each case with the old account, keys, authenticators and data, and what goes wrong in a design that finds accounts by user name.
  • Key takeaways

1 diagram · 4 runnable examples (JavaScript, Python) · 3 exercises with sample tests · 4 interview answers · about 50 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Terms in this lesson

account resurrection
A new person inheriting the data, roles, keys or authenticators of an earlier account because the two share a user name or an e-mail address.
deprovisioning
Ending a person's access in an application when they leave or lose the right to it: marking the account inactive, ending its sessions, tokens and keys, and removing what it granted.
joiner, mover and leaver
The three life-cycle events of an account. A joiner arrives, a mover changes team or role, and a leaver goes. Each one needs a change in every application the person uses.
just-in-time provisioning
Creating an account the first time a person signs in through the identity provider. The application learns of the person only then, and is not told when they leave.
privilege creep
The build-up of roles that a person keeps from earlier teams because every move adds roles and none removes them. Years later the person can do what no single job needs.
reconciliation
Comparing an application's accounts with the identity provider's list of people on a schedule, to find accounts that the provider no longer lists or now lists differently.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.