Troca de tokens On-Behalf-Of para agentes multi-tenant com Amazon Bedrock AgentCore Gateway

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-provider para https://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
}
Imagem original — fonte: Aws

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.

Imagem original — fonte: Aws

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.

Imagem original — fonte: Aws

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.

Imagem original — fonte: Aws

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_token para seus grupos para filtragem de scp. 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_proof no momento da troca. Compense com tokens OBO de curta duração e TLS em todos os saltos.
  • O Okta usa o claim cid, não client_id. O mecanismo allowedClients do Gateway corresponde a client_id, que os tokens de acesso do Okta não carregam. Use allowedAudience.
  • O parâmetro subject_token_type deve ser access_token. Vários SDKs padronizam para jwt em trocas RFC 8693, e o Okta rejeita isso com invalid_request. Substitua o valor via customParameters no 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.audience em cada target do Gateway e JwtConfiguration.Audience em 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 sub em 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 sub e 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 (cid no Okta), aud e o jti do 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/)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *