Okta Management Skill
Safely read and audit Okta — the identity and access management platform.
MANDATORY: Discovery-First Pattern
Always discover the Okta org configuration and available resources before performing targeted queries. Never guess user IDs or app IDs.
Phase 1: Discovery
#!/bin/bash
okta_api() {
local endpoint="$1"
local method="${2:-GET}"
curl -s -X "$method" \
-H "Authorization: SSWS $OKTA_API_TOKEN" \
-H "Content-Type: application/json" \
"${OKTA_ORG_URL}/api/v1/${endpoint}"
}
echo "=== Okta Org Info ==="
okta_api "org" | jq '{
id: .id,
company_name: .companyName,
subdomain: .subdomain,
status: .status,
created: .created
}'
echo ""
echo "=== User Count ==="
okta_api "users?limit=1" -I 2>/dev/null | grep -i x-rate-limit || \
okta_api "users?limit=1&filter=status+eq+%22ACTIVE%22" | jq '. | length | "Sample returned: \(.)"' -r
echo ""
echo "=== Applications (first 20) ==="
okta_api "apps?limit=20" | jq -r '.[] | "\(.id)\t\(.label)\t\(.status)\t\(.signOnMode)"' | column -t
echo ""
echo "=== Authentication Policies ==="
okta_api "policies?type=ACCESS_POLICY" | jq -r '.[] | "\(.id)\t\(.name)\t\(.status)"' | column -t
Phase 1 outputs: Org details, app inventory, policy list — only reference these in subsequent operations.
Anti-Hallucination Rules
- NEVER guess user login or ID — always search via
users?search=orusers?filter= - NEVER assume app labels — always list apps in Phase 1
- NEVER fabricate group names — always list via
groups?q= - ONLY read and list — never create, update, or delete without explicit request
Safety Rules
- READ-ONLY by default: GET requests only —
users,apps,groups,logs,policies - MASK sensitive data: When displaying user profiles, redact SSN, passwords, recovery questions
- FORBIDDEN without explicit request: POST/PUT/DELETE to users, apps, groups; password resets; MFA factor enrollment
- NEVER print API tokens: Always redact token values in output
Core Helper Functions
#!/bin/bash
okta_api() {
local endpoint="$1"
local method="${2:-GET}"
curl -s -X "$method" \
-H "Authorization: SSWS $OKTA_API_TOKEN" \
-H "Content-Type: application/json" \
"${OKTA_ORG_URL}/api/v1/${endpoint}"
}
# Paginated fetch — follows Okta's Link header pagination
okta_api_paginated() {
local endpoint="$1"
local max_pages="${2:-5}"
local url="${OKTA_ORG_URL}/api/v1/${endpoint}"
local page=0
while [ -n "$url" ] && [ $page -lt $max_pages ]; do
response=$(curl -s -D /tmp/okta_headers \
-H "Authorization: SSWS $OKTA_API_TOKEN" \
-H "Content-Type: application/json" \
"$url")
echo "$response"
url=$(grep -i '^link:' /tmp/okta_headers | grep 'rel="next"' | sed 's/.*<\(.*\)>.*/\1/' | tr -d '\r')
page=$((page + 1))
done
}
# Search users safely
search_users() {
local query="$1"
okta_api "users?search=profile.email+sw+%22${query}%22&limit=10"
}
Common Operations
User Lookup & Status
#!/bin/bash
SEARCH="${1:?Search term required (email, login, or name)}"
echo "=== User Search: $SEARCH ==="
okta_api "users?search=profile.login+sw+%22${SEARCH}%22+or+profile.email+sw+%22${SEARCH}%22&limit=10" | jq -r '.[] | {
id: .id,
login: .profile.login,
email: .profile.email,
name: "\(.profile.firstName) \(.profile.lastName)",
status: .status,
created: .created,
last_login: .lastLogin,
password_changed: .passwordChanged
}'
MFA Factor Status
#!/bin/bash
USER_ID="${1:?User ID required — discover via user search first}"
echo "=== MFA Factors for $USER_ID ==="
okta_api "users/${USER_ID}/factors" | jq -r '.[] | {
id: .id,
type: .factorType,
provider: .provider,
status: .status,
created: .created,
last_verified: .lastVerifiedAt
}'
echo ""
echo "=== Available (Not Enrolled) Factors ==="
okta_api "users/${USER_ID}/factors/catalog" | jq -r '.[] | "\(.factorType)\t\(.provider)\t\(.status)"' | column -t
Application Assignments
#!/bin/bash
USER_ID="${1:?User ID required}"
echo "=== Apps Assigned to User $USER_ID ==="
okta_api "users/${USER_ID}/appLinks" | jq -r '.[] | "\(.appName)\t\(.label)\t\(.linkUrl)"' | column -t
echo ""
echo "=== App User Count (top 10 apps) ==="
okta_api "apps?limit=10" | jq -r '.[] | .id' | while read app_id; do
label=$(okta_api "apps/$app_id" | jq -r '.label')
count=$(okta_api "apps/${app_id}/users?limit=1" | jq '. | length')
echo "$label\t$count users (sampled)"
done | column -t
System Log Analysis
#!/bin/bash
SINCE="${1:-$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)}"
echo "=== System Log Events (since $SINCE) ==="
okta_api "logs?since=${SINCE}&limit=20&sortOrder=DESCENDING" | jq -r '.[] | {
time: .published,
event: .eventType,
severity: .severity,
actor: .actor.displayName,
outcome: .outcome.result,
target: (.target[0].displayName // "N/A"),
client_ip: .client.ipAddress
}'
echo ""
echo "=== Failed Login Attempts ==="
okta_api "logs?since=${SINCE}&filter=eventType+eq+%22user.session.start%22+and+outcome.result+eq+%22FAILURE%22&limit=20" | jq -r '.[] | "\(.published)\t\(.actor.displayName)\t\(.client.ipAddress)\t\(.outcome.reason)"' | column -t
Group & Policy Review
#!/bin/bash
echo "=== Groups (first 20) ==="
okta_api "groups?limit=20" | jq -r '.[] | "\(.id)\t\(.profile.name)\t\(.type)\tMembers: \(.profile.memberCount // "N/A")"' | column -t
echo ""
echo "=== Sign-On Policies ==="
okta_api "policies?type=OKTA_SIGN_ON" | jq -r '.[] | {
id: .id,
name: .name,
status: .status,
description: .description,
priority: .priority
}'
echo ""
echo "=== Password Policies ==="
okta_api "policies?type=PASSWORD" | jq -r '.[] | {
id: .id,
name: .name,
min_length: .settings.password.complexity.minLength,
require_lowercase: .settings.password.complexity.minLowerCase,
require_uppercase: .settings.password.complexity.minUpperCase,
require_number: .settings.password.complexity.minNumber,
max_age_days: .settings.password.age.maxAgeDays
}'
Output Format
Present results as a structured report:
Managing Okta Report
════════════════════
Resources discovered: [count]
Resource Status Key Metric Issues
──────────────────────────────────────────────
[name] [ok/warn] [value] [findings]
Summary: [total] resources | [ok] healthy | [warn] warnings | [crit] critical
Action Items: [list of prioritized findings]
Target ≤50 lines of output. Use tables for multi-resource comparisons.
Counter-Rationalizations
| Shortcut | Counter | Why |
|---|---|---|
| "I'll skip discovery and check known resources" | Always run Phase 1 discovery first | Resource names change, new resources appear — assumed names cause errors |
| "The user only asked for a quick check" | Follow the full discovery → analysis flow | Quick checks miss critical issues; structured analysis catches silent failures |
| "Default configuration is probably fine" | Audit configuration explicitly | Defaults often leave logging, security, and optimization features disabled |
| "Metrics aren't needed for this" | Always check relevant metrics when available | API/CLI responses show current state; metrics reveal trends and intermittent issues |
| "I don't have access to that" | Try the command and report the actual error | Assumed permission failures prevent useful investigation; actual errors are informative |
Common Pitfalls
- Rate limits: Okta enforces per-endpoint rate limits (typically 600/min for
/api/v1/users) — checkX-Rate-Limit-Remainingheader - Pagination: List endpoints return max 200 items — always follow
Link: rel="next"headers for complete data - User status lifecycle: STAGED -> PROVISIONED -> ACTIVE -> SUSPENDED/DEPROVISIONED — status transitions are one-directional in some cases
- Search vs filter:
searchuses Okta's search engine (eventual consistency);filteris exact match (strong consistency) — use filter for precise lookups - System log retention: Default 90 days; compressed logs may take longer to query — always specify
sinceparameter - API token scope: Token inherits permissions of the admin who created it — insufficient permissions return 403, not empty results
- Group types: OKTA_GROUP (manually managed) vs APP_GROUP (synced from app) vs BUILT_IN — behavior differs by type
- Deprovisioned users: Still exist in Okta but cannot authenticate — include
status eq "DEPROVISIONED"in filter to find them