OneLogin Identity Security Posture Management (ISPM)

Console-only posture scanning for a OneLogin account: 25 weighted Admin API checks in eight categories, run from the dashboard Posture tab and scored 0-100.

Last reviewed September 16, 2026Fresh

Overview

PasskeyBridge reads a OneLogin account through the OneLogin Admin API, runs 25 checks against its users, roles, applications, MFA policies, privileges and risk rules, and stores each one as a finding with a weight, a status and evidence. The dashboard renders a 0-100 posture score and the findings grouped by category. Every call the scan makes is a read.

It lives in the dashboard Posture tab, under the OneLogin provider tab. Scan rows carry provider set to onelogin.

Who can run it. Posture is an Enterprise tab: on the starter and pro plans the dashboard shows an upgrade panel in place of the tab. The plan gate is in the dashboard only. The shield-onelogin-ispm function enforces something different: the caller must present a valid dashboard session and be an admin member of the tenant in shield_tenant_members.

Console-only. shield-onelogin-ispm is not in the Cloudflare worker's PUBLIC_FUNCTIONS list, so it has no https://api.passkeybridge.io/v1 route and no API key lane. The dashboard calls it directly with supabase.functions.invoke and your session JWT.

API credentials and dashboard fields

In OneLogin. Create an API credential pair whose scope allows reading users, roles, applications, MFA policies, privileges, risk rules, hooks, mappings, branding and events. The function authenticates with the client_credentials grant, sending the pair as HTTP Basic credentials to https://SUBDOMAIN.onelogin.com/auth/oauth2/v2/token, then calls the Admin API with the bearer token it gets back.

It prefetches ten collections and reuses them across the checks.

EndpointCap
/api/2/users2,000 items
/api/2/apps1,000 items
/api/2/roles, /api/2/mfa/policies, /api/2/privileges, /api/2/risk/rules, /api/2/hooks, /api/2/mappings, /api/2/branding/brands500 items each
/api/1/events50 events, one page

Lists ask for 100 per page and follow the after_cursor value, falling back to the Link header for the older endpoint. A collection the credentials cannot read turns its checks into skipped findings that carry the HTTP status, and skipped checks leave the score alone.

In the dashboard. Posture tab, OneLogin provider tab, expand OneLogin configuration.

FieldGoes toStored as
Account subdomainonelogin_subdomain on shield_tenantsClear text, lowercased.
Regiononelogin_region on shield_tenantsClear text. US or EU.
Client IDonelogin_client_id on shield_tenantsClear text.
Client secretonelogin_client_secret_encrypted on shield_tenantsAES-256-GCM under the server-held SHIELD_ENCRYPTION_KEY.

The Region selector has no effect today. Both values resolve to https://SUBDOMAIN.onelogin.com, which is the host the scan calls either way. The value is stored and validated, and it changes nothing about where requests go.

Press Save. The subdomain, client ID and client secret are required on every save.

{ "action": "save_onelogin_config", "tenant_id": "<tenant uuid>", "onelogin_subdomain": "acme", "onelogin_client_id": "<client id>", "onelogin_client_secret": "<secret>", "onelogin_region": "US" }

A successful save answers {"ok": true} and writes a save_onelogin_config audit row.

StatusBody
400All fields required: onelogin_subdomain, onelogin_client_id, and onelogin_client_secret.
400Invalid OneLogin subdomain format
400Invalid region: X. Valid: US, EU
500Failed to save provider configuration

Checks by category

25 checks run on every scan, in eight categories.

CategoryChecksWhat they read
identity_store6User inventory, locked or suspended accounts, unactivated accounts, external directory coverage, accounts with no sign-in for 90 days, and user mapping rules.
applications6Application inventory, applications hidden from the portal, provisioning coverage, SAML signing certificates, security policy assignment, and connector diversity.
authentication4Whether MFA policies exist, the share that require MFA for everyone, WebAuthn or FIDO2 among their factors, and lockout thresholds.
authorization4Role count, wildcard privilege definitions, the privilege inventory, and users with no role.
observability2Active event hooks, and the health of a 50-event sample from the events endpoint.
credential_management1Accounts whose password has expired.
risk1Whether any SmartFactor risk rule exists.
governance1Whether custom branding is configured.

User states come from the numeric `status` field: 0 unactivated, 2 suspended, 3 locked and 4 password expired. The locked-or-suspended check passes at 5 percent of users or fewer and fails above 20 percent.

The privilege check reads definitions rather than assignments. /api/2/privileges returns privilege templates, so the check counts templates whose statements grant * or manage:all. It passes at 2 or fewer of those and fails above 5. It does not tell you how many people hold them.

Two checks are explicit about their own limits. Hidden Application Review flags visible: false applications, which may still be live for single sign-on. Account Lockout looks for a failed-attempt threshold inside MFA policies and warns when it finds none, because lockout can also be configured in user security policies that this endpoint does not return.

Posture score

The score is a penalty ratio over the checks that actually ran. Every check carries a hard-coded weight and a score_impact: zero when it passes, a negative number when it fails or warns. The function computes, over all non-skipped checks, 100 x (1 - total_penalty / max_penalty), where total_penalty is the sum of the absolute score_impact values and max_penalty is the sum of weight x 2. It rounds it and clamps it to 0-100. Weights run from 1 to 10. OneLogin's heaviest checks are MFA policy existence and MFA enforcement coverage at 10 each, then wildcard privilege definitions at 8.

There are no severity multipliers. severity (critical, high, medium, low, info) is a label on the finding row and has no effect on the arithmetic.

Skipped checks leave both sides of the ratio, so a check that could not run neither helps nor hurts. If every check is skipped the score is 0.

The tab turns the number into one of four labels.

ScoreLabel in the tab
80-100Strong
60-79Moderate
40-59Needs work
0-39Critical

Credential and result storage

Credentials. Only the client secret is encrypted, with AES-256-GCM under SHIELD_ENCRYPTION_KEY, and it is decrypted inside the function for the length of one scan. Encryption is mandatory: a save fails rather than storing plaintext if the key is missing. The subdomain, client ID and region stay in clear text because the token request needs them, and a migration revokes column-level SELECT on onelogin_client_secret_encrypted from the authenticated and anon roles.

Findings. Evidence holds counts and percentages, plus application names for the hidden-application check. OneLogin user records are counted inside the function and are not stored.

Scan rows land in shield_posture_scans and one finding row per check in shield_posture_findings. Both tables are tenant-admin read-only under row-level security: insert, update and delete are denied to the authenticated role, and the function writes them with its service-role client.

Audit trail. A completed scan writes a run_onelogin_posture_scan row to shield_audit_log with the provider, score, check counts and scan id. A failed scan writes the same verb with result set to failure.

Running a scan and retrying failures

Scans run on demand. No cron job calls any ISPM function, so a scan happens when someone presses a button in the Posture tab. Open the tab, select OneLogin, and press Run scan in the tab header. The button is disabled while a scan is in flight and the tab reloads the scan list when it finishes.

The button invokes the function with your signed-in session.

const { data, error } = await supabase.functions.invoke("shield-onelogin-ispm", {
  body: { action: "run_scan", tenant_id: "<tenant uuid>" },
});

A completed scan answers with the summary the tab renders.

{
  "scan_id": "7c1f0b2e-...",
  "parent_scan_id": null,
  "posture_score": 68,
  "checks_total": 25,
  "checks_passed": 14,
  "checks_failed": 5,
  "checks_warning": 4,
  "checks_skipped": 2,
  "duration_ms": 8421,
  "partial_failures": []
}

Retry failed checks appears in the Findings header once a completed scan has at least one failed or warning check. It creates a second scan that re-runs only the failed and warning check ids and copies the passed and skipped findings forward, so a skipped check changes only on a full Run scan.

{ "action": "retry_failed_checks", "tenant_id": "<tenant uuid>", "retry_of_scan_id": "<scan uuid>" }
StatusBodyCause
401Missing authorization header or UnauthorizedNo session JWT, or it has expired.
400Invalid JSON bodyThe body did not parse as JSON.
400tenant_id requiredtenant_id was missing from the body.
403Forbidden: tenant admin access requiredThe signed-in user is not an admin member of that tenant.
500Failed to verify tenant accessThe membership lookup itself failed.
400Unknown actionaction was not a save action, run_scan or retry_failed_checks.
400OneLogin configuration not found. Configure subdomain, Client ID, and Client Secret first.No credentials are stored for this tenant.
400OneLogin authentication failed: ...OneLogin rejected the credentials. The status and body are included.
400retry_of_scan_id requiredRetry was called without a parent scan id.
404parent scan not foundNo scan row has that id.
403parent scan does not belong to this tenant/providerThe parent scan belongs to another tenant or another provider.
400no failed or warning checks to retryEvery check in the parent scan passed or was skipped.
500Scan failed: ...The scan threw. The scan row is stored with status failed and the thrown message.

Confirm it worked. The score card shows the band, the score out of 100, and the Passed, Failed and Warnings tiles. The card under it shows Scan, Duration, Started and Type, where Type reads Full scan or Retry, N checks. Findings are grouped by category below; expanding one shows its description, Remediation, Evidence JSON, and a line reading Weight, impact and check with that check's numbers and id. The Alerts section in the same tab can post a completed scan to a Slack webhook or to email recipients once its failed-check threshold is met.

Related from the blog