Skip to main content

Okta Integration

Integrate Okta as the identity provider for your marketplace application. This guide covers user provisioning via the Okta Users API with SSWS token authentication and token acquisition via custom authorization servers.

Prerequisites

  • Okta Account: Developer or production Okta org
  • Web Application with:
    • Client ID and Client Secret
    • Resource Owner Password grant enabled
    • Appropriate redirect URIs
  • API Token: SSWS token for admin operations (Security > API > Tokens)
  • Custom Authorization Server: default or a custom server with appropriate scopes
  • Custom Profile Attributes: gwUserId, orgId, gwApplicationId added to the user schema
  • Groups: gw-user and org-admin created in Directory > Groups

Okta Configuration

  1. Create a Web Application:

    Application Type: Web Application
    Name: GW Marketplace Integration
    Grant Types: Authorization Code, Resource Owner Password, Client Credentials
  2. Add Custom Profile Attributes (Directory > Profile Editor > Okta User):

    • gwUserId (string) -- General Wisdom user identifier
    • orgId (string) -- Organization identifier
    • gwApplicationId (string) -- Application identifier
  3. Create Groups (Directory > Groups):

    • gw-user -- Assigned to all marketplace users
    • org-admin -- Organization administrators
  4. Configure Authorization Server Claims (Security > API > Authorization Servers > default):

    • Add groups claim: Value type: Groups, Filter: Matches regex .*, Include in: Access Token
    • Add gwUserId claim: Value: appuser.gwUserId, Include in: Access Token
    • Add orgId claim: Value: appuser.orgId, Include in: Access Token
    • Add gwApplicationId claim: Value: appuser.gwApplicationId, Include in: Access Token
  5. Create an API Token (Security > API > Tokens):

    • Click "Create Token", name it, and save the value securely (shown only once)

Environment Variables

# Okta Domain (without protocol)
OKTA_DOMAIN=dev-123456.okta.com

# Authorization Server Issuer
OKTA_ISSUER=https://dev-123456.okta.com/oauth2/default

# Client Credentials
OKTA_CLIENT_ID=0oa1234567890abcdef
OKTA_CLIENT_SECRET=your-client-secret

# API Token (for user provisioning via Users API)
OKTA_API_TOKEN=00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Authorization Server ID (default: "default")
OKTA_AUTH_SERVER=default

# Marketplace Configuration
MARKETPLACE_APP_ID=your-registered-app-id

Exchange Flow

sequenceDiagram
participant SDK as Provider SDK
participant Backend as Your Backend
participant Okta as Okta

SDK->>Backend: POST /auth/exchange (Bearer marketplace-jwt)
Backend->>Backend: Validate JWT (signature, iss, applicationId, exp)
Backend->>Okta: GET /api/v1/users?search=profile.gwUserId eq "..."
Note right of Backend: Authorization: SSWS {API_TOKEN}
alt User exists
Okta-->>Backend: User record
Backend->>Okta: POST /api/v1/users/{id} (update profile)
else User not found
Backend->>Okta: POST /api/v1/users?activate=true (create)
Okta-->>Backend: Created user
Backend->>Okta: PUT /api/v1/groups/{groupId}/users/{userId}
end
Backend->>Okta: POST /oauth2/{server}/v1/token (ROPC grant)
Okta-->>Backend: access_token + refresh_token
Backend-->>SDK: Tokens + user info

Backend Implementation

import { createHash } from 'crypto';

const OKTA_BASE_URL = `https://${process.env.OKTA_DOMAIN}`;
const AUTH_SERVER = process.env.OKTA_AUTH_SERVER ?? 'default';

// Deterministic password derivation
// Okta requires: 8+ chars, lowercase, uppercase, number, special character
function derivedPassword(userId: string): string {
const hash = createHash('sha256')
.update(`${userId}${process.env.OKTA_CLIENT_SECRET}`)
.digest('hex');
return `Gw!${hash.slice(0, 29)}`;
}

// SSWS headers for Okta Admin API
function adminHeaders() {
return {
Authorization: `SSWS ${process.env.OKTA_API_TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
};
}

// Search for existing user by gwUserId profile attribute
async function findUser(gwUserId: string) {
const search = encodeURIComponent(`profile.gwUserId eq "${gwUserId}"`);
const res = await fetch(`${OKTA_BASE_URL}/api/v1/users?search=${search}`, {
headers: adminHeaders(),
});

if (!res.ok) throw new Error(`User search failed: ${res.status}`);
const users = await res.json();
return users.length > 0 ? users[0] : null;
}

// Create user with profile attributes and credentials
async function createUser(claims: any) {
const login = `gw-${claims.userId}@gw-marketplace.local`;
const email = claims.email ?? login;
const password = derivedPassword(claims.userId);

const res = await fetch(`${OKTA_BASE_URL}/api/v1/users?activate=true`, {
method: 'POST',
headers: adminHeaders(),
body: JSON.stringify({
profile: {
login,
email,
firstName: 'GW',
lastName: claims.userId,
gwUserId: claims.userId,
orgId: claims.orgId,
gwApplicationId: claims.applicationId,
},
credentials: { password: { value: password } },
}),
});

if (!res.ok) throw new Error(`Create user failed: ${res.status}`);
return res.json();
}

// Update user profile attributes
async function updateUser(userId: string, claims: any) {
const res = await fetch(`${OKTA_BASE_URL}/api/v1/users/${userId}`, {
method: 'POST',
headers: adminHeaders(),
body: JSON.stringify({
profile: { orgId: claims.orgId, gwApplicationId: claims.applicationId },
}),
});

if (!res.ok) throw new Error(`Update user failed: ${res.status}`);
}

// Assign user to a group
async function assignGroup(userId: string, groupName: string) {
// Find group by name
const searchRes = await fetch(
`${OKTA_BASE_URL}/api/v1/groups?q=${encodeURIComponent(groupName)}`,
{ headers: adminHeaders() },
);
const groups = await searchRes.json();
const match = groups.find((g: any) => g.profile.name === groupName);
if (!match) return;

// Add user to group
await fetch(`${OKTA_BASE_URL}/api/v1/groups/${match.id}/users/${userId}`, {
method: 'PUT',
headers: adminHeaders(),
});
}

// Obtain tokens via Resource Owner Password grant
async function getUserTokens(login: string, password: string) {
const params = new URLSearchParams({
grant_type: 'password',
client_id: process.env.OKTA_CLIENT_ID!,
client_secret: process.env.OKTA_CLIENT_SECRET!,
username: login,
password,
scope: 'openid profile email groups offline_access',
});

const res = await fetch(`${OKTA_BASE_URL}/oauth2/${AUTH_SERVER}/v1/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});

