Skip to content

feat(identity): await onAuthUrl before polling in withAccessToken #49

@niklas-palm

Description

@niklas-palm

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:

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:

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

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:

function createAuthSignal() {
  let resolve: ((url: string) => void) | null = null
  let promise = new Promise<string>((r) => { resolve = r })

  return {
    emit(url: string) {
      resolve?.(url)
      promise = new Promise<string>((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:

// 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
  • No, I'm just suggesting the idea

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions