Dispensing tokens via OAuthV2 with PKCE (RFC 7636)

Let's talk about OAuth, shall we?

Authorization Code grants

The authorization code grant, as described in the OAuth V2 spec (RFC 6749), is intended for use with 3rd-party client apps. "3rd party" means, an app that is not written by the same party that produces the API. The app is therefore not implicitly trusted, and OAuth says that a user should grant consent to the app, to allow it interact with the APIs on behalf of the user. This consent is embodied in the access token issued to the app, via the authorization code grant.

With the authorization code grant, both the user and the app credentials must be verified before the authorization server issues a token. In the Apigee Edge model, an external Identity Provider verifies the user credentials, and Apigee Edge itself verifies the application (client) credentials.

Beyond authenticating the user, the authorization grant should also check the user consent. In a typical login-and-consent user experience, the user first authenticates, and then consents to the app (client) receiving a token with the stated scopes. Some login-and-consent experiences may collapse those two steps into one interaction). Login-and-consent is almost always performed via a trusted user agent, in other words the web browser builtin to the client platform. In some cases a different interaction model can be used to grant consent, like an SMS exchange. Eg, "Do you consent to App X obtaining scope Y? Reply YES to allow this."

After consent, login-and-consent returns the code to the client (app) via a 302 redirect, sent to the redirect address registered for that client. The client POSTs that code to the /token endpoint to receive an access token.

PKCE

All of that is just standard authorization code grant. aka "The 3-legged OAuth dance." Proof Key for Code Exchange, also known as PKCE (RFC 7636) extends that basic model to add a code challenge and code verifier to the protocol, in this way:

In the GET /authorize call, the client passes the CHALLENGE.

When exchanging the code for a token (POST /token), the client passes the VERIFIER.

The Authorization server (Apigee Edge in this case) is responsible for retaining the challenge, and during exchange-code-for-token, checking that the verifier matches the challenge. The idea is to eliminate the possibility for a malefactor to intercept the code, and then be able to obtain a bonafide token with it. You can look at the write-up by Okta for a more detailed description of the motivation for PKCE.

Can Apigee Edge be used for PKCE grants?

PKCE is on the standards track from IETF. It's an open protocol at this point. Can Apigee Edge dispense tokens using the PKCE extension to OAuth2 3-legged grants?

YES. Out of the box, the OAuthV2 policy (and the GenerateAccessToken Operation) does not include support for PKCE. But, it's really straightforward to add this into your own token dispensing proxy, if you want it.

The way to do it:

  1. in the handler for the GET /authorize call, Apigee Edge creates a session which stores the CHALLENGE
  2. In the handler for the POST /token call, Apigee Edge checks the VERIFIER against the CHALLENGE.

It's that simple.

OK, it's not absolutely simple, because OAuthV2 authorization code grants are not simple. There are numerous interactions to handle and you need to consider all of them in the design of the token dispensing proxy. But Apigee Edge handles those grants quite easily. Adding PKCE to the mix requires a very simple enhancement to the normal token dispensing proxy that uses authorization code grant.

AND, the good news is that I have put together s a screencast showing how PKCE works in Apigee Edge:

7753-hqdefault.jpg

And here is a repo that contains all the proxy configuration, as well as some tools, so you can use the same thing in your own Apigee Edge organization.

https://github.com/DinoChiesa/Edge-OAuthV2-PKCE-Proxy

I'd love to hear your comments on this.

Comments
suprabhata914
New Member

Instead of using Cache policy, can we use custom attributes like code challenge, code challenge type etc embbeded into the auth code and then checking them back during the POST request to generate the token using auth code, code verifier?

Thanks,

Anand

sarsisod
New Member

@Dino @Dino-at-Google Can we use the above approach, highlighted by @Anand Gururaja , where code_challenge store as a custom attribute in code flow and later /token flow just use attribute (code_challenge) to verify ?
Please suggest

dchiesa1
Staff

Yes, I think that is a good idea. ...

dominiksommer
Bronze 1
Bronze 1

@Dino-at-Google I just browsed through the repo implementation, and stumbled over:

    <Flow name="token">
              <!--
                  The app uses this request to exchange the code for a token.
                  Example:
      
                  POST /devjam3/oauth2-ac/token?
                    grant_type=authorization_code
                    &client_id=wlq93FiqTw1si09wsocM7AjOBSbyi4
                    &client_secret=78djdkdjdkjd<br>

And indeed, I can't get it to work if I don't pass a client_secret. However, if I get RFC 7636 correctly, the whole point of PKCE is to avoid distributing client secrets to public clients (where PKCE provides means to protect specific attacks).

Hence, I tried setting ExternalAuthorization to true in the OAuth2 policy (following the docs) in order to avoid the client secret (that I don't want to include in the request) being checked. Due to some magic that still seems to happen, I keep ending up with HTTP 500 and the following error:

{"fault":{"faultstring":"Invalid client identifier {0}","detail":{"errorcode":"oauth.v2.InvalidClientIdentifier"}}}<br>

I double-checked that the client_id parameter is still passed and a valid API key. If I set oauth_external_authorization_status to true, the error I'm receiving is upgraded to:

{"fault":{"faultstring":"Invalid access token","detail":{"errorcode":"oauth.v2.InvalidAccessToken"}}}

Is there any specific setting that skips client authentication (using client_secret), but still generates an access token? I think that would be a plausible addition to the example repo.

Br,
Dominik

dchiesa1
Staff

Yes, good point.

PKCE attempts to avoid the requirement to pass secrets. And in fact the client_secret here need not be secret. It's redundant.

Unfortunately, The Apigee OAuthV2 policy requires a client_secret when Operation=GenerateAccessToken. There is no way, currently, to configure the Apigee policy to use PKCE which does not require the client_secret.

In the non-PKCE flow, the client app passes the client_secret and the policy within the API Proxy just references it through a context variable. In the PKCE flow, we don't NEED it. My implementation was ... ok, let me just say it... sloppy.

The way to correct this in the implementation is, within the API Proxy, to use an AccessEntity policy to retrieve the client_secret for the client_id that is passed in. Then reference THAT client_secret.

I'll modify the repo. and post-back when it's ready.

dominiksommer
Bronze 1
Bronze 1

Thanks for the quick reply! I worked around it meanwhile by setting the oauth_external_authorization_status variable to true in an AssignMessage policy, and the

ExternalAuthorizationCode element to request.formparam.code in the OAuthV2 policy. Since the auth code is dumped anyway (as opposed to ExternalAccessToken), it doesn't really hurt.

Using AccessEntity to fetch the client_secret would have been my fallback solution 🙂

dominiksommer
Bronze 1
Bronze 1

Oh and just to add the detail: I've also added the client_id as a key fragment along with the auth code. So I can be pretty sure that it is valid at the point I'm generating the access token, and set the oauth_external_authorization_status with some confidence.

dchiesa1
Staff

OK, I've updated the repo so that redemption of code-for-token does not require the client_secret. Check it out.

(I have not updated the screencast)

praviningawale
Silver 1
Silver 1

Hi Dino,

We are unable to deploy the proxy which is attached in the GitHub https://github.com/DinoChiesa/Edge-OAuthV2-PKCE-Proxy

Also when you are doing a POST to exchange the code and verifier for a token we have to pass clientsecret in the POSTMAN. Is there a way to avoid it in PKCE ?

dchiesa1
Staff

What's the problem with deployment? I haven't looked at that proxy in a while. It relies on Hosted Target, which I think perhaps is no longer supported. That could be the reason that it fails to deploy.

To answer your second question.... Is there a way to avoid passing clientsecret in postman? Yes. In Apigee you can do that. In fact I have modified the repo to not require the client secret. You can see that in the commit message of the most recent commit.

10958-pkce-proxy.png

The trick is, OAuthV2/GenerateAccessToken requires a client secret. In fact the policy requires that the client id and client secret are encoded as a basic-auth Authorization header.

The proxy makes sure that happens, starting with just the client id. The way it works is, it uses the AccessEntity policy, referencing the clientid, to retrieve the client secret. Then it encodes the clientid and secret into a basic-auth header. This is the relevant section in the OAuthv2 proxy:

        <!-- get the client secret -->
        <Step>
          <Name>AE-App</Name>
        </Step>
        <Step>
          <Name>JS-ExtractClientSecret</Name>
        </Step>


        <Step>
          <Name>AM-SetRequiredParameters</Name>
        </Step>


 

If you have more questions on this, I suggest you start a new thread, a new question.

praviningawale
Silver 1
Silver 1

Thanks for the reply. Is if possible to have update proxy which is working based on PKCE as the one currently on Git is not getting deployed.

dchiesa1
Staff

I'll see what I can do. What problem are you experiencing on deployment?

praviningawale
Silver 1
Silver 1

I ran below command using my ORG and ENV. I am using cloud apigee edge instance(https://apigee.com/login)

node ./provision.js -v -o praviningawale-eval -e test

It asks for username and password and then gives attached output.

script-output.png

dchiesa1
Staff

Pravin, you're experiencing a networking problem. The likely problem is, you have a network firewall in place, which is restricting the nodejs script from connecting to api.enterprise.apigee.com . Often you will need to authenticate through an outbound firewall to be able to connect to external system. I suggest you connect with your networking experts to find out what you need to do to connect via that firewall.

To help sort out the firewall / networking issue, You could also try connecting outbound with curl, or other command-line tools. The issue is probably not limited to this particular nodejs script, nor to nodejs in general, nor to the particular external endpoint (api.enterprise.apigee.com), but to any command-line tool that attempts to connect with any external endpoint.

Your browser is probably set up to properly connect to the outside world through a forward proxy or firewall. You need something similar for the terminal, in order to allow you to use the command-line tool for this demo.

davissean
Staff

To add to Dinos comment, you will typically need to set the HTTPS_PROXY and HTTP_PROXY variables as described here.

praviningawale
Silver 1
Silver 1

Hi Dino,

Instead of opting for opening the firewall which itself will take us very long time I thought of manually configuring the proxy by creating all policies manually(though it took some time)

Looking at your proxy I have created few new proxies

I have create two proxies.

First proxy endpoint is oauth/authorize.

When I hit this end point with below request I get the Authcode in the browser itself. The redirect URL is not specified in the call and it uses which is configured in the APPS and it itself a login app.

curl --location --request GET 'https://abv-add-ingress.asasa.com/oauth/authorize?response_type=code&client_id=SSSDDFFFb6NCKN9sBG7TyEJAcZNHL3W1X7PviMYTT27&code_challenge=j_wjZ-C6GRtv7_asasajXZ9LqqgTWsvKGO_EaCyqI&code_challenge_method=S256'

Second Proxy end point is oauth2/token and below is CURL which returns the Access Token

curl --location --request POST 'https://abv-add-ingress.asasa.com/oauth2/token?grant_type=authorization_code' \

--header 'Content-Type: application/x-www-form-urlencoded' \

--header 'Authorization: Basic SASASASA2I2TkNLTjlzQkc3VHlFSkFjWk5ITDNXMVg3UHZpTVlUVDI3OmJFdXlsSDRlVks5UlEwNjZVYjNNQmxIbE5XSUd6dWVXYzlRN3hQVVJ5TnZvSW9KZmJzR283Z3FjblMzYTVOQXg=' \

--data-urlencode 'grant_type=authorization_code' \

--data-urlencode 'client_id=ASASASASNCKN9sBG7TyEJAcZNHL3W1X7PviMYTT27' \

--data-urlencode 'code=DSDS4D#' \

--data-urlencode 'code_verifier=SY3lZFZvCXJ0FtNOtFmTJN_3EXjiGa2oBDM1toppG52HYjsQs-dnCshySXFozm2IXJLGz8qYrs48xaCIcz_2x9G6CI6_CeI9n2ns54qDZQ3

My question here is how to combine both these two end points in the single proxy and store the code challenge from GET request and use in POST. I think you have used 'jsc://importJsonToContext.js' to get the context. Is this the same way we have to do. If you have sample proxy then please share it too.

dchiesa1
Staff

Hi Pravin,

I didn't mean to say you'd need to CREATE a firewall, but rather that THERE IS a firewall, and you need to configure your terminal session to allow it to connect through it. This is analogous to configuring your browser to be able to connect through the corporate firewall. Often the browser configuration is done automatically for you, but the configuration for a shell prompt is not. Your corporate network security people would be able to advise. It's usually a very easy effort.

In any case, ...

Normally the /authorize and /token flows are part of the same API proxy bundle, and in the same Proxy Endpoint. I think the example I showed here for PKCE uses a single proxyendpoint for these requests.

So I would suggest that you did not need to create multiple distinct proxies, and the way to combine them is ... don't do that. Just create one.

Maybe follow the example in the github repo.

I think you have used 'jsc://importJsonToContext.js' to get the context.

That script ... yes, there is a context that is cached ... See CP-AuthorizationSession. The format of the data is json. The JSON just stores a bunch of information related to the session. That session is initiated during handling of /authorize , before redirecting to the login app.

The login-and-consent experience needs some of that information. The session proxyendpoint allows the login-and-consent experience to retrieve it.

The session information is also used when the login-and-consent experience generates an authcode.

The thing you asked about, importJsonToContext.js... is just a Javascript that takes a string JSON (which in this case the proxy retrieved from cache) and populates context variables with the data in the json. This allows subsequent policies to use the data that had been encoded in the JSON.

I suppose you will need something like this ... some sort of session ... to handle Authorization Code with PKCE.

praviningawale
Silver 1
Silver 1

Hi Dino,

As per your suggestion I have combine both the endpoints into a single proxy.

There is one issue I have been facing since then.

The proxy is failing at below step

<Step> <Name>RF-BadSession</Name>

<Condition>authtx = null</Condition>

</Step>


I tried to find out as to where this variable is getting set. I have checked all the AM policies and Cache Populate policy but could not find how and when this variable is set.

Can you please suggest.

praviningawale
Silver 1
Silver 1

Denis,

I have looked at it earlier and it uses lookup cache policy. What I understand is that it should have a corresponding Populate Cache policy... isn't it ?

There exist one Populate Cache policy named 'CP-AuthorizationSession', but it does not store any variable named 'authtx'

Version history
Last update:
‎11-28-2018 02:06 PM
Updated by: