Skip to content

Commit e2f1e94

Browse files
authored
Merge pull request #57 from luis5tb/max-scopes
feat: enforce OAuth scope allowlist on access tokens
2 parents 26d5f56 + 00a4eb9 commit e2f1e94

14 files changed

Lines changed: 156 additions & 2 deletions

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ RED_HAT_SSO_CLIENT_SECRET=your_client_secret
3131
# Required scopes for token introspection (comma-separated, default: api.console,api.ocm)
3232
AGENT_REQUIRED_SCOPE=api.console,api.ocm
3333

34+
# Allowed scopes allowlist (comma-separated, default: openid,profile,email,api.console,api.ocm)
35+
# Tokens carrying scopes outside this list are rejected (HTTP 403).
36+
AGENT_ALLOWED_SCOPES=openid,profile,email,api.console,api.ocm
37+
3438
# -----------------------------------------------------------------------------
3539
# Red Hat Lightspeed MCP Server Configuration
3640
# -----------------------------------------------------------------------------

deploy/cloudrun/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ Bearer token that is active and carries the `api.console` and `api.ocm` scopes.
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`. |
669669
| `AGENT_REQUIRED_SCOPE` | Comma-separated OAuth scopes required in tokens (default: `api.console,api.ocm`) |
670+
| `AGENT_ALLOWED_SCOPES` | Comma-separated allowlist of permitted scopes (default: `openid,profile,email,api.console,api.ocm`). Tokens with scopes outside this list are rejected (403). |
670671
671672
### Development Mode
672673
@@ -1128,6 +1129,24 @@ gcloud run services update ${SERVICE_NAME:-lightspeed-agent} \
11281129
This setting is also configurable in `service.yaml` via the
11291130
`AGENT_REQUIRED_SCOPE` environment variable.
11301131

1132+
**"Token carries disallowed scope(s): ..."**
1133+
1134+
The agent enforces a scope allowlist (`AGENT_ALLOWED_SCOPES`) to prevent tokens
1135+
with elevated privileges from being forwarded to downstream services. If the
1136+
token carries scopes not in the allowlist, you will see a 403 error.
1137+
1138+
Add the missing scope(s) to the allowlist:
1139+
1140+
```bash
1141+
gcloud run services update ${SERVICE_NAME:-lightspeed-agent} \
1142+
--region=$GOOGLE_CLOUD_LOCATION \
1143+
--project=$GOOGLE_CLOUD_PROJECT \
1144+
--update-env-vars="AGENT_ALLOWED_SCOPES=openid,profile,email,api.console,api.ocm,your.extra.scope"
1145+
```
1146+
1147+
This setting is also configurable in `service.yaml` via the
1148+
`AGENT_ALLOWED_SCOPES` environment variable.
1149+
11311150
**Empty response or connection refused**
11321151

11331152
- Ensure the proxy is running in a separate terminal

deploy/cloudrun/service.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ spec:
9494
# Set to empty string to disable scope checking.
9595
- name: AGENT_REQUIRED_SCOPE
9696
value: "api.console,api.ocm"
97+
# Comma-separated allowlist of OAuth scopes permitted in access tokens.
98+
# Tokens carrying scopes outside this list are rejected (HTTP 403).
99+
- name: AGENT_ALLOWED_SCOPES
100+
value: "openid,profile,email,api.console,api.ocm"
97101
# Ensure production environment do not skip JWT validation
98102
- name: SKIP_JWT_VALIDATION
99103
value: "false"

deploy/podman/lightspeed-agent-configmap.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ data:
1616
# Red Hat SSO Configuration
1717
RED_HAT_SSO_ISSUER: "https://sso.redhat.com/auth/realms/redhat-external"
1818
AGENT_REQUIRED_SCOPE: "api.console,api.ocm"
19+
AGENT_ALLOWED_SCOPES: "openid,profile,email,api.console,api.ocm"
1920

2021
# MCP Server Configuration (for agent)
2122
MCP_TRANSPORT_MODE: "http"

docs/architecture.md

Lines changed: 2 additions & 2 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 `api.console` and `api.ocm` scopes
133+
- **Scope Checking**: Checks for required `api.console` and `api.ocm` scopes; rejects tokens carrying scopes outside the configured allowlist
134134
- **Bypass for Discovery**: `/.well-known/agent.json` is public per A2A spec
135135

136136
### Agent Core
@@ -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 `api.console` and `api.ocm` scopes checked; returns 403 if missing
339+
- Required `api.console` and `api.ocm` scopes checked; returns 403 if missing or if token carries disallowed scopes
340340

341341
### Public Endpoints
342342

docs/authentication-flow.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ Gemini Enterprise Lightspeed Agent Red Hat
496496
| | |
497497
| |-- Verify "active" == true |
498498
| |-- Verify required scopes present |
499+
| |-- Verify no disallowed scopes |
499500
| | |
500501
| |-- Resolve order: |
501502
| | azp (ge-client-id) |
@@ -584,6 +585,19 @@ Gemini Enterprise Lightspeed Agent Red Hat
584585
| | |
585586
| --- OR --- | |
586587
| | |
588+
| |<-- { "active": true, |
589+
| | "scope": "openid api.console |
590+
| | api.ocm admin.super" } --------|
591+
| | (disallowed scopes present) |
592+
| | |
593+
|<-- 403 { code: -32003, | |
594+
| message: "Forbidden", | |
595+
| detail: "Token carries | |
596+
| disallowed scope(s): | |
597+
| admin.super" } ---------------| |
598+
| | |
599+
| --- OR --- | |
600+
| | |
587601
| |-- Look up azp → order_id |
588602
| | FAIL: client not in credentials DB |
589603
| | or order not ACTIVE |
@@ -602,6 +616,7 @@ Gemini Enterprise Lightspeed Agent Red Hat
602616
| Introspection endpoint returns non-200 | 401 | -32001 | `Introspection request failed (HTTP {status})` |
603617
| Network error calling introspection endpoint | 401 | -32001 | `HTTP error calling introspection endpoint: {error}` |
604618
| Token missing required scope(s) | 403 | -32003 | `Token is missing required scope(s): api.console, api.ocm` |
619+
| Token carries disallowed scope(s) | 403 | -32003 | `Token carries disallowed scope(s): <scope_name>` |
605620
| `azp` client ID not found in credentials DB | 403 | -32003 | `No active order found for this client` |
606621
| Order ID not found in entitlements DB | 403 | -32003 | `No active order found for this client` |
607622
| Order state is not `ACTIVE` | 403 | -32003 | `No active order found for this client` |

docs/authentication.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ mismatch issues when tokens are issued by DCR-created clients (each has its own
196196
5. **Check Scope**: Agent checks that the required scopes (`api.console` and
197197
`api.ocm`) are present in the token's `scope` field. If any are missing,
198198
the agent returns **403 Forbidden**.
199+
6. **Check Allowed Scopes**: Agent checks that the token does not carry scopes
200+
outside the configured allowlist (`AGENT_ALLOWED_SCOPES`). If disallowed
201+
scopes are present, the agent returns **403 Forbidden**. This prevents
202+
tokens with elevated privileges from being forwarded to downstream services.
199203
6. **Build User**: Agent maps the introspection response to an
200204
`AuthenticatedUser` for downstream use.
201205

@@ -223,6 +227,19 @@ must be:
223227
The required scopes are configurable via `AGENT_REQUIRED_SCOPE` (comma-separated,
224228
default: `api.console,api.ocm`).
225229

230+
### Allowed Scopes (Scope Allowlist)
231+
232+
In addition to checking that required scopes are present, the agent enforces an
233+
**allowlist** of permitted scopes via `AGENT_ALLOWED_SCOPES` (comma-separated,
234+
default: `openid,profile,email,api.console,api.ocm`). Tokens carrying scopes
235+
outside this list are rejected with **403 Forbidden**.
236+
237+
This is a defense-in-depth measure: since the agent forwards the caller's JWT
238+
to the MCP sidecar and downstream APIs, restricting scopes prevents tokens with
239+
elevated privileges from being exercised against those services.
240+
241+
All permitted scopes must be explicitly listed in `AGENT_ALLOWED_SCOPES`.
242+
226243
### Introspection Response Fields
227244

228245
The agent extracts the following fields from the introspection response:
@@ -287,6 +304,10 @@ RED_HAT_SSO_CLIENT_SECRET=your-client-secret
287304

288305
# Required scopes checked during token introspection (comma-separated, default: api.console,api.ocm)
289306
AGENT_REQUIRED_SCOPE=api.console,api.ocm
307+
308+
# Allowed scopes allowlist (comma-separated, default: openid,profile,email,api.console,api.ocm)
309+
# Tokens carrying scopes outside this list are rejected (HTTP 403).
310+
AGENT_ALLOWED_SCOPES=openid,profile,email,api.console,api.ocm
290311
```
291312

292313
### Registering an OAuth Client
@@ -517,6 +538,7 @@ pytest tests/test_auth.py --cov=lightspeed_agent.auth
517538
| 401 | Token not active | Introspection returned `active: false` (expired, revoked, or invalid) |
518539
| 401 | Introspection failed | HTTP error calling introspection endpoint |
519540
| 403 | Insufficient scope | Token is active but missing required scope(s) (`api.console`, `api.ocm`) |
541+
| 403 | Disallowed scope | Token carries scope(s) outside the configured allowlist (`AGENT_ALLOWED_SCOPES`) |
520542

521543
### Error Response Format
522544

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ GOOGLE_CLOUD_LOCATION=us-central1
3939
| `RED_HAT_SSO_CLIENT_ID` | - | Resource Server client ID (used for token introspection) |
4040
| `RED_HAT_SSO_CLIENT_SECRET` | - | Resource Server client secret |
4141
| `AGENT_REQUIRED_SCOPE` | `api.console,api.ocm` | Comma-separated OAuth scopes required in access tokens |
42+
| `AGENT_ALLOWED_SCOPES` | `openid,profile,email,api.console,api.ocm` | Comma-separated allowlist of permitted scopes. Tokens with scopes outside this list are rejected (403). |
4243

4344
**Example:**
4445

docs/troubleshooting.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .scope
131131

132132
**Fix**: Ensure the `api.console` and `api.ocm` Client Scopes exist in Keycloak and are assigned to the client that issued the token.
133133

134+
### Disallowed Scope (403 Forbidden)
135+
136+
**Symptom**: `Token carries disallowed scope(s): <scope_name>`
137+
138+
The token contains scopes outside the `AGENT_ALLOWED_SCOPES` allowlist.
139+
140+
**Fix**: Either add the scope to `AGENT_ALLOWED_SCOPES` or request a token without the extra scopes. All permitted scopes must be explicitly listed.
141+
134142
### OAuth Callback Errors
135143

136144
**Symptom**: Callback fails with error

src/lightspeed_agent/auth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require_scope,
1212
)
1313
from lightspeed_agent.auth.introspection import (
14+
DisallowedScopeError,
1415
InsufficientScopeError,
1516
TokenIntrospector,
1617
TokenValidationError,
@@ -31,6 +32,7 @@
3132
"TokenIntrospector",
3233
"TokenValidationError",
3334
"InsufficientScopeError",
35+
"DisallowedScopeError",
3436
"get_token_introspector",
3537
# Middleware
3638
"AuthenticationMiddleware",

0 commit comments

Comments
 (0)