Skip to content

[BUG] CompleteResourceTokenAuth fails with 'Invalid or expired session' when using Gateway 3LO (AUTHORIZATION_CODE) #37

@rpostulart

Description

@rpostulart

Description

When using Amazon Bedrock AgentCore Gateway with 3-legged OAuth (AUTHORIZATION_CODE grant type) for Microsoft Graph integration, the CompleteResourceTokenAuth API call consistently fails with AccessDeniedException: Invalid or expired session, even though the OAuth flow completes successfully from the user's perspective.

Environment

  • Region: eu-central-1
  • AgentCore Gateway with MCP target
  • Credential Provider: CustomOauth2 (Microsoft Entra ID)
  • Grant Type: AUTHORIZATION_CODE
  • AWS SDK: @aws-sdk/client-bedrock-agentcore (latest)

Configuration

Gateway Target:

{
  "grantType": "AUTHORIZATION_CODE",
  "defaultReturnUrl": "https://[api-gateway].execute-api.eu-central-1.amazonaws.com/oauth/callback",
  "providerArn": "arn:aws:bedrock-agentcore:eu-central-1:[account]:token-vault/default/oauth2credentialprovider/microsoft-oauth-acc"
}

Credential Provider:

{
  "credentialProviderVendor": "CustomOauth2",
  "callbackUrl": "https://bedrock-agentcore.eu-central-1.amazonaws.com/identities/oauth2/callback/[uuid]",
  "oauth2ProviderConfigOutput": {
    "customOauth2ProviderConfig": {
      "oauthDiscovery": {
        "discoveryUrl": "https://login.microsoftonline.com/[tenant]/v2.0/.well-known/openid-configuration"
      }
    }
  }
}

Workload Identity:

{
  "allowedResourceOauth2ReturnUrls": ["https://[api-gateway].execute-api.eu-central-1.amazonaws.com/oauth/callback"]
}

Steps to Reproduce

  1. Call Gateway with X-Amzn-Bedrock-AgentCore-Runtime-User-Id: user@example.com header
  2. Gateway returns elicitation URL with request_uri
  3. Store the request_uriuserId mapping in DynamoDB
  4. Redirect user to AgentCore authorization URL
  5. User completes Microsoft login and grants consent
  6. AgentCore redirects to defaultReturnUrl with session_id query parameter
  7. Callback Lambda calls CompleteResourceTokenAuth:
const command = new BedrockAgentCore.CompleteResourceTokenAuthCommand({
  sessionUri: params.sessionId,  // e.g., "urn:ietf:params:oauth:request_uri:Y2M4M2QyNDkt..."
  userIdentifier: { userId: "user@example.com" },  // Same as sent in step 1
});
await agentCoreClient.send(command);

Expected Behavior

CompleteResourceTokenAuth should succeed, and the OAuth token should be stored in Token Vault, allowing subsequent Gateway calls to succeed without re-authentication.

Actual Behavior

{
  "Error": {
    "Message": "Invalid or expired session",
    "Code": "AccessDeniedException"
  },
  "ResponseMetadata": {
    "HTTPStatusCode": 403
  }
}

This error occurs immediately (within 2-3 seconds of session creation), so it's not a TTL expiration issue.

After the error, calling the Gateway with the same user ID still returns an elicitation URL, indicating the token was NOT stored.

Debug Information

OAuth Start Lambda (session creation):

{
  "message": "Replaying request to Gateway",
  "userId": "xxx@xxxx.nl",
  "userIdLength": 23,
  "userIdBytes": "722e706f7374756c6172744063617265626561742e6e6c",
  "headerValue": "X-Amzn-Bedrock-AgentCore-Runtime-User-Id: rxx@xxxx.nl"
}
{
  "message": "Stored session mapping in DynamoDB",
  "requestUri": "urn:ietf:params:oauth:request_uri:MGE5YTIxMWUtOWQxMC00YWVjLWIxZGItYWVkZWI2NmY5NGMx",
  "requestUriLength": 82,
  "userId": "xx@xxxx.nl",
  "userIdLength": 23,
  "userIdBytes": "722e706f7374756c6172744063617265626561742e6e6c"
}

OAuth Callback Lambda (2 seconds later):

{
  "message": "CompleteResourceTokenAuth - Debug Info",
  "sessionUri": "urn:ietf:params:oauth:request_uri:MGE5YTIxMWUtOWQxMC00YWVjLWIxZGItYWVkZWI2NmY5NGMx",
  "sessionUriLength": 82,
  "sessionUriPattern": "VALID",
  "userId": "xx@xxxx.nl",
  "userIdLength": 23,
  "userIdSource": "DynamoDB",
  "awsRegion": "eu-central-1"
}
{
  "message": "Sending CompleteResourceTokenAuth command",
  "commandInput": "{\"sessionUri\":\"urn:ietf:params:oauth:request_uri:MGE5YTIxMWUtOWQxMC00YWVjLWIxZGItYWVkZWI2NmY5NGMx\",\"userIdentifier\":{\"userId\":\"xx@xxxx.nl\"}}"
}
{
  "message": "CompleteResourceTokenAuth failed - Full Details",
  "error": "Invalid or expired session",
  "errorName": "AccessDeniedException",
  "httpStatusCode": 403,
  "sessionUri": "urn:ietf:params:oauth:request_uri:MGE5YTIxMWUtOWQxMC00YWVjLWIxZGItYWVkZWI2NmY5NGMx",
  "userId": "xxx@xxt.nl",
  "userIdBytes": "722e706f7374756c6172744063617265626561742e6e6c"
}

Byte-level comparison confirms EXACT match:

  • Session URI: ✅ Identical
  • User ID: ✅ Identical
  • User ID bytes: ✅ 722e706f7374756c6172744063617265626561742e6e6c (both)
  • Timing: Only ~2 seconds between session creation and callback (not expired)

Questions

  1. Does Gateway 3LO handle CompleteResourceTokenAuth differently than Runtime 3LO?
  2. Is there a specific format required for the userId parameter?
  3. Does the workload identity (Cognito client credentials) need to match between session creation and completion?
  4. Is CompleteResourceTokenAuth even needed for Gateway 3LO, or does Gateway auto-complete sessions?

IAM Permissions

Lambda role has:

{
  "Effect": "Allow",
  "Action": "bedrock-agentcore:CompleteResourceTokenAuth",
  "Resource": "*"
}

Related Documentation

Any guidance on correctly implementing 3LO with Gateway would be greatly appreciated!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions