Skip to content

Commit 46aec02

Browse files
authored
Merge pull request #9 from PredicateSystems/oidc_bridge
oidc bridge
2 parents 3e30d33 + 855efa8 commit 46aec02

11 files changed

Lines changed: 688 additions & 1 deletion

File tree

.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,14 @@ ENTRA_OBO_COMPAT_CHECK_ENABLED=0
2424
ENTRA_SUPPORTS_OBO=0
2525
# Optional; required only when running true Entra OBO exchange path.
2626
ENTRA_USER_ASSERTION=
27+
28+
# Generic OIDC token exchange compatibility checks (examples/tests)
29+
OIDC_ISSUER=https://<oidc-provider>/oauth2/default
30+
OIDC_CLIENT_ID=<your-oidc-client-id>
31+
OIDC_CLIENT_SECRET=<your-oidc-client-secret>
32+
OIDC_AUDIENCE=api://predicate-authority
33+
OIDC_SCOPE=authority:check
34+
OIDC_COMPAT_CHECK_ENABLED=0
35+
OIDC_SUPPORTS_TOKEN_EXCHANGE=0
36+
# Optional; required only when testing true token exchange.
37+
OIDC_SUBJECT_TOKEN=

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ python examples/delegation/entra_obo_compat_demo.py \
142142
--scope "$ENTRA_SCOPE"
143143
```
144144

145+
### OIDC quick command (compatibility check)
146+
147+
```bash
148+
set -a && source .env && set +a
149+
python examples/delegation/oidc_compat_demo.py \
150+
--issuer "$OIDC_ISSUER" \
151+
--client-id "$OIDC_CLIENT_ID" \
152+
--client-secret "$OIDC_CLIENT_SECRET" \
153+
--audience "$OIDC_AUDIENCE" \
154+
--scope "${OIDC_SCOPE:-authority:check}"
155+
```
156+
145157
### Local IdP quick command
146158

147159
```bash

