Skip to content

Commit f92c742

Browse files
authored
Merge pull request #51 from luis5tb/scopes
feat: replace agent:insights scope with api.console and api.ocm
2 parents 5aa5f7b + 1e86281 commit f92c742

16 files changed

Lines changed: 150 additions & 94 deletions

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ RED_HAT_SSO_ISSUER=https://sso.redhat.com/auth/realms/redhat-external
2828
RED_HAT_SSO_CLIENT_ID=your_client_id
2929
RED_HAT_SSO_CLIENT_SECRET=your_client_secret
3030

31-
# Required scope for token introspection (default: agent:insights)
32-
AGENT_REQUIRED_SCOPE=agent:insights
31+
# Required scopes for token introspection (comma-separated, default: api.console,api.ocm)
32+
AGENT_REQUIRED_SCOPE=api.console,api.ocm
3333

3434
# -----------------------------------------------------------------------------
3535
# Red Hat Lightspeed MCP Server Configuration

deploy/cloudrun/README.md

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ Client Agent MCP Server console.red
614614
615615
The agent uses **Red Hat SSO** (Keycloak) for authentication via **token
616616
introspection** (RFC 7662). Requests to the A2A endpoint (POST /) require a
617-
Bearer token that is active and carries the `agent:insights` scope.
617+
Bearer token that is active and carries the `api.console` and `api.ocm` scopes.
618618
619619
### Authentication Flow
620620
@@ -637,7 +637,7 @@ Bearer token that is active and carries the `agent:insights` scope.
637637
│ Bearer token │ │ │ │
638638
├────────────────►│ 4. Introspect │ │ │
639639
│ │ token + check │ │ │
640-
│ │ agent:insights │ │ │
640+
│ │ required scopes │ │ │
641641
│ ├───────────────────►│ │ │
642642
│ │ │ │ │
643643
│ │ 5. MCP tool call │ │ │
@@ -666,7 +666,7 @@ Bearer token that is active and carries the `agent:insights` scope.
666666
| `redhat-sso-client-secret` | Resource Server client secret |
667667
| `MARKETPLACE_HANDLER_URL` | URL of the marketplace-handler service. Used to build the DCR endpoints in the AgentCard. If empty, falls back to `AGENT_PROVIDER_URL`. Set automatically by `deploy.sh`. |
668668
| `AGENT_PROVIDER_ORGANIZATION_URL` | Provider's organization website URL (default: `https://www.redhat.com`). Used in AgentCard `provider.url` and as the expected JWT audience for Google DCR `software_statement` validation. Set in YAML configs, not changed by `deploy.sh`. |
669-
| `AGENT_REQUIRED_SCOPE` | OAuth scope required in tokens (default: `agent:insights`) |
669+
| `AGENT_REQUIRED_SCOPE` | Comma-separated OAuth scopes required in tokens (default: `api.console,api.ocm`) |
670670
671671
### Development Mode
672672
@@ -1096,14 +1096,14 @@ The A2A protocol requires specific fields. A common mistake is omitting
10961096
}
10971097
```
10981098

1099-
**"Token is missing required scope: agent:insights"**
1099+
**"Token is missing required scope(s): api.console, api.ocm"**
11001100

1101-
The agent requires the `agent:insights` scope in the access token by
1102-
default. If your Red Hat SSO client is not configured to issue this scope,
1101+
The agent requires the `api.console` and `api.ocm` scopes in the access token
1102+
by default. If your Red Hat SSO client is not configured to issue these scopes,
11031103
you will see:
11041104

11051105
```json
1106-
{"jsonrpc":"2.0","error":{"code":-32003,"message":"Forbidden","data":{"detail":"Token is missing required scope: agent:insights"}},"id":null}
1106+
{"jsonrpc":"2.0","error":{"code":-32003,"message":"Forbidden","data":{"detail":"Token is missing required scope(s): api.console, api.ocm"}},"id":null}
11071107
```
11081108

11091109
To temporarily disable the scope check for testing, set `AGENT_REQUIRED_SCOPE`
@@ -1122,7 +1122,7 @@ To restore the scope requirement:
11221122
gcloud run services update ${SERVICE_NAME:-lightspeed-agent} \
11231123
--region=$GOOGLE_CLOUD_LOCATION \
11241124
--project=$GOOGLE_CLOUD_PROJECT \
1125-
--update-env-vars="AGENT_REQUIRED_SCOPE=agent:insights"
1125+
--update-env-vars="AGENT_REQUIRED_SCOPE=api.console,api.ocm"
11261126
```
11271127

11281128
This setting is also configurable in `service.yaml` via the
@@ -1406,13 +1406,13 @@ IAT=$(curl -s -X POST \
14061406
echo "Initial Access Token: $IAT"
14071407
```
14081408

1409-
#### 5. Create the `agent:insights` Scope and Resource Server Client
1409+
#### 5. Create the `api.console` and `api.ocm` Scopes and Resource Server Client
14101410

1411-
The agent validates tokens via introspection and checks for the `agent:insights`
1412-
scope. The scope must exist in the realm **before** DCR creates clients that
1413-
reference it.
1411+
The agent validates tokens via introspection and checks for the `api.console`
1412+
and `api.ocm` scopes. The scopes must exist in the realm **before** DCR
1413+
creates clients that reference them.
14141414

1415-
**Create the `agent:insights` client scope:**
1415+
**Create the `api.console` and `api.ocm` client scopes:**
14161416

14171417
```bash
14181418
# Get a fresh admin token (the one from step 4 expires after 60s)
@@ -1429,23 +1429,33 @@ curl -s -X POST "$KEYCLOAK_URL/admin/realms/test-realm/client-scopes" \
14291429
-H "Authorization: Bearer $ADMIN_TOKEN" \
14301430
-H "Content-Type: application/json" \
14311431
-d '{
1432-
"name": "agent:insights",
1432+
"name": "api.console",
14331433
"protocol": "openid-connect",
14341434
"attributes": {"include.in.token.scope": "true"}
14351435
}'
14361436

1437-
# Allow agent:insights in the DCR client registration policy.
1437+
# Create the api.ocm scope
1438+
curl -s -X POST "$KEYCLOAK_URL/admin/realms/test-realm/client-scopes" \
1439+
-H "Authorization: Bearer $ADMIN_TOKEN" \
1440+
-H "Content-Type: application/json" \
1441+
-d '{
1442+
"name": "api.ocm",
1443+
"protocol": "openid-connect",
1444+
"attributes": {"include.in.token.scope": "true"}
1445+
}'
1446+
1447+
# Allow api.console and api.ocm in the DCR client registration policy.
14381448
# Keycloak restricts which scopes can be requested during DCR.
14391449
# Get the "authenticated" Allowed Client Scopes policy and add
1440-
# agent:insights to it.
1450+
# api.console and api.ocm to it.
14411451
POLICY=$(curl -s \
14421452
"$KEYCLOAK_URL/admin/realms/test-realm/components?type=org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" \
14431453
-H "Authorization: Bearer $ADMIN_TOKEN" \
14441454
| python3 -c "
14451455
import sys, json
14461456
for p in json.load(sys.stdin):
14471457
if p.get('providerId') == 'allowed-client-templates' and p.get('subType') == 'authenticated':
1448-
p['config']['allowed-client-scopes'] = ['agent:insights']
1458+
p['config']['allowed-client-scopes'] = ['api.console', 'api.ocm']
14491459
print(json.dumps(p))
14501460
break
14511461
")
@@ -1487,13 +1497,23 @@ echo "RED_HAT_SSO_CLIENT_ID=lightspeed-agent"
14871497
echo "RED_HAT_SSO_CLIENT_SECRET=$CLIENT_SECRET"
14881498
```
14891499

1490-
**Assign `agent:insights` to the Resource Server client:**
1500+
**Assign `api.console` and `api.ocm` to the Resource Server client:**
14911501

14921502
```bash
14931503
# Get the scope UUID
14941504
SCOPE_UUID=$(curl -s "$KEYCLOAK_URL/admin/realms/test-realm/client-scopes" \
14951505
-H "Authorization: Bearer $ADMIN_TOKEN" \
1496-
| python3 -c "import sys,json; print([s['id'] for s in json.load(sys.stdin) if s['name']=='agent:insights'][0])")
1506+
| python3 -c "import sys,json; print([s['id'] for s in json.load(sys.stdin) if s['name']=='api.console'][0])")
1507+
1508+
# Add as optional scope to the client
1509+
curl -s -X PUT \
1510+
"$KEYCLOAK_URL/admin/realms/test-realm/clients/$CLIENT_UUID/optional-client-scopes/$SCOPE_UUID" \
1511+
-H "Authorization: Bearer $ADMIN_TOKEN"
1512+
1513+
# Get the api.ocm scope UUID
1514+
SCOPE_UUID=$(curl -s "$KEYCLOAK_URL/admin/realms/test-realm/client-scopes" \
1515+
-H "Authorization: Bearer $ADMIN_TOKEN" \
1516+
| python3 -c "import sys,json; print([s['id'] for s in json.load(sys.stdin) if s['name']=='api.ocm'][0])")
14971517

14981518
# Add as optional scope to the client
14991519
curl -s -X PUT \
@@ -1545,8 +1565,8 @@ echo -n "$CLIENT_SECRET" | \
15451565
--data-file=- --project=$GOOGLE_CLOUD_PROJECT
15461566
```
15471567

1548-
> **Note:** Keycloak assigns the `agent:insights` scope to DCR-created
1549-
> clients because the DCR request includes `"scope": "agent:insights"` and
1568+
> **Note:** Keycloak assigns the `api.console` and `api.ocm` scopes to DCR-created
1569+
> clients because the DCR request includes `"scope": "api.console api.ocm"` and
15501570
> the scope is in the Allowed Client Scopes registration policy (configured
15511571
> above). However, Keycloak does **not** enable `serviceAccountsEnabled`
15521572
> from the DCR `grant_types` field. After DCR, the agent automatically
@@ -1696,7 +1716,7 @@ python scripts/test_a2a_auth.py \
16961716
--message "What systems have critical advisories?"
16971717
```
16981718

1699-
The script requests `scope=openid agent:insights` via `client_credentials`
1719+
The script requests `scope=openid api.console api.ocm` via `client_credentials`
17001720
grant, then sends an A2A `message/send` request with the resulting Bearer token.
17011721

17021722
To just get a token (e.g. for pasting into the A2A Inspector):

deploy/cloudrun/service.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,10 @@ spec:
8888
# Red Hat SSO Configuration
8989
- name: RED_HAT_SSO_ISSUER
9090
value: "https://sso.redhat.com/auth/realms/redhat-external"
91-
# OAuth scope required in access tokens (checked via introspection).
91+
# Comma-separated OAuth scopes required in access tokens (checked via introspection).
9292
# Set to empty string to disable scope checking.
9393
- name: AGENT_REQUIRED_SCOPE
94-
value: ""
95-
# Replace with the required scopes if any is needed
96-
# value: "agent:insights"
94+
value: "api.console,api.ocm"
9795
# Ensure production environment do not skip JWT validation
9896
- name: SKIP_JWT_VALIDATION
9997
value: "false"

deploy/podman/lightspeed-agent-configmap.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ data:
1515

1616
# Red Hat SSO Configuration
1717
RED_HAT_SSO_ISSUER: "https://sso.redhat.com/auth/realms/redhat-external"
18-
AGENT_REQUIRED_SCOPE: "agent:insights"
18+
AGENT_REQUIRED_SCOPE: "api.console,api.ocm"
1919

2020
# MCP Server Configuration (for agent)
2121
MCP_TRANSPORT_MODE: "http"

