Como trabalhar com escopos do OAuth2

Esta página se aplica à Apigee e à Apigee híbrida.

Confira a documentação da Apigee Edge.

Neste tópico, você verá como usar e aplicar escopos do OAuth 2.0 com a Apigee.

O que é o escopo OAuth2?

Os escopos do OAuth 2.0 oferecem uma maneira de limitar a quantidade de acesso concedida a um token de acesso. Por exemplo, um token de acesso emitido para um app cliente pode receber acesso de READ e WRITE a recursos protegidos ou apenas a acesso READ. É possível implementar as APIs para impor qualquer escopo ou combinação de escopos desejados. Portanto, se um cliente receber um token com escopo READ e tentar chamar um endpoint de API que exija acesso WRITE, a chamada falhará.

Neste tópico, discutiremos como os escopos são atribuídos aos tokens de acesso e como a Apigee aplica os escopos do OAuth 2.0. Depois de ler este tópico, será possível usar os escopos com confiança.

Como os escopos são atribuídos aos tokens de acesso?

Quando a Apigee gera um token de acesso, ela atribui um escopo a esse token. Para entender como isso acontece, é preciso conhecer estas entidades da Apigee: produtos de API, desenvolvedores e apps de desenvolvedor. Para uma introdução, consulte Introdução à publicação. Recomendamos que você revise esse material, caso necessário, antes de continuar.

Um token de acesso é uma string longa de caracteres de aparência aleatória que permite que a Apigee verifique solicitações de API recebidas. Pense nele como um complemento para credenciais típicas de nome de usuário/senha. Tecnicamente, o token é uma chave que se refere a uma coleção de metadados com esta aparência:

{
  "issued_at" : "1416962591727",
  "application_name" : "0d3e1d41-a59f-4d74-957e-d4e3275d4781",
  "scope" : "A",
  "status" : "approved",
  "api_product_list" : "[scopecheck1-bs0cSuqS9y]",
  "expires_in" : "1799", //--in seconds
  "developer.email" : "scopecheck1-AdBmANhsag@apigee.com",
  "organization_id" : "0",
  "token_type" : "BearerToken",
  "client_id" : "eTtB7w5lvk3DnOZNGReBlvGvIAeAywun",
  "access_token" : "ODm47ris5AlEty8TDc1itwYPe5MW",
  "organization_name" : "wwitman",
  "refresh_token_expires_in" : "0", //--in seconds
  "refresh_count" : "0"
}

Os metadados do token incluem a string do token de acesso real, informações de vencimento, identificação do aplicativo do desenvolvedor, desenvolvedor e produtos associados ao token. Você também verá que os metadados incluem o "escopo".

Como o token recebe o escopo?

Para entender o escopo é preciso lembrar que cada produto em um app do desenvolvedor pode ter zero ou mais escopos atribuídos. É possível atribuir esses escopos ao criar o produto ou adicioná-los posteriormente. Eles existem como uma lista de nomes e são incluídos nos "metadados" associados a cada produto.

Quando você cria um app de desenvolvedor e adiciona produtos a ele, a Apigee analisa todos os produtos desse app e cria uma lista de todos os escopos desses produtos (o mestre ou lista de escopo global do app, uma união de todos os escopos reconhecidos).

Quando um app cliente solicita um token de acesso da Apigee, ele pode opcionalmente especificar quais escopos ele quer ter associado a esse token. Por exemplo, a solicitação a seguir solicita o escopo "A". Ou seja, o cliente está solicitando que o servidor de autorização (Apigee) gere um token de acesso com escopo "A" (autorizando o aplicativo a chamar APIs que tenham o escopo "A"). O aplicativo envia uma solicitação POST desta forma:

curl -i -X POST -H Authorization: Basic Mg12YTk2UkEIyIBCrtro1QpIG \
  -H content-type:application/x-www-form-urlencoded \
  https://apitest.acme.com/oauth/token?grant_type=client_credentials&scope=A

O que acontece?

Quando a Apigee recebe essa solicitação, ela sabe qual app está fazendo a solicitação e qual aplicativo do desenvolvedor o cliente registrou (o código e as chaves secretas do cliente são codificados no cabeçalho de autenticação básico). Como o parâmetro de consulta scope é incluído, a Apigee precisa decidir se algum dos produtos da API associados ao app do desenvolvedor tem o escopo "A". Nesse caso, um token de acesso é gerado com o escopo "A". Outra maneira de analisar isso é que o parâmetro de consulta de escopo é um tipo de filtro. Se o aplicativo do desenvolvedor reconhecer os escopos "A, B, X" e o parâmetro de consulta especificar "scope=X Y Z", somente o escopo "X" será atribuído ao token.

E se o cliente não anexar um parâmetro de escopo? Nesse caso, a Apigee gera um token que inclui todos os escopos reconhecidos pelo app do desenvolvedor. É importante entender que o comportamento padrão é retornar um token de acesso que contenha a união de todos os escopos de todos os produtos incluídos no app do desenvolvedor.

Se nenhum dos produtos associados a um aplicativo do desenvolvedor especificar escopos, e um token tiver um escopo, as chamadas feitas com esse token falharão.

Digamos que um aplicativo de desenvolvedor reconheça estes escopos: A B C D. Esta é a lista mestre de escopos do aplicativo. Pode ser que um produto no app tenha o escopo A e B e o outro tenha o escopo C e D ou qualquer combinação. Se o cliente não especificar um parâmetro scope (ou se especificar um parâmetro de escopo sem valor), o token receberá os quatro escopos: A, B, C e D. O token recebe um conjunto de escopos que são a união de todos os escopos reconhecidos pelo app do desenvolvedor.

Existe mais um caso em que o comportamento padrão é retornar um token de acesso com todos os escopos reconhecidos: é quando a política GenerateAccessToken (a política do Apigee que gera tokens de acesso) não especifica um elemento <Scope>. Por exemplo, veja uma política GenerateAccessToken em que <Scope> é especificado. Se o elemento <Scope> estiver ausente (ou se estiver presente, mas estiver vazio), o comportamento padrão será executado.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-GenerateAccessToken">
    <DisplayName>OAuthV2 - Generate Access Token</DisplayName>
    <Attributes>
      <Attribute name='hello' ref='system.time' display='false'>value1</Attribute>
    </Attributes>
    <Scope>request.queryparam.scope</Scope> 
    <GrantType>request.formparam.grant_type</GrantType>
    <ExternalAuthorization>false</ExternalAuthorization>
    <Operation>GenerateAccessToken</Operation>
    <SupportedGrantTypes>
      <GrantType>client_credentials</GrantType>
    </SupportedGrantTypes>
  <GenerateResponse enabled="true"/>
</OAuthV2>

Como os escopos são aplicados?

Primeiro, lembre-se de que, na Apigee, os tokens de acesso são validados com a política OAuthV2 (normalmente, colocada no início de um fluxo de proxy). A política precisa ter a operação VerifyAccessToken especificada. Vamos analisar essa política:

<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenA">
    <DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
    <ExternalAuthorization>false</ExternalAuthorization>
    <Operation>VerifyAccessToken</Operation>
    <Scope>A</Scope> <!-- Optional: space-separated list of scope names. -->
    <GenerateResponse enabled="true"/>
</OAuthV2>

Observe o elemento <Scope>. Ele é usado para especificar quais escopos a política aceita.

Neste exemplo, a política terá êxito somente se o token de acesso incluir o escopo "A". Se o elemento <Scope> for omitido ou se ele não tiver um valor, a política ignorará o escopo do token de acesso.

Agora, com a capacidade de validar tokens de acesso com base no escopo, é possível projetar suas APIs para aplicar escopos específicos. Para fazer isso, projete fluxos personalizados com políticas VerifyAccessToken de escopo anexadas a eles.

Digamos que a API tenha um fluxo definido para o endpoint /resourceA:

<Flow name="resourceA">
            <Condition>(proxy.pathsuffix MatchesPath "/resourceA") and (request.verb = "GET")</Condition>
            <Description>Get a resource A</Description>
            <Request>
                <Step>
                    <Name>OAuthV2-VerifyAccessTokenA</Name>
                </Step>
            </Request>
            <Response>
                <Step>
                    <Name>AssignMessage-CreateResponse</Name>
                </Step>
            </Response>
        </Flow>

Quando esse fluxo é acionado e a solicitação tem /resourceA no sufixo do caminho, a política OAuthV2-VerifyAccessTokenA é chamada imediatamente. Essa política verifica se o token de acesso é válido e se parece com os escopos compatíveis com o token. Se a política estiver configurada como exemplo abaixo, com <Scope>A</Scope>, ela só será bem-sucedida se o token de acesso tiver o escopo "A". Caso contrário, um erro será exibido.

<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenA">
    <DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
    <ExternalAuthorization>false</ExternalAuthorization>
    <Operation>VerifyAccessToken</Operation>
    <Scope>A</Scope>
    <GenerateResponse enabled="true"/>
</OAuthV2>

Resumindo, os desenvolvedores de API são responsáveis por projetar a aplicação de escopo nas APIs. Isso é feito por meio da criação de fluxos personalizados para lidar com escopos específicos e da adição de políticas VerifyAccessToken para aplicar esses escopos.

Exemplos de código

Por fim, vamos dar uma olhada em algumas chamadas de API de exemplo para ajudar a ilustrar como os tokens recebem escopos e como os escopos são aplicados.

Caso padrão

Digamos que você tenha um app de desenvolvedor com produtos e que a união dos escopos dele seja: A, B e C. Essa chamada de API solicita um token de acesso, mas não especifica um parâmetro de consulta de escopo.

curl -X POST -H content-type:application/x-www-form-urlencoded \
  https://apitest.acme.com/scopecheck1/token?grant_type=client_credentials

Nesse caso, o token gerado receberá os escopos A, B e C (o comportamento padrão). Os metadados do token seriam assim:

{
  "issued_at" : "1417016208588",
  "application_name" : "eb1a0333-5775-4116-9eb2-c36075ddc360",
  "scope" : "A B C",
  "status" : "approved",
  "api_product_list" : "[scopecheck1-yEgQbQqjRR]",
  "expires_in" : "1799", //--in seconds
  "developer.email" : "scopecheck1-yxiuHuZcDW@apigee.com",
  "organization_id" : "0",
  "token_type" : "BearerToken",
  "client_id" : "atGFvl3jgA0pJd05rXKHeNAC69naDmpW",
  "access_token" : "MveXpj4UYXol38thNoJYIa8fBGlI",
  "organization_name" : "wwitman",
  "refresh_token_expires_in" : "0", //--in seconds
  "refresh_count" : "0"
}

Digamos que você tenha um endpoint da API com o escopo "A" (ou seja, que VerifyAccessToken requer o escopo "A"). Veja a política VerifyAccessToken:

<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenA">
    <DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
    <ExternalAuthorization>false</ExternalAuthorization>
    <Operation>VerifyAccessToken</Operation>
    <Scope>A</Scope>
    <GenerateResponse enabled="true"/>
</OAuthV2>

Veja um exemplo de chamada e um endpoint que aplica o escopo A:

curl -X GET -H Authorization: Bearer MveXpj4UYXol38thNoJYIa8fBGlI \
  https://apitest.acme.com/scopecheck1/resourceA

Essa chamada GET funciona:

 {
   "hello" : "Tue, 25 Nov 2014 01:35:53 UTC"
 }

Ela funciona porque a política VerifyAccessToken, que é acionada quando o endpoint é chamado, exige o escopo A e o token de acesso recebeu os escopos A, B e C, o comportamento padrão.

Como filtrar maiúsculas e minúsculas

Digamos que você tenha um app de desenvolvedor com produtos que tenham os escopos A, B, C e X. Você solicita um token de acesso e inclui o parâmetro de consulta scope desta forma:

curl -i -X POST -H content-type:application/x-www-form-urlencoded \
  'https://apitest.acme.com/oauth/token?grant_type=client_credentials&scope=A X'

Nesse caso, o token gerado receberá os escopos A e X, porque A e X são escopos válidos. Lembre-se de que o aplicativo do desenvolvedor reconhece os escopos A, B, C e X. Neste caso, você está filtrando a lista de produtos de API com base nesses escopos. Se um produto tiver o escopo A ou X, será possível configurar os endpoints da API que aplicarão esses escopos. Se um produto não tiver o escopo A ou X (por exemplo, ele tem B, C e Z), as APIs que aplicam os escopos A ou X não podem ser chamadas com o token.

Ao chamar a API com o novo token:

curl -X GET -H Authorization: Bearer Rkmqo2UkEIyIBCrtro1QpIG \
  https://apitest.acme.com/scopecheck1/resourceX

O token de acesso é validado pelo proxy da API. Por exemplo:

<OAuthV2 async="false" continueOnError="false" enabled="true" name="OAuthV2-VerifyAccessTokenX">
    <DisplayName>Verify OAuth v2.0 Access Token</DisplayName>
    <ExternalAuthorization>false</ExternalAuthorization>
    <Operation>VerifyAccessToken</Operation>
    <Scope>A X</Scope>
    <GenerateResponse enabled="true"/>
</OAuthV2>

Os gatilhos GET da chamada são bem-sucedidos e retornam uma resposta. Por exemplo:

 {
   "hello" : "Tue, 25 Nov 2014 01:35:53 UTC"
 }

Ela será bem-sucedida porque a política VerifyAccessToken requer o escopo A ou X, e o token de acesso inclui o escopo A e X. Obviamente, se o elemento <Scope> for definido como "B", essa chamada falhará.

Resumo

É importante entender como a Apigee administra os escopos do OAuth 2.0. Veja os pontos principais:

  • Um aplicativo do desenvolvedor "reconhece" a união de todos os escopos definidos de todos os produtos correspondentes.
  • Quando um app solicita um token de acesso, ele tem a chance de especificar quais escopos ele quer ter. Cabe ao Apigee (o servidor de autorização) descobrir quais escopos realmente serão atribuídos ao token de acesso com base (a) nos escopos solicitados e (b) naqueles que são reconhecidos pelo app do desenvolvedor.
  • Se a Apigee não estiver configurada para verificar o escopo (o elemento <Scope> não está na política VerifyAccessToken ou está vazio), a chamada de API será bem-sucedida, desde que o escopo incorporado no token de acesso corresponda a um dos escopos reconhecidos pelo app do desenvolvedor registrado (um dos escopos da lista "mestre" do app).
  • Se um token de acesso não tiver nenhum escopo associado, ele só terá êxito nos casos em que a Apigee não considerar o escopo (o elemento <Scope> não está presente na política VerifyAccessToken ou está vazia).