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:
defaultor a custom server with appropriate scopes - Custom Profile Attributes:
gwUserId,orgId,gwApplicationIdadded to the user schema - Groups:
gw-userandorg-admincreated in Directory > Groups
Okta Configuration
-
Create a Web Application:
Application Type: Web ApplicationName: GW Marketplace IntegrationGrant Types: Authorization Code, Resource Owner Password, Client Credentials -
Add Custom Profile Attributes (Directory > Profile Editor > Okta User):
gwUserId(string) -- General Wisdom user identifierorgId(string) -- Organization identifiergwApplicationId(string) -- Application identifier
-
Create Groups (Directory > Groups):
gw-user-- Assigned to all marketplace usersorg-admin-- Organization administrators
-
Configure Authorization Server Claims (Security > API > Authorization Servers > default):
- Add
groupsclaim: Value type: Groups, Filter: Matches regex.*, Include in: Access Token - Add
gwUserIdclaim: Value:appuser.gwUserId, Include in: Access Token - Add
orgIdclaim: Value:appuser.orgId, Include in: Access Token - Add
gwApplicationIdclaim: Value:appuser.gwApplicationId, Include in: Access Token
- Add
-
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
| Operation | Method | Endpoint |
|---|---|---|
| Search Users | GET | /users?search={query} |
| Create User | POST | /users?activate=true |
| Get User | GET | /users/{userId} |
| Update User | POST | /users/{userId} |
| Activate User | POST | /users/{userId}/lifecycle/activate |
| Deactivate User | POST | /users/{userId}/lifecycle/deactivate |
| List Groups | GET | /groups?q={name} |
| Add to Group | PUT | /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
-
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. -
Resource Owner Password grant disabled by default: Enable it explicitly in Application > General > Grant type > Resource Owner Password.
-
STAGED users cannot authenticate: Newly created users may be in
STAGEDstatus. Pass?activate=trueon the create request or call the activate lifecycle endpoint. -
API token permissions: SSWS tokens inherit permissions from the admin who created them. Use a service account with minimal required permissions.
-
searchvsfilterparameter: Usesearch(notfilter) for custom profile attributes. Thefilterparameter only works with standard attributes. -
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.
-
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.