if (!res.ok) throw new Error(`Token grant failed: ${res.status}`);
return res.json();
}

SSWS Authentication Pattern

Okta uses the SSWS (Secure Web Service) token format for admin API authentication instead of OAuth2 bearer tokens:

# All admin API requests use this header format
Authorization: SSWS 00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Key differences from OAuth2:

  • SSWS tokens do not expire automatically (must be revoked manually)
  • Created via Security > API > Tokens in the Okta admin console
  • Tied to the admin user who created them (inherits their permissions)
  • Should be rotated periodically and treated as highly privileged credentials

Token Structure

Okta access tokens contain groups and custom claims:

{
"iss": "https://dev-123456.okta.com/oauth2/default",
"exp": 1714000000,
"sub": "gw-user-abc123@gw-marketplace.local",
"groups": ["gw-user", "org-admin"],
"gwUserId": "user-abc123",
"orgId": "org-456",
"gwApplicationId": "app-789"
}

Session Revocation

Revoke sessions using the OAuth2 revocation endpoint:

async function revokeSession(refreshToken: string) {
const params = new URLSearchParams({
token: refreshToken,
token_type_hint: 'refresh_token',
client_id: process.env.OKTA_CLIENT_ID!,
client_secret: process.env.OKTA_CLIENT_SECRET!,
});

await fetch(`${OKTA_BASE_URL}/oauth2/${AUTH_SERVER}/v1/revoke`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
}

Token Introspection

Verify token validity server-side:

async function introspectToken(accessToken: string) {
const params = new URLSearchParams({
token: accessToken,
token_type_hint: 'access_token',
client_id: process.env.OKTA_CLIENT_ID!,
client_secret: process.env.OKTA_CLIENT_SECRET!,
});

const res = await fetch(`${OKTA_BASE_URL}/oauth2/${AUTH_SERVER}/v1/introspect`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});

return res.json(); // { active: true/false, ... }
}

Users API Reference

Base URL: https://{OKTA_DOMAIN}/api/v1

OperationMethodEndpoint
Search UsersGET/users?search={query}
Create UserPOST/users?activate=true
Get UserGET/users/{userId}
Update UserPOST/users/{userId}
Activate UserPOST/users/{userId}/lifecycle/activate
Deactivate UserPOST/users/{userId}/lifecycle/deactivate
List GroupsGET/groups?q={name}
Add to GroupPUT/groups/{groupId}/users/{userId}

Search Query Syntax

Okta uses expression-based search:

# Search by custom profile attribute
search=profile.gwUserId eq "user-abc123"

# Search by email
search=profile.email eq "user@example.com"

# Search by login
search=profile.login eq "gw-user-abc123@gw-marketplace.local"

# Combined search (OR)
search=profile.login eq "user-123" or profile.email eq "user@example.com"

Rate Limit Handling

Okta enforces rate limits and returns a 429 status with X-Rate-Limit-Reset header:

async function withRateLimitRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
if (err.status === 429 && attempt < maxRetries - 1) {
const resetTime = parseInt(err.headers?.['x-rate-limit-reset'] ?? '0', 10);
const delay = Math.max((resetTime * 1000) - Date.now(), 1000 * (2 ** attempt));
await new Promise(r => setTimeout(r, delay));
continue;
}
throw err;
}
}
throw new Error('Max retries exceeded');
}

Common Gotchas

  1. Password policy violations: Okta's default policy requires lowercase, uppercase, number, and special character. The Gw! prefix satisfies these requirements. Check your org's password policy if you get 400 errors.

  2. Resource Owner Password grant disabled by default: Enable it explicitly in Application > General > Grant type > Resource Owner Password.

  3. STAGED users cannot authenticate: Newly created users may be in STAGED status. Pass ?activate=true on the create request or call the activate lifecycle endpoint.

  4. API token permissions: SSWS tokens inherit permissions from the admin who created them. Use a service account with minimal required permissions.

  5. search vs filter parameter: Use search (not filter) for custom profile attributes. The filter parameter only works with standard attributes.

  6. Group assignment requires group ID: You must look up the group by name first to get the ID, then use the ID in the assignment endpoint.

  7. Eventual consistency: After creating a user, search may return empty for a brief period. Add a small delay if you need to search immediately after creation.

Additional Resources