aws / aws/bedrock-agentcore-sdk-typescript

feat(identity): await onAuthUrl before polling in withAccessToken

Open
#49 1 comment 0 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.