docs/authorityd-operations.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,50 @@ python examples/delegation/entra_obo_compat_demo.py \
267267
--supports-obo
268268
```
269269

270+
### Generic OIDC token exchange compatibility (capability-gated)
271+
272+
```bash
273+
export OIDC_COMPAT_CHECK_ENABLED=1
274+
export OIDC_ISSUER="https://<oidc-provider>/oauth2/default"
275+
export OIDC_CLIENT_ID="<oidc-client-id>"
276+
export OIDC_CLIENT_SECRET="<oidc-client-secret>"
277+
export OIDC_AUDIENCE="api://predicate-authority"
278+
export OIDC_SCOPE="authority:check"
279+
280+
# Provider does NOT support token exchange:
281+
export OIDC_SUPPORTS_TOKEN_EXCHANGE=false
282+
python3 -m pytest tests/test_oidc_compatibility.py -k "live_check_when_enabled"
283+
284+
# Provider supports token exchange:
285+
export OIDC_SUPPORTS_TOKEN_EXCHANGE=true
286+
export OIDC_SUBJECT_TOKEN="<subject-access-token>"
287+
python3 -m pytest tests/test_oidc_compatibility.py -k "live_check_when_enabled"
288+
```
289+
290+
Run demo script:
291+
292+
```bash
293+
python examples/delegation/oidc_compat_demo.py \
294+
--issuer "$OIDC_ISSUER" \
295+
--client-id "$OIDC_CLIENT_ID" \
296+
--client-secret "$OIDC_CLIENT_SECRET" \
297+
--audience "$OIDC_AUDIENCE" \
298+
--scope "${OIDC_SCOPE:-authority:check}"
299+
```
300+
301+
If token exchange is supported and subject token is available:
302+
303+
```bash
304+
python examples/delegation/oidc_compat_demo.py \
305+
--issuer "$OIDC_ISSUER" \
306+
--client-id "$OIDC_CLIENT_ID" \
307+
--client-secret "$OIDC_CLIENT_SECRET" \
308+
--audience "$OIDC_AUDIENCE" \
309+
--scope "${OIDC_SCOPE:-authority:check}" \
310+
--subject-token "$OIDC_SUBJECT_TOKEN" \
311+
--supports-token-exchange
312+
```
313+
270314
### Secret storage policy (Okta credentials)
271315

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

docs/predicate-authority-user-manual.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,59 @@ Expected delegation path output:
329329

330330
---
331331

332+
## Generic OIDC token exchange compatibility (capability-gated)
333+
334+
Use this when integrating with a non-Okta, non-Entra OIDC provider and validating token-exchange readiness.
335+
336+
### 1) Set environment variables
337+
338+
```bash
339+
export OIDC_ISSUER="https://<oidc-provider>/oauth2/default"
340+
export OIDC_CLIENT_ID="<oidc-client-id>"
341+
export OIDC_CLIENT_SECRET="<oidc-client-secret>"
342+
export OIDC_AUDIENCE="api://predicate-authority"
343+
export OIDC_SCOPE="authority:check"
344+
```
345+
346+
### 2) Run compatibility test
347+
348+
```bash
349+
# token exchange not supported or intentionally disabled:
350+
export OIDC_COMPAT_CHECK_ENABLED=1
351+
export OIDC_SUPPORTS_TOKEN_EXCHANGE=false
352+
python -m pytest tests/test_oidc_compatibility.py -k "live_check_when_enabled"
353+
354+
# token exchange supported:
355+
export OIDC_COMPAT_CHECK_ENABLED=1
356+
export OIDC_SUPPORTS_TOKEN_EXCHANGE=true
357+
export OIDC_SUBJECT_TOKEN="<subject-access-token>"
358+
python -m pytest tests/test_oidc_compatibility.py -k "live_check_when_enabled"
359+
```
360+
361+
### 3) Run demo script in `examples/`
362+
363+
```bash
364+
python examples/delegation/oidc_compat_demo.py \
365+
--issuer "$OIDC_ISSUER" \
366+
--client-id "$OIDC_CLIENT_ID" \
367+
--client-secret "$OIDC_CLIENT_SECRET" \
368+
--audience "$OIDC_AUDIENCE" \
369+
--scope "${OIDC_SCOPE:-authority:check}"
370+
```
371+
372+
If token exchange is supported and subject token is available, add:
373+
374+
```bash
375+
--subject-token "$OIDC_SUBJECT_TOKEN" --supports-token-exchange
376+
```
377+
378+
Expected delegation path output:
379+
380+
- `idp_token_exchange` (if exchange succeeds), or
381+
- `authority_mandate_delegation` (fallback).
382+
383+
---
384+
332385
## Local identity registry + flush queue
333386

334387
Enable ephemeral task identity registry and local ledger queue:

examples/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ For Okta OBO/token-exchange compatibility setup and troubleshooting, see:
4848
For Entra OBO compatibility setup and troubleshooting, see:
4949

5050
- `examples/README_Entra.md`
51+
52+
## OIDC compatibility example notes
53+
54+
For generic OIDC token-exchange compatibility setup and fallback behavior, see:
55+
56+
- `examples/README_OIDC.md`

examples/README_OIDC.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# OIDC Example Notes
2+
3+
This note covers running the generic OIDC token-exchange compatibility demo.
4+
5+
## Prerequisites
6+
7+
- Populate `AgentIdentity/.env` with:
8+
- `OIDC_ISSUER`
9+
- `OIDC_CLIENT_ID`
10+
- `OIDC_CLIENT_SECRET`
11+
- `OIDC_AUDIENCE`
12+
- `OIDC_SCOPE` (default `authority:check`)
13+
- Load env vars:
14+
15+
```bash
16+
set -a
17+
source .env
18+
set +a
19+
```
20+
21+
## Run OIDC compatibility demo
22+
23+
```bash
24+
python examples/delegation/oidc_compat_demo.py \
25+
--issuer "$OIDC_ISSUER" \
26+
--client-id "$OIDC_CLIENT_ID" \
27+
--client-secret "$OIDC_CLIENT_SECRET" \
28+
--audience "$OIDC_AUDIENCE" \
29+
--scope "${OIDC_SCOPE:-authority:check}"
30+
```
31+
32+
If provider supports token exchange and you have a subject token:
33+
34+
```bash
35+
python examples/delegation/oidc_compat_demo.py \
36+
--issuer "$OIDC_ISSUER" \
37+
--client-id "$OIDC_CLIENT_ID" \
38+
--client-secret "$OIDC_CLIENT_SECRET" \
39+
--audience "$OIDC_AUDIENCE" \
40+
--scope "${OIDC_SCOPE:-authority:check}" \
41+
--subject-token "$OIDC_SUBJECT_TOKEN" \
42+
--supports-token-exchange
43+
```
44+
45+
## Compatibility behavior
46+
47+
- token exchange success -> `delegation_path: idp_token_exchange`
48+
- token exchange unavailable or unsupported -> `delegation_path: authority_mandate_delegation`
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+
subject_token: str | None,
25+
timeout_s: float,
26+
) -> dict[str, object]:
27+
_ensure_repo_root_on_syspath()
28+
from predicate_authority import ( # pylint: disable=import-error
29+
OidcCompatibilityConfig,
30+
OidcProviderCapabilities,
31+
run_oidc_token_exchange_compatibility_check,
32+
)
33+
34+
result = run_oidc_token_exchange_compatibility_check(
35+
config=OidcCompatibilityConfig(
36+
issuer=issuer,
37+
client_id=client_id,
38+
client_secret=client_secret,
39+
audience=audience,
40+
scope=scope,
41+
),
42+
capabilities=OidcProviderCapabilities(supports_token_exchange=supports_token_exchange),
43+
subject_token=subject_token,
44+
timeout_s=timeout_s,
45+
)
46+
result["delegation_path"] = (
47+
"idp_token_exchange"
48+
if bool(result.get("token_exchange_ok", False))
49+
else "authority_mandate_delegation"
50+
)
51+
return result
52+
53+
54+
def main() -> None:
55+
parser = argparse.ArgumentParser(description="OIDC token exchange compatibility demo.")
56+
parser.add_argument("--issuer", default=os.getenv("OIDC_ISSUER"))
57+
parser.add_argument("--client-id", default=os.getenv("OIDC_CLIENT_ID"))
58+
parser.add_argument("--client-secret", default=os.getenv("OIDC_CLIENT_SECRET"))
59+
parser.add_argument("--audience", default=os.getenv("OIDC_AUDIENCE"))
60+
parser.add_argument("--scope", default=os.getenv("OIDC_SCOPE", "authority:check"))
61+
parser.add_argument("--subject-token", default=os.getenv("OIDC_SUBJECT_TOKEN"))
62+
parser.add_argument("--supports-token-exchange", action="store_true")
63+
parser.add_argument("--timeout-s", type=float, default=5.0)
64+
args = parser.parse_args()
65+
66+
missing = [
67+
name
68+
for name, value in (
69+
("issuer", args.issuer),
70+
("client_id", args.client_id),
71+
("client_secret", args.client_secret),
72+
("audience", args.audience),
73+
)
74+
if value is None or str(value).strip() == ""
75+
]
76+
if missing:
77+
raise SystemExit(f"Missing required arguments/env vars: {', '.join(missing)}")
78+
79+
payload = run(
80+
issuer=str(args.issuer),
81+
client_id=str(args.client_id),
82+
client_secret=str(args.client_secret),
83+
audience=str(args.audience),
84+
scope=str(args.scope),
85+
supports_token_exchange=bool(args.supports_token_exchange),
86+
subject_token=(str(args.subject_token) if args.subject_token is not None else None),
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/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,30 @@ python examples/delegation/entra_obo_compat_demo.py \
7272
--scope "${ENTRA_SCOPE:-api://predicate-authority/.default}"
7373
```
7474

