Configuring Apigee to dispense OAuth tokens - either opaque or JWT

I had a question recently from someone who wanted to explore using Apigee to issue different kinds of OAuth tokens: either opaque tokens or JWT.

If you're not clear on the difference between Opaque tokens and JWT, then maybe read this first.

Apigee can generate either kind of token. Today, you need to use a distinct policy type, depending on the type of token you want to issue to the client.

But it's not as simple as using either one policy or the other. I'll explain.

In the simple case, the request-for-token looks like this:

POST $tokenserver/token 
Content-Type: application/x-www-form-urlencoded
Authorization: Basic Zm9vOmJhcg==

grant_type=client_credentials

In other words, the client passes in credentials, as well as a form payload saying "this is a request for token for the grant_type of client_credentials".

In Apigee, the OAuthV2 policy with Operation=GenerateAccessToken will implicitly validate the client credentials provided in a Basic Auth header. The OAuth spec says "The authorization server MUST authenticate the client." and a good way to do that in Apigee is to use the OAuthV2 policy.

Normally the OAuthV2/GenerateAccessToken policy just generates the token response, and you're done. The flow looks like this:

    <Flow name='token1a'>
      <Description>token endpoint #1a</Description>
      <Condition>(proxy.pathsuffix MatchesPath "/token1a") and (request.verb = "POST")</Condition>
      <Request>
        <Step>
          <!-- Validation of the inbound request. Is the required form
               param present? -->
          <Condition>request.formparam.grant_type != "client_credentials"</Condition>
          <Name>RF-InvalidGrantType</Name>
        </Step>
      </Request>
      <Response>
        <Step>
          <!-- This policy implicitly validates the client credentials, and if
               valid, generates a token, and then generates a response
               containing that token, immediately. -->
          <Name>OAuthV2-GenerateAccessToken-CC</Name>
        </Step>
      </Response>
    </Flow>

Very simple!

The response payload looks something like this:

HTTP/1.1 200 OK
Date: Wed, 27 Jan 2021 01:13:31 GMT
Content-Type: application/json
Content-Length: 335
Connection: keep-alive
{
  "issued_at": 1611710011557,
  "client_id": "yzh0A4y6g9GsnR0MNIoAyiR17yXjVQnBF1eetkDAT9",
  "access_token": "lGU8R7nt5djo8VsnBa3GRZK99CGqAFkp8zkiu8Aw6MxGJPG1",
  "grant_type": "client_credentials",
  "expires_in": 1799,
  "issued": "2021-01-27T01:13:31.557Z",
  "expires_at": 1611711810557,
  "expires": "2021-01-27T01:43:30.557Z"
}

If you know anything about JWT, you can immediately recognize that the access_token in the above payload is not a JWT. It is not a dot-separated string, for one thing, and for another, it's not long enough. That is an opaque token. It can be validated only by Apigee.

OK, now what if you want to generate a JWT rather than an opaque token? In this case we use GenerateJWT, but... we also still need the OAuthV2/GenerateAccessToken. The reason is the requirement to authenticate the client. The OAuthV2 policy does that. It *also* generates a token, and for this use case, we don't need the token. But that's ok. With the OAuthV2/GenerateAccessToken, we accomplish our purpose of authenticating the client credentials.

After authenticating the client, we want Apigee to execute the GenerateJWT policy, and generate a signed JWT with the payload claims we need. And then "manually" embed that JWT into a response. The "flow" for this in Apigee looks like this:

    <Flow name='token2'>
      <Description>token endpoint #2</Description>
      <Condition>(proxy.pathsuffix MatchesPath "/token2") and (request.verb = "POST")</Condition>
      <Request>
        <Step>
          <!-- Validation of the inbound request. Is the required form
               param present? -->
          <Condition>request.formparam.grant_type != "client_credentials"</Condition>
          <Name>RF-InvalidGrantType</Name>
        </Step>
      </Request>
      <Response>
        <Step>
          <!-- This policy implicitly validates the client credentials, and if
               valid, generates a token, and then stores that token and other
               information about the token in a set of flow variables.
               We can then generate a JWT containing that information. -->
          <Name>OAuthV2-GenerateAccessToken-CC-NoResponse</Name>
        </Step>
        <Step>
          <Name>AM-SigningKeys</Name>
        </Step>
        <Step>
          <Name>GenerateJWT-RS256</Name>
        </Step>
        <Step>
          <Name>AM-Explicit-JWT-Response</Name>
        </Step>
      </Response>
    </Flow>

While the request for a JWT uses EXACTLY the same shape as a request for an opaque token, the response is different. It might look something like this:

HTTP/1.1 200 OK
Date: Wed, 27 Jan 2021 01:19:55 GMT
Content-Type: application/json
Content-Length: 846
Connection: keep-alive
{
  "access_token" : "eyJraWQiOiJyc2EtMjAyMTAxMjYtMTMxOCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJvcGFxdWUtdG9rZW4iOiJtT2lybVM5RXY5RmxjOUcxbmoxbFI4YVgyTG1FNEw5S0ZhWjhNbnNKR1o5bGV1cGciLCJzdWIiOiJ5emgwQTR5Nmc5R3NuUjBNTklvQXlpUjE3eVhqVlFuQkYxZWV0a0RBVDkiLCJpc3MiOiJodHRwczpcL1wvZ2FjY2VsZXJhdGUzLXRlc3QuYXBpZ2VlLm5ldFwvb2F1dGgyLWNjLWFuZC1qd3RcL2Rpc3BlbnNhcnlcL3Rva2VuMiIsImV4cCI6MTYxMTcxMjE5NSwiaWF0IjoxNjExNzEwMzk1LCJqdGkiOiI0NGFlNjFmMC0xMzUyLTQyODgtYTI0MS1kNDEzOTUyOTAyM2EifQ.AG_y8bG7mFXzZEKBCLFXcZtPw3fL2P0zURG7tK9Vq6uynJ_Y86fOwWilDphyAhiqntq6TrqOFtTliMi2uKDW5QNr7C1BlPBoRvPtHz3AShpuovQzVpO4xTBCYE9rbugfxM-JkATJSJn39Ui5A9UrtLEzkcVhpS_xOdDVIn-bn1QEcI0-gspG5aFDqsHXtA9wFl1vbyRPTvh8aqm8-Rpo5ib4UBWvgOUKaTfI_a5uHINLFbVkP3zxzlx9zzIrCX_ehL7veEuvjLqplJMtUfff6yaHpmznxTMVXMM9EP9NIY6WMZLnlUgxEZUcbJSVdyFHbEFBOiPhLTHV5zNIsgEWeA",
  "token_type" : "Bearer"
}

Yes, a much longer token string there. That is a signed JWT. You can see the dots denoting the three distinct parts of the JWT. If you want to decode that particular JWT, you can click here.

----

If you want to try this yourself, I've got a GitHub repository with all the sample code, and a README to guide you along.

And finally, a Youtube screencast where I walk through this same information.

10734-screenshot-20210126-180628.png

I hope this is helpful! Let me know if you have questions.

Comments
dchiesa1
Staff
Version history
Last update:
‎01-26-2021 05:54 PM
Updated by: