O problema de identidade em agentes multi-tenant
Quando um agente de IA generativa é colocado em produção em arquiteturas multi-tenant, surge um problema de identidade bastante específico: ao fazer uma chamada para uma API downstream em nome de um usuário, qual identidade viaja com essa chamada?
Existem três abordagens possíveis, mas apenas uma delas é segura. Executar a chamada com a identidade de serviço do próprio agente destrói a trilha de auditoria, pois cada sistema downstream precisa confiar incondicionalmente no agente. Encaminhar o token do usuário sem modificação só funciona quando a audiência do token já corresponde à API downstream — condição raramente verdadeira em sistemas multi-tenant. A terceira opção, a troca On-Behalf-Of (OBO), é a única que preserva a identidade do usuário de ponta a ponta, aplica o princípio do menor privilégio na fronteira de audiência e produz um token que a API downstream pode validar de forma independente.
A especificação OAuth 2.0 Token Exchange (RFC 8693) resolve exatamente esse problema, e o Amazon Bedrock AgentCore Identity oferece suporte nativo a ela como um tipo de concessão de provedor de credenciais. Os artigos Construindo agentes multi-tenant com Amazon Bedrock AgentCore e Aplicando controle de acesso refinado com interceptadores do Bedrock AgentCore Gateway estabelecem a fundação conceitual para esse padrão. O post original da AWS funciona como o guia de implementação prático.
O que é o padrão On-Behalf-Of e por que ele importa
Em uma troca OBO, o claim sub do token de entrada é preservado enquanto o claim aud é reescrito para o serviço downstream. O ator da troca fica registrado em um claim separado (act conforme a RFC 8693, ou cid no Okta), permitindo que a API downstream responda duas perguntas a partir de um único token: em nome de quem a ação está sendo executada? (claim sub) e quem está executando a ação? (claim do ator).
O AgentCore Gateway suporta esse padrão de forma transparente: ele intercepta a chamada de ferramenta, identifica o tenant de destino e instrui o AgentCore Identity a realizar a troca contra o servidor de autorização do tenant antes de emitir a chamada downstream. O código do agente não precisa implementar nenhuma lógica de troca — ele obtém um único token de entrada e invoca ferramentas normalmente.
A implementação de referência: TravelBot
O TravelBot é um assistente de reservas multi-tenant que atende dois tenants de exemplo, Acme e Globex. A implementação de referência estará disponível no repositório aws-samples/sample-obo-flow-poc após a publicação.
A arquitetura tem seis componentes principais:
- Servidor de autorização provedor — Emite o Token Web JSON (JWT) de entrada que o agente apresenta ao Gateway. No TravelBot, é o servidor Okta chamado TravelBot Provider.
- AgentCore Gateway — Valida o JWT de entrada, roteia a invocação de ferramenta para o tenant correto e orquestra a troca OBO.
- AgentCore Identity — Armazena as credenciais do cliente delegado e executa a troca de token RFC 8693 contra o servidor de autorização do tenant.
- Servidores de autorização por tenant — Emitem tokens OBO com escopo para a audiência de cada tenant. No TravelBot, ACME Travel API e Globex Travel API são dois servidores Okta distintos, cada um com sua própria audiência e política de acesso.
- Superfície de API por tenant — Um Amazon API Gateway HTTP API com um autorizador JWT por tenant. Cada autorizador valida emissor, audiência e escopos obrigatórios.
- Lógica de negócio do tenant — Uma função AWS Lambda que recebe o token OBO validado, lê os claims e armazena reservas no Amazon DynamoDB particionado pelo claim
sub.
Fluxo de troca de tokens: três fases
O fluxo completo passa por três transformações de token. A tabela a seguir resume como cada claim JWT é transformado entre o token de entrada e o token OBO:
- iss: reescrito do servidor provedor para o servidor do tenant
- aud: reescrito de
travelbot-providerparahttps://api.acme-travel.example - sub: preservado de ponta a ponta (
alice@acme-travel.example) - cid: reescrito do cliente provedor para o cliente delegado (AgentCore Delegate)
- scp: reescrito de
[openid email gateway/invoke]para[booking/read booking/write] - authorized_scopes: novo claim calculado a partir da associação de grupo do usuário
Três claims carregam a história de segurança: o sub é preservado para que logs de auditoria e decisões de autorização resolvam para o chamador original; o aud é reescrito para a API do tenant, vinculando o token criptograficamente a um único serviço downstream; e o cid registra o delegado que realizou a troca, separando o ator do chamador.
Fase 1 — Token de entrada, emitido pelo servidor de autorização provedor
{
"iss": "https://example.okta.com/oauth2/aus<provider-id>",
"aud": "travelbot-provider",
"sub": "alice@acme-travel.example",
"cid": "0oa<provider-client-id>",
"scp": ["openid","email","gateway/invoke"],
"exp": 1748395200
}
Fase 2 — Requisição de troca de token RFC 8693, enviada pelo AgentCore Identity
POST /oauth2/aus<acme-id>/v1/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<inbound JWT from Phase 1>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=https://api.acme-travel.example
&scope=booking/read+booking/write
&client_id=<delegate-client-id>
&client_secret=<delegate-client-secret>
Fase 3 — Token OBO, emitido pelo servidor de autorização do tenant
{
"iss": "https://example.okta.com/oauth2/aus<acme-id>",
"aud": "https://api.acme-travel.example",
"sub": "alice@acme-travel.example",
"cid": "0oa<delegate-client-id>",
"scp": ["booking/read", "booking/write"],
"authorized_scopes": "booking/read",
"exp": 1748395800
}

