Secure audience injection and the resource parameter

OAuth access tokens (including Hosted Login access tokens) are “bearer” tokens: that means that the token can be used by anyone who has possession of it. For example, suppose an access token is issued to Maria Fuentes and, somehow or another, that token ends up in the hands of Karim Nafir. Because Karim doesn’t have to prove that he’s entitled to that token, he’s able to do whatever the token has been authorized to do, no questions asked.

Token leakage (the process by which someone winds up with an access token that was never issued to them) is very rare, but it can happen. Because of that, the OAuth standards include several security measures that can help minimize the misuse (intentional or otherwise) of access tokens. For one, your authorization requests can include the scope parameter, which limits the activities that can be carried out by using a given token. For example, when requesting a configuration access token you can set the following scope:

*:config/tokenPolicies

That scope gives the bearer access to Hosted Login’s token policy APIs, but only to those APIs. If the user tries to use that token to call the login policy APIs that API call will be rejected with a Not Authorized error. Similarly, access tokens have very short lifetimes: for Hosted Login, the maximum lifetime for an access token is one hour. If you are planning to misuse a token, you’ll have to do it quickly; otherwise, the token will expire. That’s by design.

Another way to place limits on an access token is to use the resource parameter. The resource parameter is the primary driver behind secure audience injection (also referred to by such names as audience injection, resource parameter support, resource indicators, and hinted audiences). As we just noted, the scope parameter helps dictate what a token can do; the resource parameter helps dictate where a token can be used. With the resource parameter, you can specify that a token can only be used with a specific protected resource (or set of resources). When you present your access token to a resource server, the server can introspect the token and see if it (the resource server) is included in the token’s audience claim. If it is, access can be granted. If it’s not, access can be denied.


📘

For a more detailed, and a more technical, explanation of social audience injection and the resource parameter, see RFC 8707: Resource Indicators for OAuth 2.0.


To better explain what that means, let’s walk you through a quick example. The following authorization request includes the resource parameter, and sets the value of that parameter to the Uniform Resource Name (URN) urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234 (we’ll talk more about these names later). This means that the returned token is intended for use by the client with the client ID 12341234-1234-4312-1234-123412341234.

Here’s the authorization request::

https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/authorize
    ?client_id=64430515-01ea-4f5d-82e4-c36161af0093
    &redirect_uri=http://localhost&scope=openid
    &code_challenge=ufBhlb8CfzV874maXPFHIguVkjBYssKgi8L5Ec
    &code_challenge_method=S256
    &response_type=token&state=LA6fKYw_1_7x_xrSpJQlStcu5YiqbycKE8QjVWEmeng
    &resource=urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234

So what happens after this request is submitted and the user is authenticated? Well, because we requested an access token, the user is issued exactly what we asked for: an access token. If we introspect the token we’ll see the following:

{
    "active": true,
    "scope": "openid",
    "client_id": "64430515-01ea-4f5d-82e4-c36161af0093",
    "token_type": "Bearer",
    "exp": 1613063532,
    "iat": 1613059932,
    "sub": "0c02a89a-f296-4550-9fad-055cf87099f4",
    "aud": [
        "64430515-01ea-4f5d-82e4-c36161af0093", 
        "12341234-1234-4312-1234-123412341234"
    ],
    "auth_time": 1613059368,
    "acr": "urn:akamai-ic:nist:800-63-3:aal:2",
    "amr": [
        "pwd",
        "email",
        "mfa",
        "rba"
    ]
}

If you look closely at the aud (audience) claim, you’ll see client ID 12341234-1234-4312-1234-123412341234, the same client ID referenced by the resource parameter.


📘

Also included is the entity 64430515-01ea-4f5d-82e4-c36161af0093, the client ID of the OIDC login client that brokered the authorization request. Any time the aud claim includes multiple audiences, the first item in the list is the client responsible for authorization (i.e., the client referenced by the client_id parameter in the authorization request). Any additional audiences are listed after that, although in no specific order.


Suppose you present this access token to a resource server. The resource server can do the same thing we just did: introspect the token and take a look at the aud claim. If the resource server happens to be OAuth client 12341234-1234-4312-1234-123412341234 then access can be granted. If the resource server *isn’t*12341234-1234-4312-1234-123412341234 then the access request can be denied.

Before we go any further we should note that you can’t just include any old URN or URI in an authorization request. Instead, the resource parameter can only reference items that have been defined in your token policy’s allowed resource indicators list (another thing we’ll discuss momentarily). For example, suppose we make the following authorization request, a request for a URN that isn’t found in the token policy:

https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/authorize
    ?client_id=64430515-01ea-4f5d-82e4-c36161af0093
    &redirect_uri=http://localhost
    &scope=openid
    &code_challenge=ufBhlb8CfzV874maXPFHIguVkjBYssKgi8L5Ec
    &code_challenge_method=S256
    &response_type=token
    &state=LA6fKYw_1_7x_xrSpJQlStcu5YiqbycKE8QjVWEmeng
    &resource=urn:ietf:params:oauth:client_id:not-in-token-policy

That request is going to fail with the following error:

Client token policy a7f902b3-6e63-4f60-87a6-6cf5a1bc8ff4 does not allow one or more of the requested resource indicators

To avoid this error, you need to add urn:ietf:params:oauth:client_id:not-in-token-policy to your token policy.


📘

Yes, even though there’s no OAuth client that has the client ID not-in-token-policy that URN can still be added to your token policy. Hosted Login doesn’t verify that resources added to a token policy actually exist: it’s up to you to provide valid resources names and URIs.


A simplified version of this process is shown in the diagram below:

img


The resource parameter and identity tokens

If you request an identity token as well as an access token, the aud claim that’s added to the identity token must match the aud claim in the access token (in other words, it must include all the resources referenced by the resource parameter). This sample identity token does just that:

