Deactivated Okta user still has app access: trace deprovisioning end to end
For developers and support engineers debugging why a user deactivated in Okta can still sign in or keep working in a downstream app. This runbook walks the deprovisioning path from Okta status to app assignment, SCIM calls, session invalidation, and fallback local accounts, with concrete checks and fixes.
TL;DR — If a deactivated Okta user still has downstream access, the most common causes are: the app was never actually deprovisioned from Okta, SCIM/API deprovisioning failed, or the downstream app is honoring an existing session/API token and not revoking it on disable. Start by checking the user’s Okta app assignment and the app’s provisioning logs, then verify in the target app whether the account is disabled or just still logged in. Reading time: ~6 min
The scenario
It’s Tuesday at 3:40 PM. HR confirms a contractor was deactivated in Okta an hour ago, but your engineering manager just watched that same user open the downstream app and pull customer data. Okta shows the person as deactivated, your offboarding automation says it completed, and now you need to answer the uncomfortable question: was this a stale browser session, a failed SCIM deprovision, or a second identity path nobody documented? You need to trace the whole chain fast, without guessing.
Symptoms
- In Okta, the user shows as deactivated/suspended, but in the downstream app the account still appears active.
- The user can still access the app with an existing browser session.
- The user cannot start a fresh SSO login, but existing tabs continue to work.
- The downstream app audit log shows successful requests after deactivation time.
- Provisioning logs show errors like:
HTTP 404 from SCIM endpoint
HTTP 401 Unauthorized
HTTP 403 Forbidden
PATCH /Users/123 failed
DELETE /Users/123 failed
- SAML/OIDC sign-in logs show old sessions still valid, or no new authentication events at all after deactivation.
- The app has a local user record not linked cleanly to the Okta identity.
- API access continues via personal access token, refresh token, or service token issued before deactivation.
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| User is still assigned to the app in Okta, or deactivation did not trigger app unassignment | Very common | In Okta admin, open the user and inspect assigned applications for the affected app |
| SCIM/API deprovisioning failed between Okta and the downstream app | Very common | In Okta admin, open the app's provisioning/task log and filter on the user |
| Downstream app keeps existing sessions valid after disable | Very common | In the app audit log, compare last successful request time with last fresh SSO login time |
| User has a separate local account or alternate IdP path in the app | Common | Search the app user table/API by email and external IdP subject |
| Tokens issued before deactivation remain valid and are not revoked | Common | Inspect app/API token list for the user and last-used timestamps |
| Attribute mismatch prevented Okta from targeting the correct downstream account | Less common | Compare Okta username/email/external ID to the downstream account identifier |
Step-by-step diagnosis
- Check whether this is fresh authentication or a stale session.
Command/menu path: in the downstream app audit log, filter by user email/ID and inspect events after the deactivation timestamp.
What indicates the problem:
- You see API/page access after deactivation, but no new SAML/OIDC login event.
- Requests continue with an already-issued session cookie or bearer token.
Jump to: Fixes → Existing sessions remain valid downstream or Fixes → Tokens remain valid after deactivation.
- Confirm the user is actually deactivated in Okta and note the exact timestamp.
Command/menu path: Okta admin → Directory/People → open user → status and recent lifecycle events.
What indicates the problem:
- Status is not actually deactivated/suspended yet, or deactivation happened later than reported.
- Lifecycle event is missing or delayed.
Jump to: Fixes → User still assigned in Okta / lifecycle action incomplete.
- Check whether the user is still assigned to the affected app in Okta.
Command/menu path: Okta admin → open user → Applications/Assignments, or open the app and search assigned users.
What indicates the problem:
- The user still appears assigned to the app.
- Assignment was inherited from a group that still includes the user.
Jump to: Fixes → User still assigned in Okta / lifecycle action incomplete.
- Check the app provisioning log for SCIM/API failures.
Command/menu path: Okta admin → open the app integration → Provisioning / Tasks / System Log, then filter on the user.
What indicates the problem:
- Failed deactivation/update calls around the offboarding time.
- Errors like 401/403/404/409 or schema/attribute validation failures.
Typical output shape from a SCIM endpoint check:
curl -i -H "Authorization: Bearer $SCIM_TOKEN" https://app.example.com/scim/v2/Users/123
HTTP/1.1 401 Unauthorized
Content-Type: application/scim+json
{"detail":"Invalid token","status":"401"}
Jump to: Fixes → SCIM/API deprovisioning failed.
- Verify the downstream account state directly.
Command/API path: use the app admin UI or user API to fetch the user by email, username, and external ID.
Example:
curl -sS -H "Authorization: Bearer $APP_ADMIN_TOKEN" "https://app.example.com/api/users?email=user@example.com" | jq
What indicates the problem:
- The account is still active.
- There are duplicate accounts for the same email.
- The active account has no Okta external ID mapped.
Jump to: Fixes → Separate local account or alternate identity path or Fixes → Attribute mismatch prevented the correct account from being deprovisioned.
- Check whether group-driven assignment is lagging or broken.
Command/menu path: inspect the groups that grant the app, then confirm the user was removed from those groups.
What indicates the problem:
- User is gone from Okta People but still present in an app-entitling group.
- Group membership changed after the app assignment evaluation window, or automation failed.
Jump to: Fixes → User still assigned in Okta / lifecycle action incomplete.
- Check for surviving API tokens, refresh tokens, PATs, or SSH keys in the downstream app.
Command/API path: app admin → user security/tokens page, or token API.
Example:
curl -sS -H "Authorization: Bearer $APP_ADMIN_TOKEN" "https://app.example.com/api/users/123/tokens" | jq '.[] | {id,last_used_at,revoked}'
What indicates the problem:
- Tokens are still active and show recent use after deactivation.
Jump to: Fixes → Tokens remain valid after deactivation.
Fixes
User still assigned in Okta / lifecycle action incomplete
Remove the app assignment directly and remove any group membership that re-grants it.
Actions:
- In Okta admin, open the user and remove the affected app assignment.
- Open the groups that entitle the app and remove the user from those groups.
- If offboarding is automated, rerun the lifecycle workflow/job that handles app unassignment.
If your agency keeps group state in code, fix the source of truth and re-apply it. Example with Terraform-managed group membership:
terraform plan
terraform apply
If you have an internal offboarding script, rerun only the app-unassign stage:
./offboard-user.sh --user user@example.com --stage unassign-apps --app app-example
Verify it worked:
In Okta, the user no longer appears under the app's assigned users, and a fresh SSO attempt is denied.
SCIM/API deprovisioning failed
Fix the app provisioning credentials or endpoint, then replay deprovisioning.
Common remediations:
- Replace expired SCIM bearer token or OAuth client secret in the app provisioning configuration.
- Correct the SCIM base URL.
- Fix required attribute mappings so the target user can be found.
- If the app expects
active:falseinstead of DELETE, align the connector behavior with the app.
Sanity-check the endpoint manually:
curl -i -H "Authorization: Bearer $SCIM_TOKEN" https://app.example.com/scim/v2/ServiceProviderConfig
Expected shape:
HTTP/1.1 200 OK
Content-Type: application/scim+json
Check the target user lookup:
curl -sS -G -H "Authorization: Bearer $SCIM_TOKEN" --data-urlencode 'filter=userName eq "user@example.com"' https://app.example.com/scim/v2/Users | jq
If lookup returns zero results, fix the identifier mapping in the app integration so Okta targets the same value the app stores, then replay provisioning from the app integration task page or equivalent retry control in your provider dashboard.
Verify it worked:
The provisioning log shows a successful disable/deactivate/update for the user, and the downstream account now shows inactive.
Existing sessions remain valid downstream
Terminate active sessions in the downstream app and shorten session TTL if your risk model requires near-immediate lockout.
⚠️ Forcing global session logout can sign out active users and cause brief support noise. If the app only supports tenant-wide session revocation, schedule carefully.
Actions:
- In the app admin UI, revoke all sessions for the user.
- If the app has an API, call the session revocation endpoint.
Example:
curl -X POST -H "Authorization: Bearer $APP_ADMIN_TOKEN" https://app.example.com/api/users/123/revoke_sessions
If your app is your own service, invalidate server-side sessions by user ID:
DELETE FROM user_sessions WHERE user_id = 123;
DELETE FROM oauth_refresh_tokens WHERE user_id = 123;
Then reduce session lifetime and require re-auth for sensitive routes. Example app config:
{
"session_ttl_minutes": 60,
"idle_timeout_minutes": 15,
"revoke_on_user_disable": true
}
Verify it worked:
Existing browser tabs are redirected to login on next request, and no further requests succeed without a new IdP login.
Separate local account or alternate identity path
Disable or merge the local account, and remove any password-based login path that bypasses Okta.
Actions:
- Search by email and by external subject/employee ID, not just display name.
- Disable the local account in the app admin UI or via API.
- If duplicates exist, merge them according to the app’s supported procedure; otherwise archive the local account.
Example query in your own database:
SELECT id, email, status, auth_provider, external_id
FROM users
WHERE lower(email) = lower('user@example.com')
OR external_id = '00u123example';
If a local password exists, clear it and force SSO-only:
UPDATE users
SET password_hash = NULL, auth_provider = 'okta', disabled = true
WHERE id = 123;
Verify it worked:
Only one user record remains, it is linked to the Okta identity, and password login/local auth for that user fails.
Tokens remain valid after deactivation
Revoke all user-issued tokens and add automatic token revocation to your offboarding flow.
Actions:
- Revoke PATs, API keys, refresh tokens, app passwords, and SSH keys associated with the user.
- If your app supports token introspection or deny-lists, mark existing tokens invalid immediately.
Example API cleanup:
curl -sS -H "Authorization: Bearer $APP_ADMIN_TOKEN" "https://app.example.com/api/users/123/tokens" | jq -r '.[].id' | while read -r id; do
curl -X DELETE -H "Authorization: Bearer $APP_ADMIN_TOKEN" "https://app.example.com/api/tokens/$id"
done
For your own OAuth server, revoke refresh tokens and rotate signing keys only if absolutely necessary. Key rotation is high blast radius; prefer targeted revocation first.
Verify it worked:
Previously issued tokens return 401/invalid_token, and token last-used timestamps stop advancing.
Attribute mismatch prevented the correct account from being deprovisioned
Align the immutable identifier used between Okta and the downstream app, then backfill existing users.
Actions:
- Pick one stable identifier for provisioning: usually external ID/employee ID, not mutable email.
- Update the app integration mapping so user lookup and updates use the same field the app stores.
- Backfill missing external IDs in the downstream app.
Example backfill in your own database:
UPDATE users
SET external_id = '00u123example'
WHERE email = 'user@example.com' AND external_id IS NULL;
Then rerun the deprovision operation for that user.
Verify it worked:
A SCIM lookup by the mapped identifier returns exactly one user, and the disable action lands on that record.
Prevention
- Add an offboarding canary that checks both IdP state and app state within minutes of deactivation.
./check-offboarding.sh --user user@example.com --apps app1,app2 --fail-on active-session,active-account,active-token
- Alert on provisioning failures from the IdP to downstream apps. If your logs are centralized, create a query for SCIM/API failures:
source=okta_logs ("deactivate" OR "provision") ("401" OR "403" OR "404" OR "409" OR "failed")
- In your app, revoke sessions and refresh tokens automatically on user disable.
{
"revoke_sessions_on_disable": true,
"revoke_refresh_tokens_on_disable": true
}
- Stop using email as the only join key for identity lifecycle. Pin an immutable external ID in your schema and CI checks.
ALTER TABLE users ADD CONSTRAINT users_external_id_unique UNIQUE (external_id);
- Add an integration test in CI for deprovisioning against a staging app tenant.
./integration-tests/deprovisioning_test.sh --idp okta --app staging-app --user fixture-offboard-1
- Inventory and disable alternate auth paths for SSO-managed apps: local passwords, API tokens, SSH keys, backup accounts. Track them in code or a machine-readable manifest.
app: app-example
auth_paths:
- sso_oidc
- local_password: disabled
- personal_access_tokens: allowed
- ssh_keys: disabled
offboarding_hooks:
- revoke_sessions
- revoke_tokens
- disable_local_account
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI