aws / aws/bedrock-agentcore-sdk-typescript
feat(identity): await onAuthUrl before polling in withAccessToken
- Dominant language
- TypeScript
- Stars
- 91
- Forks
- 31
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 5
Description
# Feature Request: Support async `onAuthUrl` callback in `withAccessToken`
**Is your feature request related to a problem? Please describe.**
When using `withAccessToken` with `USER_FEDERATION` (3LO) flow in a streaming agent, there's no clean way to send the OAuth authorization URL to the client before polling starts.
Currently, `onAuthUrl` fires synchronously and polling begins immediately:
```typescript
const fetchWithAuth = withAccessToken({
onAuthUrl: (url) => {
// This fires, then SDK immediately starts polling
// In an async generator, we can't "yield" from inside a callback
console.log('Auth URL:', url) // Only option is logging
},
})(myFunction)
```
In SSE streaming contexts (like `BedrockAgentCoreApp` handlers), we need to yield the auth URL to the client as an event. But we can't yield from inside a callback — by the time control returns to our async generator, polling has already started.
**Describe the solution you'd like**
Allow `onAuthUrl` to be an async function that the SDK awaits **before** starting to poll:
```typescript
const fetchWithAuth = withAccessToken({
providerName: 'google',
authFlow: 'USER_FEDERATION',
onAuthUrl: async (url) => {
// SDK awaits this BEFORE starting to poll
await sendAuthUrlToClient(url)
},
})(myFunction)
```
This gives developers control over when polling starts, ensuring the client receives the auth URL first.
**Proposed API**
```typescript
import { withAccessToken } from 'bedrock-agentcore/identity'
const fetchCalendarWithAuth = withAccessToken({
providerName: 'google-cal-provider',
scopes: ['https://www.googleapis.com/auth/calendar.readonly'],
authFlow: 'USER_FEDERATION',
callbackUrl: 'http://localhost:9090/oauth2/callback',
// NEW: SDK awaits this before polling starts
onAuthUrl: async (url) => {
// Developer can now do async work before polling begins
await emitToStream({ type: 'auth_url', url })
},
})(async (token) => {
return fetch('https://googleapis.com/calendar/v3/...', {
headers: { Authorization: `Bearer ${token}` },
})
})
```
**Describe alternatives you've considered**
We implemented a workaround using a signal pattern with `Promise.race`:
```typescript
function createAuthSignal() {
let resolve: ((url: string) => void) | null = null
let promise = new Promise((r) => { resolve = r })
return {
emit(url: string) {
resolve?.(url)
promise = new Promise((r) => { resolve = r })
},
wait() { return promise },
}
}
const authSignal = createAuthSignal()
// onAuthUrl emits to signal
onAuthUrl: (url) => authSignal.emit(url)
// Handler races between agent stream and auth signal
const result = await Promise.race([
stream.next().then((r) => ({ type: 'stream', ...r })),
authSignal.wait().then((url) => ({ type: 'auth', url })),
])
if (result.type === 'auth') {
yield { event: 'auth_url', data: { authUrl: result.url } }
}
```
This works but requires manual orchestration that could be avoided with async `onAuthUrl` support.
**Use Case**
Building agents with `BedrockAgentCoreApp` that access user resources via 3LO OAuth (Google Calendar, Slack, etc.) while streaming responses to the client.
The flow:
1. User asks "What's on my calendar?"
2. Agent calls tool → `withAccessToken` needs authorization
3. Auth URL should be **yielded to client immediately** so user can authorize
4. After user authorizes, polling completes and agent continues
Without async `onAuthUrl`, step 3 requires complex workarounds.
**Additional context**
Example from our Google Calendar sample showing the full streaming handler with workaround:
```typescript
// agent.ts
const authSignal = createAuthSignal()
const fetchCalendarEvents = withAccessToken({
providerName: PROVIDER_NAME,
authFlow: 'USER_FEDERATION',
onAuthUrl: (url) => authSignal.emit(url),
})(async (maxResults: number, token: string) => {
// fetch calendar data
})
const app = new BedrockAgentCoreApp({
invocationHandler: {
process: async function* (request, context) {
const stream = agent.stream(request.prompt)[Symbol.asyncIterator]()
while (true) {
const result = await Promise.race([
stream.next().then((r) => ({ type: 'stream' as const, ...r })),
authSignal.wait().then((url) => ({ type: 'auth' as const, url })),
])
if (result.type === 'auth') {
yield { event: 'auth_url', data: { authUrl: result.url } }
continue
}
if (result.done) break
// yield other events...
}
},
},
})
```
With async `onAuthUrl`, this simplifies significantly.
**Would you be willing to contribute this feature?**
- [ ] Yes, I would like to contribute this feature
- [x] No, I'm just suggesting the idea
Contributor guide
Research direction
Start by locating the withAccessToken entry point and its onAuthUrl handling for the USER_FEDERATION flow, then trace where polling begins. Done means an async callback can complete before polling starts without breaking the existing synchronous callback path; verify the behavior with the relevant identity-flow tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authentication
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100