docs/architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ The main AI agent FastAPI application, providing:
130130
Handles all authentication and authorization:
131131

132132
- **Token Introspection**: Validates tokens via Keycloak introspection endpoint (RFC 7662)
133-
- **Scope Checking**: Checks for required `agent:insights` scope
133+
- **Scope Checking**: Checks for required `api.console` and `api.ocm` scopes
134134
- **Bypass for Discovery**: `/.well-known/agent.json` is public per A2A spec
135135

136136
### Agent Core
@@ -222,7 +222,7 @@ acts purely as a Resource Server.
222222

223223
```
224224
1. Client authenticates directly with Red Hat SSO (e.g., client_credentials grant)
225-
2. Red Hat SSO issues access token with agent:insights scope
225+
2. Red Hat SSO issues access token with api.console and api.ocm scopes
226226
3. Client uses the token for A2A requests to the agent
227227
```
228228

@@ -336,7 +336,7 @@ src/lightspeed_agent/
336336

337337
- A2A query endpoints require valid Bearer token from Red Hat SSO
338338
- Tokens validated via Keycloak introspection endpoint (RFC 7662)
339-
- Required `agent:insights` scope checked; returns 403 if missing
339+
- Required `api.console` and `api.ocm` scopes checked; returns 403 if missing
340340

341341
### Public Endpoints
342342

docs/authentication-flow.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ Gemini Enterprise Agent (Marketplace Handler) Re
146146
- `grant_types`: `authorization_code`, `refresh_token`, `client_credentials`
147147
- `token_endpoint_auth_method`: `client_secret_basic`
148148
- `redirect_uris`: from the Google JWT claims
149-
- `scope`: `agent:insights`
149+
- `scope`: `api.console api.ocm`
150150
5. Enables service accounts on the new client via the Keycloak Admin API (for
151151
`client_credentials` grant support).
152152
6. Encrypts the `client_secret` (Fernet symmetric encryption) and stores the
@@ -368,7 +368,8 @@ Customer User Gemini Enterprise Red Hat SSO (Keycloak)
368368
| client_id=<ge_id> | | |
369369
| redirect_uri=<uri> | | |
370370
| scope=openid | | |
371-
| agent:insights | | |
371+
| api.console | | |
372+
| api.ocm | | |
372373
| state=<csrf_state> | | |
373374
| | | |
374375
|-- Follow redirect ----|----------------------------->| |
@@ -402,7 +403,7 @@ Customer User Gemini Enterprise Red Hat SSO (Keycloak)
402403
| | token_type: "Bearer", | |
403404
| | expires_in: 300, | |
404405
| | scope: "openid | |
405-
| | agent:insights" | |
406+
| | api.console api.ocm" | |
406407
| | } ------------------------| |
407408
| | | |
408409
| | [Access token obtained — user is authenticated] |
@@ -420,7 +421,7 @@ Customer User Gemini Enterprise Red Hat SSO (Keycloak)
420421
(created via DCR or provided as static credentials)
421422
- `redirect_uri` = Gemini Enterprise's callback URL (from the registration
422423
`redirect_uris`)
423-
- `scope` = `openid agent:insights`
424+
- `scope` = `openid api.console api.ocm`
424425
- `state` = CSRF protection token
425426
3. The user sees the Red Hat SSO login page and authenticates with their
426427
**Red Hat credentials** (username/password, or federated SSO).
@@ -433,7 +434,7 @@ Customer User Gemini Enterprise Red Hat SSO (Keycloak)
433434
- The `client_id` and `client_secret` (HTTP Basic or POST body)
434435
6. Red Hat SSO returns an **access token** (JWT), a refresh token, and token
435436
metadata. The access token contains the user's identity claims and the
436-
granted scopes (including `agent:insights`).
437+
granted scopes (including `api.console` and `api.ocm`).
437438

