Skip to content

Commit e00d70e

Browse files
committed
test okta delegation
1 parent ce69bac commit e00d70e

7 files changed

Lines changed: 559 additions & 0 deletions

File tree

.env.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# AgentIdentity Okta compatibility checks (examples/tests)
2+
3+
# Core Okta app settings used by OBO compatibility demo/test.
4+
OKTA_ISSUER=https://<your-okta-domain>/oauth2/default
5+
OKTA_CLIENT_ID=<your-okta-client-id>
6+
OKTA_CLIENT_SECRET=<your-okta-client-secret>
7+
OKTA_AUDIENCE=api://predicate-authority
8+
OKTA_SCOPE=authority:check
9+
10+
# Enable live compatibility check test (disabled by default).
11+
OKTA_OBO_COMPAT_CHECK_ENABLED=0
12+
13+
# Set to 1/true only if your Okta tenant supports token exchange/OBO.
14+
OKTA_SUPPORTS_TOKEN_EXCHANGE=0

docs/authorityd-operations.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,45 @@ Checkpoints:
191191
- validation error includes a reason category (e.g. issuer mismatch),
192192
- error text does not include raw token string or sensitive claim values.
193193

194+
4) Okta token exchange/OBO compatibility (tenant capability-gated):
195+
196+
```bash
197+
# If tenant supports token exchange:
198+
export OKTA_OBO_COMPAT_CHECK_ENABLED=1
199+
export OKTA_SUPPORTS_TOKEN_EXCHANGE=true
200+
python3 -m pytest tests/test_okta_obo_compatibility.py -k "live_check_when_enabled"
201+
202+
# If tenant does NOT support token exchange:
203+
export OKTA_OBO_COMPAT_CHECK_ENABLED=1
204+
export OKTA_SUPPORTS_TOKEN_EXCHANGE=false
205+
python3 -m pytest tests/test_okta_obo_compatibility.py -k "live_check_when_enabled"
206+
```
207+
208+
Checkpoints:
209+
210+
- `client_credentials_ok` must pass in both modes,
211+
- when `OKTA_SUPPORTS_TOKEN_EXCHANGE=true`, token exchange must succeed,
212+
- when `OKTA_SUPPORTS_TOKEN_EXCHANGE=false`, token exchange path is explicitly gated as tenant-disabled (no false failure).
213+
214+
### Example demo script: Okta delegation compatibility
215+
216+
Run example from repo root:
217+
218+
```bash
219+
python3 examples/delegation/okta_obo_compat_demo.py \
220+
--issuer "$OKTA_ISSUER" \
221+
--client-id "$OKTA_CLIENT_ID" \
222+
--client-secret "$OKTA_CLIENT_SECRET" \
223+
--audience "$OKTA_AUDIENCE" \
224+
--scope "${OKTA_SCOPE:-authority:check}" \
225+
--supports-token-exchange
226+
```
227+
228+
Notes:
229+
230+
- omit `--supports-token-exchange` for tenants that do not support OBO/token exchange,
231+
- script reports whether delegation path should use IdP token exchange or authority mandate delegation.
232+
194233
### Secret storage policy (Okta credentials)
195234

196235
- never commit Okta client secrets/API tokens/private keys to repo files,

docs/predicate-authority-user-manual.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,65 @@ predicate-authorityd \
219219

220220
---
221221

222+
## Okta delegation compatibility check (capability-gated)
223+
224+
Use this when you want to verify whether your Okta tenant can do IdP token
225+
exchange/OBO for delegation, or if you should use authority mandate delegation
226+
as the fallback path.
227+
228+
### 1) Set environment variables
229+
230+
```bash
231+
cp .env.example .env
232+
233+
export OKTA_ISSUER="https://<org>.okta.com/oauth2/default"
234+
export OKTA_CLIENT_ID="<okta-client-id>"
235+
export OKTA_CLIENT_SECRET="<okta-client-secret>"
236+
export OKTA_AUDIENCE="api://predicate-authority"
237+
export OKTA_SCOPE="authority:check"
238+
```
239+
240+
### 2) Run compatibility test (live check is opt-in)
241+
242+
```bash
243+
# Tenant supports token exchange/OBO
244+
export OKTA_OBO_COMPAT_CHECK_ENABLED=1
245+
export OKTA_SUPPORTS_TOKEN_EXCHANGE=true
246+
python -m pytest tests/test_okta_obo_compatibility.py -k "live_check_when_enabled"
247+
248+
# Tenant does NOT support token exchange/OBO
249+
export OKTA_OBO_COMPAT_CHECK_ENABLED=1
250+
export OKTA_SUPPORTS_TOKEN_EXCHANGE=false
251+
python -m pytest tests/test_okta_obo_compatibility.py -k "live_check_when_enabled"
252+
```
253+
254+
Expected behavior:
255+
256+
- `client_credentials` path succeeds in both modes.
257+
- if `OKTA_SUPPORTS_TOKEN_EXCHANGE=true`, token exchange should succeed.
258+
- if `OKTA_SUPPORTS_TOKEN_EXCHANGE=false`, test is explicitly gated and does not
259+
fail as a false negative.
260+
261+
### 3) Run demo script in `examples/`
262+
263+
```bash
264+
python examples/delegation/okta_obo_compat_demo.py \
265+
--issuer "$OKTA_ISSUER" \
266+
--client-id "$OKTA_CLIENT_ID" \
267+
--client-secret "$OKTA_CLIENT_SECRET" \
268+
--audience "$OKTA_AUDIENCE" \
269+
--scope "${OKTA_SCOPE:-authority:check}" \
270+
--supports-token-exchange
271+
```
272+
273+
If your tenant does not support token exchange, omit
274+
`--supports-token-exchange`. The script reports which delegation path to use:
275+
276+
- `idp_token_exchange` (when supported), or
277+
- `authority_mandate_delegation` (fallback).
278+
279+
---
280+
222281
## Local identity registry + flush queue
223282

224283
Enable ephemeral task identity registry and local ledger queue:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import json
5+
import os
6+
import sys
7+
from pathlib import Path
8+
9+
10+
def _ensure_repo_root_on_syspath() -> None:
11+
repo_root = Path(__file__).resolve().parents[2]
12+
root = str(repo_root)
13+
if root not in sys.path:
14+
sys.path.insert(0, root)
15+
16+
17+
def run(
18+
issuer: str,
19+
client_id: str,
20+
client_secret: str,
21+
audience: str,
22+
scope: str,
23+
supports_token_exchange: bool,
24+
timeout_s: float,
25+
) -> dict[str, object]:
26+
_ensure_repo_root_on_syspath()
27+
from predicate_authority import ( # pylint: disable=import-error
28+
OktaCompatibilityConfig,
29+
OktaTenantCapabilities,
30+
run_okta_obo_compatibility_check,
31+
)
32+
33+
result = run_okta_obo_compatibility_check(
34+
config=OktaCompatibilityConfig(
35+
issuer=issuer,
36+
client_id=client_id,
37+
client_secret=client_secret,
38+
audience=audience,
39+
scope=scope,
40+
),
41+
capabilities=OktaTenantCapabilities(supports_token_exchange=supports_token_exchange),
42+
timeout_s=timeout_s,
43+
)
44+
result["delegation_path"] = (
45+
"idp_token_exchange"
46+
if bool(result.get("token_exchange_ok", False))
47+
else "authority_mandate_delegation"
48+
)
49+
return result
50+
51+
52+
def main() -> None:
53+
parser = argparse.ArgumentParser(description="Okta OBO compatibility demo for delegation flow.")
54+
parser.add_argument("--issuer", default=os.getenv("OKTA_ISSUER"))
55+
parser.add_argument("--client-id", default=os.getenv("OKTA_CLIENT_ID"))
56+
parser.add_argument("--client-secret", default=os.getenv("OKTA_CLIENT_SECRET"))
57+
parser.add_argument("--audience", default=os.getenv("OKTA_AUDIENCE"))
58+
parser.add_argument("--scope", default=os.getenv("OKTA_SCOPE", "authority:check"))
59+
parser.add_argument(
60+
"--supports-token-exchange",
61+
action="store_true",
62+
help="Set if this Okta tenant is expected to support token exchange/OBO.",
63+
)
64+
parser.add_argument("--timeout-s", type=float, default=5.0)
65+
args = parser.parse_args()
66+
67+
missing = [
68+
name
69+
for name, value in (
70+
("issuer", args.issuer),
71+
("client_id", args.client_id),
72+
("client_secret", args.client_secret),
73+
("audience", args.audience),
74+
)
75+
if value is None or str(value).strip() == ""
76+
]
77+
if missing:
78+
raise SystemExit(f"Missing required arguments/env vars: {', '.join(missing)}")
79+
80+
payload = run(
81+
issuer=str(args.issuer),
82+
client_id=str(args.client_id),
83+
client_secret=str(args.client_secret),
84+
audience=str(args.audience),
85+
scope=str(args.scope),
86+
supports_token_exchange=bool(args.supports_token_exchange),
87+
timeout_s=float(args.timeout_s),
88+
)
89+
print(json.dumps(payload, indent=2, sort_keys=True))
90+
91+
92+
if __name__ == "__main__":
93+
main()

predicate_authority/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
TaskIdentityRecord,
3434
)
3535
from predicate_authority.mandate import LocalMandateSigner
36+
from predicate_authority.okta_compat import (
37+
OktaCompatibilityConfig,
38+
OktaCompatibilityError,
39+
OktaTenantCapabilities,
40+
parse_bool,
41+
run_okta_obo_compatibility_check,
42+
)
3643
from predicate_authority.policy import PolicyEngine, PolicyMatchResult
3744
from predicate_authority.policy_source import PolicyFileSource, PolicyReloadResult
3845
from predicate_authority.proof import InMemoryProofLedger
@@ -79,6 +86,9 @@
7986
"OktaIdentityBridge",
8087
"OktaTokenClaims",
8188
"OpenTelemetryTraceEmitter",
89+
"OktaCompatibilityConfig",
90+
"OktaCompatibilityError",
91+
"OktaTenantCapabilities",
8292
"PolicyEngine",
8393
"PolicyFileSource",
8494
"PolicyMatchResult",
@@ -94,4 +104,6 @@
94104
"TaskIdentityRecord",
95105
"TokenValidationError",
96106
"UsageCreditRecord",
107+
"parse_bool",
108+
"run_okta_obo_compatibility_check",
97109
]

0 commit comments

Comments
 (0)