This guide provides step-by-step instructions for configuring Okta to support AGBAC dual-subject authorization. After completing this guide, your Okta instance will:
✅ Accept token requests from AI agents including human identity (act)
✅ Issue tokens containing both agent identity (sub) and human identity (act)
✅ Enforce that both subjects are pre-approved before issuing tokens
✅ Enable resource servers to validate both subjects for access control
Estimated Time: 60-75 minutes
Okta Edition: Workforce Identity (requires Custom Authorization Server feature)
Prerequisites: Okta admin access, basic understanding of OAuth 2.0
- Prerequisites
- Architecture Overview
- Step 1: Create Custom Authorization Server
- Step 2: Define Scopes
- Step 3: Create Agent Application
- Step 4: Configure Custom Claim for Act
- Step 5: Configure Access Policy
- Step 6: Assign Applications and Users
- Step 7: Create Test User
- Step 8: Test Configuration
- Step 9: Configure Resource Server Validation
- Troubleshooting
- Reference: Configuration Examples
Before starting, ensure you have:
- Okta Workforce Identity (Okta Developer account works)
- Admin access to Okta Admin Console
- Custom Authorization Server feature enabled
- Okta domain (e.g.,
dev-123456.okta.comoryour-company.okta.com) - Basic familiarity with OAuth 2.0
-
curlor Postman for testing
Access Okta Admin Console:
https://your-okta-domain/admin
Verify Custom Authorization Server Available:
- Log in to Okta Admin Console
- Navigate to Security → API → Authorization Servers
- If you see the option to create authorization servers, you're good to go
- If not available, contact Okta support or use Okta Developer account
Note: The default authorization server (default) can be used for testing but custom authorization servers are recommended for production.
┌─────────────┐
│ Human │ Authenticates to application
└──────┬──────┘
│
▼
┌─────────────────────┐
│ Application │ Extracts human identity (act)
│ (Hybrid Sender) │ Provides to agent
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ AI Agent │ Creates client assertion JWT
│ │ Includes: sub (agent) + act (human)
└──────┬──────────────┘
│ Token Request
│ (client_credentials + client_assertion)
▼
┌─────────────────────┐
│ Okta Custom │ 1. Validates client assertion
│ Authorization │ 2. Custom claim extracts act from assertion
│ Server │ 3. Validates agent authorized (policy)
│ │ 4. Issues token with sub + act
└──────┬──────────────┘
│
▼ Access Token (contains sub + act)
┌─────────────────────┐
│ Resource Server │ 1. Validates token signature
│ │ 2. Validates agent (sub) authorized
│ │ 3. Validates human (act) authorized
│ │ 4. Grants access if BOTH pass
└─────────────────────┘
Custom Authorization Server:
A dedicated OAuth 2.0 server in Okta for issuing tokens. Required for custom claims.
Client Assertion:
A JWT created by the agent containing both identities, signed with agent credentials.
Custom Claim:
Okta expression that extracts act from the client assertion and adds it to the access token.
Access Policy:
Rules determining which clients can get tokens and under what conditions.
A custom authorization server provides dedicated OAuth endpoints and allows custom claims.
- Log in to Okta Admin Console
- Navigate to Security → API (left sidebar)
- Click Authorization Servers tab
- Click Add Authorization Server
Name: AGBAC-AS
Audience: https://api.example.com/finance
Description: Authorization Server for AGBAC dual-subject authorization
Important Notes:
- Audience is the identifier for your API (use your actual API URL in production)
- This value will appear in token
audclaim - Cannot be changed after creation
Click Save
After creation, you'll see:
Issuer: https://your-okta-domain/oauth2/aus123abc456 (example)
Metadata URI: https://your-okta-domain/oauth2/aus123abc456/.well-known/oauth-authorization-server
Save the Authorization Server ID: It's in the Issuer URL after /oauth2/ (e.g., aus123abc456)
You'll need this for token requests.
{
"id": "aus123abc456",
"name": "AGBAC-AS",
"description": "Authorization Server for AGBAC dual-subject authorization",
"audiences": [
"https://api.example.com/finance"
],
"issuer": "https://your-okta-domain/oauth2/aus123abc456",
"status": "ACTIVE",
"created": "2026-01-02T10:00:00.000Z",
"lastUpdated": "2026-01-02T10:00:00.000Z"
}Scopes represent permissions in Okta OAuth.
- Click on your AGBAC-AS authorization server
- Click Scopes tab
- Click Add Scope
Name: finance.read
Display Name: Read Finance Data
Description: Allows reading finance system data
User Consent: Not required (leave unchecked)
Default Scope: No (leave unchecked)
Metadata: Published (keep default)
Click Create
Name: finance.write
Display Name: Write Finance Data
Description: Allows writing finance system data
For this guide, we'll use finance.read.
The Scopes tab should show:
finance.read Read Finance Data Not required
{
"name": "finance.read",
"displayName": "Read Finance Data",
"description": "Allows reading finance system data",
"consent": "REQUIRED",
"default": false,
"metadataPublish": "ALL_CLIENTS",
"system": false
}The application represents the AI agent's identity.
- Navigate to Applications → Applications (left sidebar)
- Click Create App Integration
Sign-in method: API Services (OAuth 2.0 client credentials)
Click Next
App integration name: Finance Agent
Click Save
You'll see:
Client ID: 0oa123abc456xyz (example)
Client Secret: Click Copy to clipboard
Client authentication: Client Secret (default)
- Click Edit in General Settings section
- Scroll to Proof Key for Code Exchange (PKCE)
- Ensure it's set to:
Not required(for client_credentials) - Click Save
- Scroll to General Settings
- Under Grant type, verify:
- ✅ Client Credentials is checked
- If not, click Edit, check it, and Save
{
"id": "0oa123abc456xyz",
"name": "Finance Agent",
"label": "Finance Agent",
"status": "ACTIVE",
"accessibility": {
"selfService": false,
"errorRedirectUrl": null,
"loginRedirectUrl": null
},
"credentials": {
"oauthClient": {
"client_id": "0oa123abc456xyz",
"client_secret": "***",
"token_endpoint_auth_method": "client_secret_post"
}
},
"settings": {
"oauthClient": {
"grant_types": [
"client_credentials"
],
"response_types": [
"token"
],
"application_type": "service"
}
}
}This is the most critical step. The custom claim extracts act from the client assertion.
- Go to Security → API → Authorization Servers
- Click on AGBAC-AS
- Click Claims tab
- Click Add Claim
Name: act
Include in token type: Access Token (select from dropdown)
Value type: Expression
Value: clientAssertion.claims.act
Disable claim: Unchecked
Include in: Any scope
Alternative (scope-specific): If you want the claim only for specific scopes:
- Include in:
The following scopes - Select:
finance.read
Click Create
Expression: clientAssertion.claims.act
What it means:
clientAssertion- The JWT the agent sends during token request.claims- Access the claims in that JWT.act- Extract theactclaim specifically
How it works:
- Agent creates client assertion JWT with
actclaim - Agent sends assertion during token request
- Okta evaluates the expression
- Okta extracts
actfrom assertion - Okta adds
actto the access token being issued
The Claims tab should show:
Name Value Type Value Include In
act Expression clientAssertion.claims.act Access Token
{
"id": "ocl123abc456",
"name": "act",
"status": "ACTIVE",
"claimType": "RESOURCE",
"valueType": "EXPRESSION",
"value": "clientAssertion.claims.act",
"alwaysIncludeInToken": false,
"conditions": {
"scopes": []
},
"system": false
}- Okta expressions cannot perform complex logic
- The expression assumes
actexists in client assertion - If
actis missing, the claim will be omitted (not cause error)
✅ Best Practice:
- Agent must always include
actin client assertion - Validate
actpresence at resource server - Log when
actis missing for debugging
Access policies control which clients can obtain tokens.
- In AGBAC-AS authorization server
- Click Access Policies tab
- Click Add New Access Policy
Name: AGBAC-Policy
Description: Access policy for AGBAC dual-subject authorization
Assign to: The following clients:
Click Create Policy
- In the Assign to section, click Assign
- Search for:
Finance Agent - Select the checkbox next to Finance Agent
- Click Done
- Click Add Rule in the AGBAC-Min-Policy
- Configure the rule:
Rule Name: Allow Client Credentials with Act
IF Grant type is: Check ✅ Client Credentials
AND User is: Any user assigned the app (default is fine, not used for client_credentials)
AND Scopes requested: Any scopes
THEN Access token lifetime is: 1 hour (3600 seconds)
Refresh token lifetime is: Leave as is (not used for client_credentials)
Click Create Rule
You should see:
Policy: AGBAC-Policy
Assigned to: Finance Agent (1 client)
Rules: Allow Client Credentials with Act
{
"type": "OAUTH_AUTHORIZATION_POLICY",
"id": "pol123abc456",
"name": "AGBAC-Policy",
"description": "Access policy for AGBAC dual-subject authorization",
"status": "ACTIVE",
"priority": 1,
"conditions": {
"clients": {
"include": ["0oa123abc456xyz"]
}
},
"rules": [
{
"name": "Allow Client Credentials with Act",
"status": "ACTIVE",
"priority": 1,
"conditions": {
"grantTypes": {
"include": ["client_credentials"]
},
"scopes": {
"include": ["*"]
}
},
"actions": {
"token": {
"accessTokenLifetimeMinutes": 60,
"refreshTokenLifetimeMinutes": 0,
"refreshTokenWindowMinutes": 10080
}
}
}
]
}In Okta, application assignments represent pre-approval.
For Client Credentials Flow:
- Agents don't have user context
- Assignment represents that the client (agent) is approved
- Human authorization validated at resource server
Pre-Approval Model:
- Agent assigned to policy (Step 5.3) = Agent pre-approved
- Human assigned to application (for governance) = Human tracked
- Resource server validates both subjects independently
- Navigate to Applications → Applications
- Click Finance Agent
- Click Assignments tab
You should see the service client itself (no user assignments needed for client_credentials).
While not required for token issuance, you can assign human users to the application for governance tracking.
- In Finance Agent application
- Click Assignments tab
- Click Assign → Assign to People
- Search for users (we'll create test user next)
- Click Assign next to the user
- Click Save and Go Back
- Click Done
Note: This assignment is for governance only. The human's authorization is validated at the resource server, not by Okta during token issuance.
Create a test user representing the human principal.
- Navigate to Directory → People (left sidebar)
- Click Add Person
First name: Alice
Last name: Smith
Username: alice@corp.example.com
Primary email: alice@corp.example.com
Secondary email: Leave empty
Groups: Leave as default
Password: Set by admin
Enter password: Test123!@# (use secure password in production)
User must change password on first login: Unchecked (for testing)
Click Save
If the user is created in Staged status:
- Click on the user
- Click Activate
- Confirm activation
For governance tracking:
- Navigate to Applications → Applications → Finance Agent
- Click Assignments tab
- Click Assign → Assign to People
- Find
alice@corp.example.com - Click Assign
- Click Save and Go Back
- Click Done
{
"id": "00u123abc456xyz",
"status": "ACTIVE",
"profile": {
"firstName": "Alice",
"lastName": "Smith",
"email": "alice@corp.example.com",
"login": "alice@corp.example.com"
},
"credentials": {
"password": {}
}
}After creating the user, you need to obtain the Okta user ID to use in the act.sub field.
Method 1: From User Profile Page
- Navigate to Directory → People
- Click on
alice@corp.example.com - The user ID is displayed in the URL and can be found in the user profile
- Format:
00u123abc456xyz(starts with00u)
Method 2: From URL
The user ID appears in the browser URL when viewing the user:
https://your-okta-domain/admin/user/profile/view/00u123abc456xyz
^^^^^^^^^^^^^^^
This is the User ID
Method 3: Via Okta API
curl -X GET "https://your-okta-domain/api/v1/users/alice@corp.example.com" \
-H "Authorization: SSWS YOUR_API_TOKEN" \
-H "Accept: application/json"Response will include:
{
"id": "00u123abc456xyz",
"profile": {
"email": "alice@corp.example.com",
...
}
}Copy this User ID - you'll use it in the act.sub field when creating the act claim.
Why User ID instead of email?
- Privacy: User ID is pseudonymous (not PII like email)
- Stability: Doesn't change if user's email changes
- Correlation: Matches Okta's internal user ID for perfect audit log correlation
- Uniqueness: Guaranteed unique across all Okta organizations
Okta User ID Format:
- Always starts with
00ufor users - Followed by 15-17 alphanumeric characters
- Example:
00u123abc456xyz - Different from group IDs (
00g) or application IDs (0oa)
Test the complete flow by requesting a token with client assertion.
Okta Domain: https://your-okta-domain
Authorization Server ID: aus123abc456 (from Step 1.3)
Token Endpoint: https://your-okta-domain/oauth2/aus123abc456/v1/token
Client ID: (from Step 3.4)
Client Secret: (from Step 3.4)
Scope: finance.read
Client Assertion Payload:
{
"iss": "0oa123abc456xyz",
"sub": "0oa123abc456xyz",
"aud": "https://your-okta-domain/oauth2/aus123abc456",
"exp": 1735686300,
"iat": 1735686000,
"jti": "test-assertion-unique-123",
"act": {
"sub": "00u123abc456xyz",
"email": "alice@corp.example.com",
"name": "Alice Smith"
}
}Critical Values:
iss: Your client IDsub: Your client ID (same as iss)aud: Your authorization server URL (not the token endpoint!)exp: Current timestamp + 300 secondsiat: Current timestampjti: Unique identifier (prevent replay)act: Human identity object
Important Field Explanations:
| Field | Value | Notes |
|---|---|---|
iss |
Client ID | Issuer = the agent client ID |
sub |
Client ID | Subject = the agent client ID |
aud |
Authorization Server URL | Not the token endpoint - just the auth server base URL |
exp |
Current time + 300 | Expiration (5 minutes from now) |
iat |
Current time | Issued at timestamp |
jti |
Unique nonce | Prevents replay attacks |
act.sub |
Okta User ID | From Step 7.5 - format: 00u123abc456xyz |
act.email |
User's email | For human-readable logging |
act.name |
User's name | For human-readable logging |
Critical: act.sub must be the Okta User ID
The act.sub field should contain the user's Okta user ID (like 00u123abc456xyz), not their email address. This provides:
- Better privacy (pseudonymous identifier)
- Stability (doesn't change if email changes)
- Perfect correlation with Okta audit logs
- Guaranteed uniqueness across all users
Sign with Client Secret (HS256):
Using Python:
import jwt
import time
import jwt
import time
# Replace with your actual values
CLIENT_ID = "0oa123abc456xyz"
CLIENT_SECRET = "your-client-secret-here"
AUTH_SERVER_ID = "aus123abc456"
OKTA_DOMAIN = "https://dev-123456.okta.com"
USER_ID = "00u123abc456xyz" # From Step 7.5
payload = {
"iss": CLIENT_ID,
"sub": CLIENT_ID,
"aud": f"{OKTA_DOMAIN}/oauth2/{AUTH_SERVER_ID}",
"exp": int(time.time()) + 300,
"iat": int(time.time()),
"jti": f"assertion-{int(time.time())}",
"act": {
"sub": USER_ID, # Okta user ID (not email!)
"email": "alice@corp.example.com",
"name": "Alice Smith"
}
}
client_assertion = jwt.encode(payload, CLIENT_SECRET, algorithm="HS256")
print(f"Client Assertion:\n{client_assertion}")Using https://jwt.io:
- Go to https://jwt.io
- Paste the payload above (update values)
- Select algorithm:
HS256 - In "Verify Signature", paste your client secret
- Copy the encoded JWT
Using curl:
curl --request POST \
--url 'https://your-okta-domain/oauth2/aus123abc456/v1/token' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=finance.read' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode "client_assertion=YOUR_SIGNED_JWT_HERE"Replace:
your-okta-domainwith your actual domainaus123abc456with your auth server IDYOUR_SIGNED_JWT_HEREwith the JWT from step 8.2
Expected Response:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJraWQiOiJGSkE4...",
"scope": "finance.read"
}Copy the access_token and decode at https://jwt.io
Expected Token Structure:
{
"ver": 1,
"jti": "AT.abc123xyz",
"iss": "https://your-okta-domain/oauth2/aus123abc456",
"aud": "https://api.example.com/finance",
"iat": 1735686000,
"exp": 1735689600,
"cid": "0oa123abc456xyz",
"scp": [
"finance.read"
],
"sub": "0oa123abc456xyz",
"act": {
"sub": "00u123abc456xyz",
"email": "alice@corp.example.com",
"name": "Alice Smith"
}
}✅ Success Criteria:
-
Agent Identity:
sub: Your client IDcid: Your client ID (client ID claim)
-
Human Identity:
act: Object with human dataact.sub:alice@corp.example.comact.email:alice@corp.example.comact.name:Alice Smith
-
Authorization:
scp: Array containingfinance.readaud: Your API audience
-
Issuer:
iss: Your authorization server URL
If all claims are present → Configuration successful! ✅
HTTP 401 Unauthorized:
Possible causes:
1. Client secret incorrect
2. Client assertion signature invalid
3. Client assertion aud doesn't match auth server URL
Solutions:
- Verify client secret in Applications → Finance Agent → Credentials
- Check client assertion is signed with correct secret
- Ensure aud is: https://your-okta-domain/oauth2/aus123abc456
NOT the token endpoint URL
HTTP 400 Bad Request:
Error: "The client assertion is invalid"
Causes:
1. Client assertion exp is expired
2. Client assertion iat is in the future
3. Client assertion iss doesn't match client_id
Solutions:
- Use current timestamp for iat
- Set exp to iat + 300 seconds
- Ensure iss and sub both equal client ID
Token issued but missing act claim:
Possible causes:
1. Custom claim not configured
2. Client assertion missing act
3. Claim expression incorrect
Solutions:
- Verify claim exists: Security → API → AGBAC-AS → Claims
- Check claim value: clientAssertion.claims.act
- Decode client assertion at jwt.io - verify it has act claim
Your API/resource server must validate both subjects.
Pseudocode:
def validate_dual_subject_token(token, resource):
# 1. Verify token signature
decoded = verify_jwt_signature(token, okta_public_key)
# 2. Validate standard claims
validate_expiry(decoded['exp'])
validate_issuer(decoded['iss'], expected_issuer)
validate_audience(decoded['aud'], expected_audience)
# 3. Extract subjects
agent_id = decoded['sub'] # Client ID
act_claim = decoded.get('act')
if not act_claim:
raise Unauthorized("Token missing human identity (act)")
human_id = act_claim['sub'] # 00u123abc456xyz (Okta user ID)
# 4. Validate agent authorization
agent_scopes = decoded.get('scp', [])
if 'finance.read' not in agent_scopes:
raise Forbidden("Agent not authorized for finance.read")
# 5. Validate human authorization
# Check against your user database or Okta API
if not user_has_finance_access(human_id):
raise Forbidden("Human not authorized for finance access")
# 6. Log for audit
audit_log(agent_id, human_id, resource, "ALLOWED")
return TrueJWKS Endpoint:
https://your-okta-domain/oauth2/aus123abc456/v1/keys
Example Python Implementation:
import jwt
import requests
from functools import lru_cache
@lru_cache()
def get_okta_public_key(token):
"""Fetch Okta public key from JWKS endpoint."""
# Decode header to get key ID
header = jwt.get_unverified_header(token)
kid = header['kid']
# Fetch JWKS
auth_server_id = "aus123abc456" # Your auth server ID
okta_domain = "https://your-okta-domain"
jwks_url = f"{okta_domain}/oauth2/{auth_server_id}/v1/keys"
response = requests.get(jwks_url)
jwks = response.json()
# Find matching key
for key in jwks['keys']:
if key['kid'] == kid:
return jwt.algorithms.RSAAlgorithm.from_jwk(key)
raise ValueError(f"Public key not found for kid: {kid}")
def validate_agbac_token(token, resource):
"""Validate AGBAC dual-subject token from Okta."""
try:
# Get public key and verify
public_key = get_okta_public_key(token)
# Expected values
auth_server_id = "aus123abc456"
okta_domain = "https://your-okta-domain"
expected_issuer = f"{okta_domain}/oauth2/{auth_server_id}"
expected_audience = "https://api.example.com/finance"
decoded = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience=expected_audience,
issuer=expected_issuer
)
# Extract subjects
agent_id = decoded['sub']
act = decoded.get('act')
if not act or 'sub' not in act:
return {
'authorized': False,
'reason': 'Missing human identity (act)'
}
human_id = act['sub']
# Validate agent scopes
scopes = decoded.get('scp', [])
if 'finance.read' not in scopes:
return {
'authorized': False,
'reason': f'Agent {agent_id} lacks required scope'
}
# Validate human authorization
if not user_has_finance_access(human_id):
return {
'authorized': False,
'reason': f'Human {human_id} not authorized'
}
# Success
return {
'authorized': True,
'agent': agent_id,
'human': human_id
}
except jwt.ExpiredSignatureError:
return {'authorized': False, 'reason': 'Token expired'}
except jwt.InvalidAudienceError:
return {'authorized': False, 'reason': 'Invalid audience'}
except jwt.InvalidIssuerError:
return {'authorized': False, 'reason': 'Invalid issuer'}
except jwt.InvalidTokenError as e:
return {'authorized': False, 'reason': f'Invalid token: {e}'}
def user_has_finance_access(email):
"""
Validate human has finance access.
Implement based on your system.
"""
# Option 1: Query your database
user = db.query(User).filter_by(email=email).first()
return user and 'FinanceUser' in user.roles
# Option 2: Query Okta Users API
# (Requires Okta API token)
# See Okta API documentationIf you manage users in Okta, you can query the Okta Users API:
import requests
def get_okta_api_token():
"""
Get Okta API token using API Services application.
Create separate M2M app with Okta API scopes.
"""
# This requires setting up Okta API Access Management
# See: https://developer.okta.com/docs/guides/implement-oauth-for-okta/
pass
def user_has_finance_access_okta(email):
"""Check if user exists in Okta and has appropriate groups."""
okta_domain = "https://your-okta-domain"
api_token = get_okta_api_token()
# Search for user
response = requests.get(
f"{okta_domain}/api/v1/users",
params={"filter": f'profile.email eq "{email}"'},
headers={"Authorization": f"SSWS {api_token}"}
)
users = response.json()
if not users:
return False
user_id = users[0]['id']
# Check user's groups or app assignments
response = requests.get(
f"{okta_domain}/api/v1/users/{user_id}/groups",
headers={"Authorization": f"SSWS {api_token}"}
)
groups = response.json()
# Check if user in FinanceUsers group
return any(group['profile']['name'] == 'FinanceUsers' for group in groups)Log every dual-subject access:
import json
import logging
from datetime import datetime
def audit_log(agent_id, human_id, resource, result, reason=None):
"""Log dual-subject access for audit trail."""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': 'DUAL_AUTH_ACCESS',
'agent_identity': agent_id,
'human_identity': human_id,
'resource': resource,
'result': result, # ALLOWED or DENIED
'reason': reason
}
logging.info(json.dumps(log_entry))Example Log:
{
"timestamp": "2026-01-02T15:30:45.123Z",
"event_type": "DUAL_AUTH_ACCESS",
"agent_identity": "0oa123abc456xyz",
"human_identity": "00u123abc456xyz",
"resource": "/api/finance/reports/Q4-2025",
"result": "ALLOWED",
"reason": null
}✅ DO: Log IAM identifiers
logger.info(
"API access",
extra={
"agent_id": "0oa123abc456xyz",
"human_id": "00u123abc456xyz", # Okta user ID
"action": "read",
"resource": "/api/finance/reports"
}
)❌ DON'T: Log PII (email, name)
# BAD - This logs PII
# logger.info(f"Access by {human_email}") # ❌ Email is PII
# logger.info(f"User {human_name}") # ❌ Name is PIIWhy log user ID instead of email?
- Privacy: User ID is pseudonymous (not PII)
- GDPR/CCPA compliance: Reduces PII in logs
- Correlation: Can correlate with Okta System Log using user ID
- Stability: Doesn't change if user's email changes
- Uniqueness: Guaranteed unique across all Okta orgs
Symptom: "Authorization Servers" option not available or grayed out.
Cause: Custom Authorization Servers require Okta Workforce Identity or Developer Edition.
Solutions:
-
Use Okta Developer Account (free):
- Sign up at https://developer.okta.com/signup/
- Provides custom authorization servers
-
Use Default Authorization Server (limited):
- URL:
https://your-okta-domain/oauth2/default - Limited customization options
- Not recommended for production
- URL:
-
Contact Okta Sales:
- Upgrade to Workforce Identity
Error: invalid_client
Possible Causes:
- Client secret incorrect
- Client assertion signature invalid
- Client ID wrong
Solutions:
# Verify client credentials
# Applications → Finance Agent → General Settings
# Client ID should match iss/sub in assertion
# Regenerate secret if needed
# Check client assertion signature
# Decode at jwt.io
# Verify HS256 selected and secret correct
# Verify client_assertion_type parameter
# Must be: urn:ietf:params:oauth:client-assertion-type:jwt-bearerError: The client assertion is invalid
Possible Causes:
- Client assertion expired
- Client assertion aud incorrect
- Client assertion iss/sub mismatch
Solutions:
# Check timestamps
# iat should be current time
# exp should be iat + 300 seconds
# Verify aud claim
# Must match: https://your-okta-domain/oauth2/aus123abc456
# Common error: using token endpoint instead of auth server URL
# Verify iss and sub
# Both must equal client_idPossible Causes:
- Custom claim not configured
- Claim expression incorrect
- Client assertion missing
act
Solutions:
# Verify custom claim exists
# Security → API → AGBAC-AS → Claims
# Should show: act with expression clientAssertion.claims.act
# Decode client assertion
# Use jwt.io
# Verify it contains act claim with proper structure
# Check claim conditions
# Ensure claim is set to "Any scope" or includes your scopeError: The requested scope is invalid, unknown, or malformed
Possible Causes:
- Scope doesn't exist in authorization server
- Scope not assigned to client
Solutions:
# Verify scope exists
# Security → API → AGBAC-AS → Scopes
# Should show: finance.read
# Check policy allows scope
# AGBAC-AS → Access Policies → AGBAC-Policy → Rule
# Scopes requested: Any scopes (or specifically finance.read)Error: Signature verification failed
Possible Causes:
- Using wrong public key
- Token from different authorization server
- Token expired
Solutions:
# Verify JWKS endpoint
correct_jwks = f"https://{okta_domain}/oauth2/{auth_server_id}/v1/keys"
# Check token header for kid
header = jwt.get_unverified_header(token)
print(f"Key ID: {header['kid']}")
# Verify issuer matches
decoded = jwt.decode(token, options={"verify_signature": False})
print(f"Issuer: {decoded['iss']}")
# Should be: https://your-okta-domain/oauth2/aus123abc456Custom Authorization Server:
{
"id": "aus123abc456",
"name": "AGBAC-AS",
"audiences": ["https://api.example.com/finance"],
"issuer": "https://your-okta-domain/oauth2/aus123abc456",
"status": "ACTIVE"
}Scope:
{
"name": "finance.read",
"displayName": "Read Finance Data",
"description": "Allows reading finance system data"
}Custom Claim:
{
"name": "act",
"claimType": "RESOURCE",
"valueType": "EXPRESSION",
"value": "clientAssertion.claims.act",
"alwaysIncludeInToken": false
}Application:
{
"name": "Finance Agent",
"credentials": {
"oauthClient": {
"client_id": "0oa123abc456xyz",
"token_endpoint_auth_method": "client_secret_post"
}
},
"settings": {
"oauthClient": {
"grant_types": ["client_credentials"]
}
}
}Access Policy:
{
"name": "AGBAC-Policy",
"conditions": {
"clients": {
"include": ["0oa123abc456xyz"]
}
},
"rules": [
{
"name": "Allow Client Credentials with Act",
"conditions": {
"grantTypes": {
"include": ["client_credentials"]
}
},
"actions": {
"token": {
"accessTokenLifetimeMinutes": 60
}
}
}
]
}{
"iss": "0oa123abc456xyz",
"sub": "0oa123abc456xyz",
"aud": "https://dev-123456.okta.com/oauth2/aus123abc456",
"exp": 1735686300,
"iat": 1735686000,
"jti": "unique-assertion-id-abc123",
"act": {
"sub": "00u123abc456xyz",
"email": "alice@corp.example.com",
"name": "Alice Smith"
}
}{
"ver": 1,
"jti": "AT.abc123xyz789",
"iss": "https://dev-123456.okta.com/oauth2/aus123abc456",
"aud": "https://api.example.com/finance",
"iat": 1735686000,
"exp": 1735689600,
"cid": "0oa123abc456xyz",
"scp": [
"finance.read"
],
"sub": "0oa123abc456xyz",
"act": {
"sub": "00u123abc456xyz",
"email": "alice@corp.example.com",
"name": "Alice Smith"
}
}You've successfully configured Okta for AGBAC dual-subject authorization!
What You Configured:
✅ Custom Authorization Server for dedicated OAuth endpoints
✅ Scope representing finance system access
✅ Agent application with client credentials grant
✅ Custom claim to extract act from client assertion
✅ Access policy controlling token issuance
✅ Test user representing human principal
✅ Tested token issuance with dual subjects
Key Components:
- Custom Authorization Server - Dedicated OAuth server
- Custom Claim - Expression extracts
actfrom client assertion - Client Assertion - JWT containing both agent and human identities
- Access Policy - Controls which clients can get tokens
- Dual-Subject Token - Contains
sub(agent) andact(human)
Next Steps:
- Configure Python Application: Follow Python Application/Agent Configuration Guide
- Implement Resource Server Validation: Use code from Step 9
- Test End-to-End: Run complete workflow with real agent
- Add More Agents/Users: Create additional applications and users
- Production Hardening: Enable MFA, rotate secrets, implement monitoring
Security Reminders:
- 🔒 Use HTTPS everywhere
- 🔒 Rotate client secrets regularly (Okta: Applications → Credentials)
- 🔒 Monitor Okta System Log for anomalies
- 🔒 Implement rate limiting at API layer
- 🔒 Audit all dual-subject access attempts
Okta-Specific Notes:
- Custom Authorization Servers provide isolation and customization
- Expression language limited but sufficient for
actextraction - System Log provides detailed audit trail
- Okta API enables programmatic user/group management
- Consider Okta Workflows for complex orchestration
▁ ▂ ▂ ▃ ▃ ▄ ▄ ▅ ▅ ▆ ▆ Created with Aloha by Kahalewai - 2026 ▆ ▆ ▅ ▅ ▄ ▄ ▃ ▃ ▂ ▂ ▁