438439
**Key point:** The user authenticates with their Red Hat identity. The access
439440
token is issued by Red Hat SSO and represents both the user's identity and the
@@ -485,15 +486,16 @@ Gemini Enterprise Lightspeed Agent Red Hat
485486
| | "active": true, |
486487
| | "sub": "<user-id>", |
487488
| | "azp": "<ge-client-id>", |
488-
| | "scope": "openid agent:insights",|
489+
| | "scope": "openid api.console |
490+
| api.ocm", |
489491
| | "preferred_username": "jdoe", |
490492
| | "email": "jdoe@example.com", |
491493
| | "org_id": "<org-id>", |
492494
| | "exp": 1234567890 |
493495
| | } ---------------------------------|
494496
| | |
495497
| |-- Verify "active" == true |
496-
| |-- Verify "agent:insights" in scopes |
498+
| |-- Verify required scopes present |
497499
| | |
498500
| |-- Resolve order: |
499501
| | azp (ge-client-id) |
@@ -526,8 +528,9 @@ Gemini Enterprise Lightspeed Agent Red Hat
526528

527529
c. **Validates** the introspection response:
528530
- `active` must be `true` (token is not expired/revoked).
529-
- The `agent:insights` scope must be present in the token's scope list.
530-
If missing, the agent returns `403 Forbidden`.
531+
- The required scopes (`api.console` and `api.ocm`) must be present in
532+
the token's scope list. If any are missing, the agent returns
533+
`403 Forbidden`.
531534

532535
d. **Resolves the order**: Uses the `azp` (authorized party) claim from
533536
the introspection response — this is the Gemini Enterprise `client_id`
@@ -571,13 +574,13 @@ Gemini Enterprise Lightspeed Agent Red Hat
571574
| | |
572575
| |<-- { "active": true, |
573576
| | "scope": "openid" } -------------|
574-
| | (missing agent:insights scope) |
577+
| | (missing required scopes) |
575578
| | |
576579
|<-- 403 { code: -32003, | |
577580
| message: "Forbidden", | |
578581
| detail: "Token is missing | |
579-
| required scope: | |
580-
| agent:insights" } -------------| |
582+
| required scope(s): | |
583+
| api.console, api.ocm" } -------| |
581584
| | |
582585
| --- OR --- | |
583586
| | |
@@ -598,7 +601,7 @@ Gemini Enterprise Lightspeed Agent Red Hat
598601
| Token is expired or revoked (`active: false`) | 401 | -32001 | `Token is not active` |
599602
| Introspection endpoint returns non-200 | 401 | -32001 | `Introspection request failed (HTTP {status})` |
600603
| Network error calling introspection endpoint | 401 | -32001 | `HTTP error calling introspection endpoint: {error}` |
601-
| Token missing `agent:insights` scope | 403 | -32003 | `Token is missing required scope: agent:insights` |
604+
| Token missing required scope(s) | 403 | -32003 | `Token is missing required scope(s): api.console, api.ocm` |
602605
| `azp` client ID not found in credentials DB | 403 | -32003 | `No active order found for this client` |
603606
| Order ID not found in entitlements DB | 403 | -32003 | `No active order found for this client` |
604607
| Order state is not `ACTIVE` | 403 | -32003 | `No active order found for this client` |
@@ -712,8 +715,9 @@ independently. This mode preserves the user's identity end-to-end.
712715
- **Order-bound access**: Every authenticated request is tied to an active
713716
marketplace order. Cancelled or expired subscriptions are immediately
714717
rejected.
715-
- **Scope-based authorization**: The `agent:insights` scope must be present
716-
in the access token. Tokens without this scope receive `403 Forbidden`.
718+
- **Scope-based authorization**: The `api.console` and `api.ocm` scopes must
719+
be present in the access token. Tokens without these scopes receive
720+
`403 Forbidden`.
717721
- **Secrets encrypted at rest**: All client secrets stored in the database are
718722
encrypted with Fernet (symmetric AES-128-CBC with HMAC-SHA256).
719723
- **Token introspection (not local JWT verification)**: The agent validates

0 commit comments

Comments
 (0)