Working with OAuth2 scopes

You're viewing Apigee Edge documentation.
Go to the Apigee X documentation.
info

This topic discusses how to use OAuth 2.0 scopes on Apigee Edge.

What is OAuth2 scope?

OAuth 2.0 scopes provide a way to limit the amount of access that is granted to an access token. For example, an access token issued to a client app may be granted READ and WRITE access to protected resources, or just READ access. You can implement your APIs to enforce any scope or combination of scopes you wish. So, if a client receives a token that has READ scope, and it tries to call an API endpoint that requires WRITE access, the call will fail.

In this topic, we'll discuss how scopes are assigned to access tokens and how Apigee Edge enforces OAuth 2.0 scopes. After reading this topic, you'll be able to use scopes with confidence.

How are scopes assigned to access tokens?

When Edge generates an access token, it may assign a scope to that token. To understand how this happens, you must first be familiar with these Apigee Edge entities: API products, developers, and developer apps. For an introduction, see Introduction to publishing. We recommend that you review this material if you need to before continuing.

An access token is a long string of random-looking characters that allows Edge to verify incoming API requests (think of it as a stand-in for typical username/password credentials). Technically, the token is a key that refers to a collection of metadata that that looks like this:

{
  "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"
}

The token's metadata includes the actual access token string, expiry information, identification of the developer app, developer, and products associated with the token. You'll also notice that the metadata also includes "scope".

How does the token get its scope?

The first key to understanding scope is to remember that each product in a developer app can have zero or more scopes assigned to it. These scopes can be assigned when the product is created, or they can be added later. They exist as a list of names and are included in the "metadata" associated with each product.

When you create a developer app and add products to it, Edge looks at all of the products in the developer app and creates a list of all of the scopes for those products (the app's master or global scope list -- a union of all recognized scopes).

When a client app requests an access token from Apigee Edge, it can optionally specify which scopes it would like to have associated with that token. For example, the following request asks for the scope "A". That is, the client is asking that the authorization server (Edge) generate an access token that has scope "A" (giving the app authorization to call APIs that have scope "A"). The app sends a POST request like this:

curl -i -X POST -H Authorization: Basic Mg12YTk2UkEIyIBCrtro1QpIG -H content-type:application/x-www-form-urlencoded http://myorg-test.apigee.net/oauth/token?grant_type=client_credentials&scope=A

What happens?

When Edge receives this request it knows which app is making the request and it knows which developer app the client registered (the client ID and client secret keys are encoded in the basic auth header). Because the scope query parameter is included, Edge needs to decide if any of the API products associated with the developer app have scope "A". If they do, then an access token is generated with scope "A". Another way to look at this is that the scope query parameter is a kind of filter. If the developer app recognizes scopes "A, B, X", and the query parameter specifies "scope=X Y Z", then only scope "X" will be assigned to the token.

What if the client does not attach a scope parameter? In this case, Edge generates a token that includes all of the scopes recognized by the developer app. It's important to understand that the default behavior is to return an access token that contains the union of all scopes for all of the products included in the developer app.

If none of the products associated with a developer app specify scopes, and a token does have a scope, then calls made with that token will fail.

Let's say a developer app recognizes these scopes: A B C D. This is the app's master list of scopes. It could be that one product in the app has scope A and B, and a second one has scope C and D, or any combination. If the client does not specify a scope parameter (or if it specifies scope parameter with no value) the token will be granted all four scopes: A, B, C, and D. Again, the token receives a set of scopes that is the union of all the scopes recognized by the developer app.

There is one more case where the default behavior is to return an access token with all of the recognized scopes, and that is when the GenerateAccessToken policy (the Apigee Edge policy that generates access tokens) does not specify a <Scope> element. For example, here's a GenerateAccessToken policy where <Scope> is specified. If that <Scope> element is missing (or if it is present but empty), then the default behavior is executed.

<?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>

How are scopes enforced?

First, remember that on Apigee Edge, access tokens are validated with the OAuthV2 policy (typically placed at the very beginning of a proxy flow). The policy must have the VerifyAccessToken operation specified. Let's look at this policy:

<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>

Note the <Scope> element. It is used to specify which scopes the policy will accept.

In this example, the policy will succeed only if the access token includes scope "A". If this <Scope> element is omitted or if it has no value, then the policy ignores the scope of the access token.

Now, with the ability to validate access tokens based on scope, you can design your APIs to enforce specific scopes. You do this by designing custom flows with scope-aware VerifyAccessToken policies attached to them.

Let's say your API has a flow defined for the 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>

When this flow is triggered (a request comes in with /resourceA in the path suffix), the OAuthV2-VerifyAccessTokenA policy is called immediately. This policy verifies that the access token is valid and it looks to see what scope(s) the token supports. If the policy is configured as the example below, with <Scope>A</Scope>, the policy will only succeed if the access token has scope "A". Otherwise, it will return an error.

<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>

To summarize, API developers are responsible for designing scope enforcement into their APIs. They do this by creating custom flows to handle specific scopes, and attaching VerifyAccessToken policies to enforce those scopes.

Code examples

Finally, let's take a look at some example API calls to help illustrate how tokens receive scopes and how scopes are enforced.

Default case

Let's say you have a developer app with products, and that the union of those products' scopes are: A, B, and C. This API call requests an access token, but does not specify a scope query parameter.

curl -X POST -H content-type:application/x-www-form-urlencoded http://wwitman-test.apigee.net/scopecheck1/token?grant_type=client_credentials

In this case, the generated token will be given scopes A, B, and C (the default behavior). The token's metadata would look something like this:

{
  "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"
}

Now, let's say you have an API endpoint that has scope "A" (that is, it's VerifyAccessToken requires scope "A"). Here's the VerifyAccessToken policy:

<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>

Here's a sample call to and endpoint that enforces scope A:

curl -X GET -H Authorization: Bearer MveXpj4UYXol38thNoJYIa8fBGlI http://wwitman-test.apigee.net/scopecheck1/resourceA 

This GET call succeeds:

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

It succeeds because the VerifyAccessToken policy that is triggered when the endpoint is called requires scope A, and the access token was granted scopes A, B, and C -- the default behavior.

Filtering case

Let's say you have a developer app with products that have scopes A, B, C, and X. You request an access token and include the scope query parameter, like this:

curl -i -X POST -H content-type:application/x-www-form-urlencoded 'http://myorg-test.apigee.net/oauth/token?grant_type=client_credentials&scope=A X'

In this case, the generated token will be given scopes A and X, because both A and X are a valid scopes. Remember that the developer app recognizes scopes A, B, C, and X. In this case, you're filtering the list of API products based on these scopes. If a product has scope A or X, you can configure API endpoints that will enforce these scopes. If a product does not have scope A or X (let's say it has B,C, and Z), then the APIs that enforce scopes A or X cannot be called with the token.

When you call the API with the new token:

curl -X GET -H Authorization: Bearer Rkmqo2UkEIyIBCrtro1QpIG http://wwitman-test.apigee.net/scopecheck1/resourceX

The access token is validated by the API proxy. For example:

<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>

The GET call triggers succeeds and it returns a response. For example:

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

It succeeds because the VerifyAccessToken policy requires scope A or X, and the access token includes scope A and X. Of course, if the <Scope> element were set to "B", this call would fail.

Summary

It's important to understand how Apigee Edge handles OAuth 2.0 scopes. Here are key takeaway points:

  • A developer app "recognizes" the union of all scopes defined for all of its products.
  • When an app requests an access token, it has the chance to specify which scopes it would like to have. It's up to Apigee Edge (the authorization server) to figure out which scopes it will actually assign to the access token based on (a) the scope(s) that are requested and (b) the ones that are recognized by the developer app.
  • If Apigee Edge is not configured to check for scope (the <Scope> element is missing from the VerifyAccessToken policy or it is empty), then the API call will succeed as long as the scope embedded in the access token matches one of the scopes recognized by the registered developer app (one of the scopes in the app's "master" list of scopes).
  • If an access token does not have any scopes associated with it, then it will only succeed in cases where Edge does not consider scope (the <Scope> element is missing from the VerifyAccessToken policy or it is empty).