"acr": "urn:akamai-ic:nist:800-63-3:aal:2",
  "amr": [
    "pwd",
    "email",
    "mfa",
    "rba"
  ],
  "at_hash": "ute_IVd-MGvmCJPvkrI8sQ",
  "aud": [
    "64430515-01ea-4f5d-82e4-c36161af0093",
    "12341234-1234-4312-1234-123412341234"
  ],
  "auth_time": 1613059368,
  "azp": "64430515-01ea-4f5d-82e4-c36161af0093",
  "exp": 1613084350,

If you’re wondering about the green type in the preceding example, the identity token also includes the azp (Authorized Party) claim:

"azp": "64430515-01ea-4f5d-82e4-c36161af0093"

What’s this claim for? Well, anytime the aud claim includes multiple values the azp claim is added to the identity token; this claim tells you which of the aud values is the login client used to obtain the token. (In other words, the value of the azp claim should always match the value of the client_id parameter used in the authorization request.) And what if your authorization request doesn’t include multiple audiences? That’s fine: you just won’t see the azp claim in the identity token. With only one audience, there’s no need to include this claim.

The azp claim is simply another safeguard: if the azp claim doesn’t match the client_id parameter then your identity token shouldn’t be trusted.


Add allowed resource indicators to your token policies

As mentioned previously, resources referenced by the resource parameter must be added to your token policy’s allowed resource indicators list. If you reference a resource that isn’t included in the token policy then your authorization request will fail with an error similar to this one:

Client token policy a7f902b3-6e63-4f60-87a6-6cf5a1bc8ff4 does not allow one or more of the requested resource indicators

However, even though the allowed resource indicators list is a property of a Hosted Login token policy, you can’t use the [/tokenPolicies/{tokenPolicyId}] operation to manage your resource indicators. Instead, there’s a separate operation – /tokenPolicies/{tokenPolicyId}/allowedResourceIndicators – used to manage these indicators.

When it comes to resource indicators and token policies, we should start by saying that all existing token policies have been upgraded to work with these indicators. For example, the following API call uses the GET method to return the allowed indicators list from token policy 37a7bf21-9ac5-48c5-96b5-c2173debee26, a policy created when Hosted Login was first released (i.e., before the resource parameter was supported):

curl -L -X GET \
  'https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/config/tokenPolicies/37a7bf21-9ac5-48c5-96b5-c2173debee26/allowedResourceIndicators' \
  -H 'Authorization: Bearer QVdYZT4Up7cl4DRLhZAvAlmbjneV6e3MEVwOaWTmEV9t4iCK82jdByFy9WthtWIe'

When we make this API call, we get back the following:

null

The null value means that you’ve made a valid API call; it’s just that no resource indicators have been defined for this token policy.


📘

What if the resource indicators had been defined for the policy but were then removed? In that case, the API call returns an empty JSON array:

[]


To add a resource indicator to a token policy, use the PUT method and an API call similar to this one:

curl -L -X PUT \
  'https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/config/tokenPolicies/37a7bf21-9ac5-48c5-96b5-c2173debee26/allowedResourceIndicators' \
  -H 'Authorization: Bearer QVdYZT4Up7cl4DRLhZAvAlmbjneV6e3MEVwOaWTmEV9t4iCK82jdByFy9WthtWIe' \
  -H 'Content-Type: application/json' \
  --data-raw '["urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234"]'

In the preceding call, we’re adding the URN urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234 to the token policy. Note that the resource being added must be enclosed in double quote marks and the entire request body must be surrounded by square brackets (i.e., formatted as a JSON array):

["urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234"]

And before you ask, yes, you can reference multiple resources in a single token policy; just separate the resources by using commas:

[
    "urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234",
    "urn:ietf:params:oauth:client_id:56788765-5678-8765-8765-567887655678"
]

Oh, and you aren’t limited to using URNs when defining resource indicators. Instead, you can use absolute URIs, URIs that don’t include such things as fragments, query parameters, or wildcards. For example, this is a valid URI and can be added to a token policy:

https://identitydocs.akamai.com/documentation/resources

And this (because of the wildcard) is not a valid URI

https://identitydocs.akamai.com/*/resources

We just showed you how to add a pair of resource indicators to a token policy. What if you later decide to add a third resource to the policy? In that case, sure to specify all three indicators in your request body:

[
    "urn:ietf:params:oauth:client_id:12341234-1234-4312-1234-123412341234",
    "urn:ietf:params:oauth:client_id:56788765-5678-8765-8765-567887655678",
    "https://identitydocs.akamai.com/documentation/resources"
]

That’s because the /allowedResourceIndicators operation uses the PUT method to add items to a token policy, and the PUT method replaces all your existing indicators with whatever happens to be included in the request body of your API call. For example, suppose your API call included only the new parameter:

[
    "https://identitydocs.akamai.com/documentation/resources"
]

That API call will succeed. However, if you use the GET method to verify the allowed resource indicators you’ll see this:

[
    "https://identitydocs.akamai.com/documentation/resources"
]

That’s because the new indicator wasn’t added to the token policy; instead, it replaced all the indicators previously in the token policy.

A similar approach is used if you want to remove all the resource indicators from a policy. To do that, call the PUT method, and set the request body an empty array:

curl -L -X \
  PUT 'https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/config/tokenPolicies/37a7bf21-9ac5-48c5-96b5-c2173debee26/allowedResourceIndicators' \
  -H 'Authorization: Bearer QVdYZT4Up7cl4DRLhZAvAlmbjneV6e3MEVwOaWTmEV9t4iCK82jdByFy9WthtWIe' \
  -H 'Content-Type: application/json' \
  --data-raw '[]'