75+
## OIDC compatibility demo (capability-gated token exchange)
76+
77+
```bash
78+
python examples/delegation/oidc_compat_demo.py \
79+
--issuer "$OIDC_ISSUER" \
80+
--client-id "$OIDC_CLIENT_ID" \
81+
--client-secret "$OIDC_CLIENT_SECRET" \
82+
--audience "$OIDC_AUDIENCE" \
83+
--scope "${OIDC_SCOPE:-authority:check}"
84+
```
85+
86+
If your provider supports token exchange and you have a subject token:
87+
88+
```bash
89+
python examples/delegation/oidc_compat_demo.py \
90+
--issuer "$OIDC_ISSUER" \
91+
--client-id "$OIDC_CLIENT_ID" \
92+
--client-secret "$OIDC_CLIENT_SECRET" \
93+
--audience "$OIDC_AUDIENCE" \
94+
--scope "${OIDC_SCOPE:-authority:check}" \
95+
--subject-token "$OIDC_SUBJECT_TOKEN" \
96+
--supports-token-exchange
97+
```
98+
7599
## Local IdP quick example
76100

77101
```python

predicate_authority/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
TaskIdentityRecord,
4040
)
4141
from predicate_authority.mandate import LocalMandateSigner
42+
from predicate_authority.oidc_compat import (
43+
OidcCompatibilityConfig,
44+
OidcCompatibilityError,
45+
OidcProviderCapabilities,
46+
run_oidc_token_exchange_compatibility_check,
47+
)
4248
from predicate_authority.okta_compat import (
4349
OktaCompatibilityConfig,
4450
OktaCompatibilityError,
@@ -98,6 +104,9 @@
98104
"OktaCompatibilityConfig",
99105
"OktaCompatibilityError",
100106
"OktaTenantCapabilities",
107+
"OidcCompatibilityConfig",
108+
"OidcCompatibilityError",
109+
"OidcProviderCapabilities",
101110
"PolicyEngine",
102111
"PolicyFileSource",
103112
"PolicyMatchResult",
@@ -114,6 +123,7 @@
114123
"TokenValidationError",
115124
"UsageCreditRecord",
116125
"parse_bool",
117-
"run_okta_obo_compatibility_check",
118126
"run_entra_obo_compatibility_check",
127+
"run_okta_obo_compatibility_check",
128+
"run_oidc_token_exchange_compatibility_check",
119129
]

0 commit comments

Comments
 (0)