Skip to content

Commit c1f9b95

Browse files
committed
feat: use OIDC discovery for token endpoint resolution
Resolve the token endpoint from the Redpanda Cloud OIDC discovery document instead of hardcoding it. Replace REDPANDA_TOKEN_ENDPOINT with REDPANDA_ISSUER and configure the accepted audience (cloudv2-production.redpanda.cloud).
1 parent b7568d2 commit c1f9b95

3 files changed

Lines changed: 39 additions & 25 deletions

File tree

ai/examples/langchain-docs-agent/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ REDPANDA_GATEWAY_ID=d6b3mk93mouc73cortj0
66
# Gateway URL
77
REDPANDA_GATEWAY_URL=https://ai-gateway.d6b2mdhdvf8ruqkbl2mg.clusters.rdpa.co
88

9-
# OIDC token endpoint (from IdP discovery: https://auth.prd.cloud.redpanda.com/.well-known/openid-configuration)
10-
# REDPANDA_TOKEN_ENDPOINT=https://auth.prd.cloud.redpanda.com/oauth/token
9+
# OIDC issuer (discovery URL is {issuer}/.well-known/openid-configuration)
10+
# REDPANDA_ISSUER=https://auth.prd.cloud.redpanda.com
1111

1212
# OIDC audience for the token request
1313
# REDPANDA_AUDIENCE=cloudv2-production.redpanda.cloud

ai/examples/langchain-docs-agent/README.md

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ poetry run redpanda-agent
4949
```
5050
[Python Agent (LangGraph)]
5151
|
52-
|-- OIDC client_credentials flow (authlib) --> Bearer token
52+
|-- OIDC client_credentials flow (authlib) --> Redpanda Cloud IdP --> Bearer token
5353
|
5454
|-- ChatOpenAI(base_url="https://ai-gateway.d6b2mdhdvf8ruqkbl2mg.clusters.rdpa.co/v1", ...)
5555
| |
@@ -118,28 +118,26 @@ llm = ChatOpenAI(
118118

119119
### OIDC authentication
120120

121-
The identity provider metadata is at:
122-
```
123-
https://auth.prd.cloud.redpanda.com/.well-known/openid-configuration
124-
```
121+
Authentication is against the **Redpanda Cloud OIDC identity provider**, not
122+
the gateway itself. The gateway validates the resulting tokens.
123+
124+
The `GatewayAuth` class uses **OIDC discovery** to resolve the token endpoint
125+
automatically from the issuer (`https://auth.prd.cloud.redpanda.com`):
125126

126-
The token endpoint (from the discovery document) is:
127127
```
128-
POST https://auth.prd.cloud.redpanda.com/oauth/token
128+
https://auth.prd.cloud.redpanda.com/.well-known/openid-configuration
129129
```
130130

131-
Use a plain `client_credentials` grant with no explicit scope:
131+
It fetches this discovery document on the first token request, then uses a
132+
`client_credentials` grant with the audience `cloudv2-production.redpanda.cloud`:
132133

133134
```python
134-
token_response = await client.fetch_token(
135-
url=token_endpoint,
136-
grant_type="client_credentials",
137-
)
135+
auth = GatewayAuth() # uses REDPANDA_ISSUER env var or default
136+
token = await auth.get_token()
138137
```
139138

140-
> **Note:** The required scopes depend on the IdP and client configuration.
141-
> Some gateway deployments may require `scope=openid` — check your IdP's
142-
> discovery document and client settings if you get `access_denied` errors.
139+
> **Note:** The AI agent is responsible for refreshing tokens before they expire.
140+
> `GatewayAuth` handles this automatically with a 30-second buffer.
143141
144142
### Every request needs two headers
145143

ai/examples/langchain-docs-agent/src/agent/auth.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@
33
import os
44
import time
55

6+
import httpx
67
from authlib.integrations.httpx_client import AsyncOAuth2Client
78

89

910
class GatewayAuth:
1011
"""Manages OIDC client_credentials tokens for the Redpanda AI Gateway."""
1112

12-
DEFAULT_TOKEN_ENDPOINT = "https://auth.prd.cloud.redpanda.com/oauth/token"
13+
DEFAULT_ISSUER = "https://auth.prd.cloud.redpanda.com"
14+
DEFAULT_AUDIENCE = "cloudv2-production.redpanda.cloud"
1315

1416
def __init__(
1517
self,
1618
client_id: str | None = None,
1719
client_secret: str | None = None,
1820
gateway_url: str | None = None,
1921
gateway_id: str | None = None,
20-
token_endpoint: str | None = None,
22+
issuer: str | None = None,
2123
audience: str | None = None,
2224
) -> None:
2325
self._gateway_url = (
@@ -30,33 +32,47 @@ def __init__(
3032
self._gateway_id = (
3133
gateway_id or os.environ.get("REDPANDA_GATEWAY_ID", "d6b3mk93mouc73cortj0")
3234
)
33-
self._token_endpoint = (
34-
token_endpoint
35-
or os.environ.get("REDPANDA_TOKEN_ENDPOINT", self.DEFAULT_TOKEN_ENDPOINT)
36-
)
35+
self._issuer = (
36+
issuer
37+
or os.environ.get("REDPANDA_ISSUER", self.DEFAULT_ISSUER)
38+
).rstrip("/")
3739
self._audience = (
3840
audience
39-
or os.environ.get("REDPANDA_AUDIENCE", "cloudv2-production.redpanda.cloud")
41+
or os.environ.get("REDPANDA_AUDIENCE", self.DEFAULT_AUDIENCE)
4042
)
4143
self._client = AsyncOAuth2Client(
4244
client_id=client_id or os.environ["REDPANDA_CLIENT_ID"],
4345
client_secret=client_secret or os.environ["REDPANDA_CLIENT_SECRET"],
4446
)
4547
self._token: str = ""
4648
self._expires_at: float = 0.0
49+
self._token_endpoint: str | None = None
4750

4851
@property
4952
def gateway_url(self) -> str:
5053
"""The resolved gateway base URL."""
5154
return self._gateway_url
5255

56+
async def _discover_token_endpoint(self) -> str:
57+
"""Fetch the token endpoint from the OIDC discovery document."""
58+
if self._token_endpoint:
59+
return self._token_endpoint
60+
61+
discovery_url = f"{self._issuer}/.well-known/openid-configuration"
62+
async with httpx.AsyncClient() as http:
63+
resp = await http.get(discovery_url)
64+
resp.raise_for_status()
65+
self._token_endpoint = resp.json()["token_endpoint"]
66+
return self._token_endpoint
67+
5368
async def get_token(self) -> str:
5469
"""Return a valid Bearer token, refreshing if expired."""
5570
if self._token and time.time() < self._expires_at - 30:
5671
return self._token
5772

73+
token_endpoint = await self._discover_token_endpoint()
5874
token_response = await self._client.fetch_token(
59-
url=self._token_endpoint,
75+
url=token_endpoint,
6076
grant_type="client_credentials",
6177
audience=self._audience,
6278
)

0 commit comments

Comments
 (0)