Escopos por usuário durante a troca de tokens
Um objetivo natural em sistemas multi-tenant é conceder escopos diferentes a cada usuário por papel. Por exemplo, um grupo acme-readonly recebe booking/read, e um grupo acme-fullaccess recebe ambos booking/read e booking/write. O problema é que a troca OBO é processada como uma concessão máquina-a-máquina (M2M): o Okta não mapeia o usuário sujeito de volta para seus grupos para fins de filtragem de escopos. O claim scp do token OBO retorna contendo todos os escopos que o cliente solicitou, independentemente do grupo do usuário.
A solução utiliza um claim em vez de um escopo. Embora o Okta não filtre scp por usuário durante a troca, ele avalia um claim do tipo Expression contra o usuário sujeito no momento da emissão. Adiciona-se um claim personalizado em cada servidor de autorização do tenant:
Nome: authorized_scopes
Tipo de token: Access Token
Tipo de valor: Expression
Valor (Acme): isMemberOfGroupName("acme-fullaccess") ? "booking/read booking/write" : (isMemberOfGroupName("acme-readonly") ? "booking/read" : "")
Incluir em: Qualquer escopo
O token OBO passa a carregar tanto um scp permissivo (informativo) quanto um claim authorized_scopes que reflete a permissão real do usuário. O servidor de recursos toma sua decisão com base em authorized_scopes.

A função Lambda trata authorized_scopes como fonte da verdade para operações de escrita:
# Requisições GET consultam o DynamoDB particionado por tenant + sub.
# Requisições POST exigem adicionalmente booking/write em authorized_scopes:
if method == "POST":
authorized = obo_claims.get("authorized_scopes") or []
if isinstance(authorized, str):
authorized = authorized.split()
if "booking/write" not in authorized:
return {
"statusCode": 403,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"error": "forbidden",
"message": f"User {username} is not permitted to create bookings.",
}),
}
# ... proceed to write the booking
Isolamento de dados por tenant com o claim sub
A propagação de identidade só tem valor se os sistemas downstream agirem sobre ela. O TravelBot armazena reservas no DynamoDB particionado pela identidade do usuário: a chave de partição (pk) é "{tenant}#{sub}" — por exemplo, acme#alice@acme-travel.example — e a chave de ordenação é o ID da reserva. Em uma leitura, a Lambda emite uma Query com escopo para o pk do chamador. Em uma escrita, coloca um item sob o mesmo pk. O efeito é que um usuário não consegue recuperar as reservas de outro usuário — não por um filtro na camada de aplicação, mas porque a consulta é construída a partir do próprio claim sub do chamador, assinado pelo Okta e verificado pelo autorizador do API Gateway antes de a Lambda ser invocada.

Rejeição de tokens entre tenants
Um token OBO emitido para um tenant não pode ser usado para acessar os recursos de outro tenant. No TravelBot, esse princípio é aplicado em três locais independentes: no target do Gateway (o valor customParameters.audience é definido por target); no servidor de autorização do tenant (o Okta valida se o parâmetro audience da requisição de troca está registrado no servidor antes de assinar o token OBO); e no autorizador JWT do API Gateway (cada rota está vinculada a um autorizador cuja JwtConfiguration.Audience é a audiência do tenant). Um token cujo claim aud não corresponde é rejeitado com HTTP 401 antes de a função Lambda ser invocada.

Passo a passo de implementação
A implementação do OBO no AgentCore Gateway envolve três operações usando o SDK AWS para Python (Boto3) contra a API bedrock-agentcore-control.
Passo 1: Criar o Gateway com autorizador JWT personalizado
agentcore.create_gateway(
name="travelbot-obo-gateway",
roleArn=role_arn,
protocolType="MCP",
authorizerType="CUSTOM_JWT",
authorizerConfiguration={
"customJWTAuthorizer": {
"discoveryUrl": f"{provider_issuer}/.well-known/openid-configuration",
"allowedAudience": [provider_audience], # "travelbot-provider"
}
},
)
Passo 2: Criar um provedor de credenciais OAuth2 por tenant
agentcore.create_oauth2_credential_provider(
name="travelbot-cred-acme",
credentialProviderVendor="CustomOauth2",
oauth2ProviderConfigInput={
"customOauth2ProviderConfig": {
"oauthDiscovery": {
"discoveryUrl": f"{acme_issuer}/.well-known/openid-configuration"
},
"clientId": delegate_client_id,
"clientSecret": delegate_client_secret,
"clientAuthenticationMethod": "CLIENT_SECRET_POST",
"onBehalfOfTokenExchangeConfig": {
"grantType": "TOKEN_EXCHANGE",
"tokenExchangeGrantTypeConfig": {
"actorTokenContent": "NONE",
},
},
}
},
)
Passo 3: Criar um target de Gateway por tenant
agentcore.create_gateway_target(
gatewayIdentifier=gateway_id,
name="travelbot-acme-booking",
targetConfiguration={
"mcp": {"openApiSchema": {"inlinePayload": acme_openapi_spec}}
},
credentialProviderConfigurations=[{
"credentialProviderType": "OAUTH",
"credentialProvider": {
"oauthCredentialProvider": {
"providerArn": acme_credential_provider_arn,
"scopes": ["booking/read", "booking/write"],
"grantType": "TOKEN_EXCHANGE",
"customParameters": {
"audience": "https://api.acme-travel.example",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
},
}
},
}],
)
Após essas três operações, o código do agente não contém nenhuma lógica de troca de tokens. Ele adquire um token do provedor, abre uma sessão MCP contra o Gateway e invoca ferramentas. O Gateway e o Identity realizam a troca de forma transparente em cada chamada de ferramenta. A integração de um novo tenant requer apenas a criação de um provedor de credenciais e um target de Gateway — sem alterações no código do agente.
Armadilhas comuns na integração com Okta
A implementação do TravelBot revelou um conjunto de problemas que aparecem consistentemente em integrações OBO contra o Okta:
- Filtragem de escopos por grupo é ignorada durante a troca de tokens. Como o Okta processa a concessão OBO como M2M, não mapeia o usuário do
subject_tokenpara seus grupos para filtragem descp. Use um claim Expression (authorized_scopes) aplicado no servidor de recursos. - O DPoP (Demonstração de Prova de Posse) deve ser desabilitado. O DPoP vincula um token a uma chave privada que o cliente original possui. O AgentCore Identity é um relay de tokens e não possui essa chave, portanto deixar o DPoP obrigatório produz erros
invalid_dpop_proofno momento da troca. Compense com tokens OBO de curta duração e TLS em todos os saltos. - O Okta usa o claim
cid, nãoclient_id. O mecanismoallowedClientsdo Gateway corresponde aclient_id, que os tokens de acesso do Okta não carregam. UseallowedAudience. - O parâmetro
subject_token_typedeve seraccess_token. Vários SDKs padronizam parajwtem trocas RFC 8693, e o Okta rejeita isso cominvalid_request. Substitua o valor viacustomParametersno target do Gateway. - O servidor de autorização provedor deve ser registrado como emissor confiável em cada servidor de autorização do tenant. Isso é configurado em “Trusted Servers” no console de administração do Okta. Sem essa relação de confiança, mesmo uma requisição de troca corretamente construída falha.
- O aplicativo delegado deve listar o grant type de troca de token em seus grant types permitidos. Tanto o controle no nível do aplicativo quanto a política de acesso do servidor de autorização são obrigatórios. A ausência de qualquer um produz um erro
unauthorized_client.
Boas práticas para produção
- Um provedor de credenciais e um cliente delegado por tenant — evite compartilhar provedores entre tenants.
- Vincule audiências explicitamente — defina
customParameters.audienceem cada target do Gateway eJwtConfiguration.Audienceem cada autorizador do API Gateway. - Emita tokens OBO de curta duração — configure os servidores de autorização dos tenants para emitir tokens OBO com o menor tempo de vida (TTL) que a aplicação tolere.
- Trate o client secret do AgentCore Delegate como a credencial mais sensível do sistema — um secret vazado permite que um usuário não autorizado emita tokens OBO para qualquer valor de
subem todos os servidores de autorização de tenant aos quais o delegado está atribuído. Armazene o secret no AWS Secrets Manager com rotação habilitada. - Tome decisões de autorização com base em
sube claims por usuário, não no claim do ator — o chamador original é o principal; o delegado que realizou a troca é o ator. - Nunca emita claims de nomes ou tokens brutos nos logs de aplicação — registre
sub, o ator (cidno Okta),aude ojtido token. Nunca registre o valor bruto do cabeçalho Authorization. - O AgentCore Identity suporta conectividade pública e privada (VPC) para servidores de autorização de tenants. Consulte Conectar a provedores de identidade privados no guia do desenvolvedor do AgentCore para padrões de configuração.
Conclusão
A troca de tokens On-Behalf-Of é o padrão de identidade correto para agentes de IA multi-tenant, e o Amazon Bedrock AgentCore Gateway o operacionaliza sem exigir que o agente implemente a RFC 8693. Ao combinar os provedores de credenciais com audiência vinculada do Gateway com os autorizadores JWT por tenant do API Gateway, preserva-se a identidade do usuário de ponta a ponta, aplica-se o menor privilégio na fronteira de audiência e produz-se uma trilha de auditoria que distingue o delegado do usuário. O claim sub torna-se um principal confiável que os serviços downstream podem passar para uma camada de autorização refinada, como o Amazon Verified Permissions.
Fonte
Implement on-behalf-of token exchange for multi-tenant agents with Amazon Bedrock AgentCore Gateway (https://aws.amazon.com/blogs/machine-learning/implement-on-behalf-of-token-exchange-for-multi-tenant-agents-with-amazon-bedrock-agentcore-gateway/)
Leave a Reply