Does Apigee support mTLS + OAuth2.0 towards backends?

Hi,

We have an API provider that exposes its APIs using mTLS + OAuth2.0 client credential grant. The API request flow would be -

1. Consumer APP invokes Apigee proxy

2. Apigee proxy validates the consuming app and if valid proceeds to the next step, otherwise returns an error.

3. Apigee checks if it has a valid token (to connect to the backend) in the cache. If yes, proceed to step 6, otherwise proceed to next step.

4. Apigee invokes the backends OAuth provider to obtain an OAuth token. The OAuth providers Token endpoint is protected using mTLS, so Apigee needs to connect to the same using mTLS

5. Apigee obtains the OAuth token from the backend OAuth provider. Apigee caches this token based on the expiry obtained in the Token API response

6. Apigee injects the OAuth token in the request and invokes the functional endpoint exposed by the backend provider. This functional endpoint is protected using mTLS + OAuth2.0, so along with the injected Oauth token, Apigee has to perform a mTLS handshake with the backend

7. Apigee obtains the response from the functional endpoint of the backend

8. Apigee returns the response as-is to the consumer application

Amongst all the steps listed above, I would like to understand if step 4 & step 6 can be achieved using Apigee.

If yes, please do advise the recommended solution to achieve this.

Any solutions would be greatly appreciated.

Regards,

Ian

2 4 243
4 REPLIES 4

Yes, you can do that.

To construct that kind of a proxy in Apigee, you would rely on the builtin policies like

  • OAuthV2/VerifyAccessToken in step 2. That verifies the inbound request.
  • LookupCache in step 3. This will read anything you like from cache, including things like cached tokens.
  • ServiceCallout in step 4. No problem issuing a POST to a /token endpoint, and no problem using 2-way TLS on that connection. You can specify the keystore and truststore for the TLS handshake. That covers the transport-layer credentials. You will need to decide how to store+retrieve the application-layer credentials which will be used to authenticate to this backend /token endpoint. Sometimes it's a simple client_credentials flow as defined in IETF RFC 6749, in which case you need a client id and client secret.  Sometimes it's a JWT-bearer flow ( "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" as defined in IETF RFC 7523 ), in which case the credentials will be a public/private keypair.  Sometimes the flow is something else.  In any case, Apigee is acting as the "client" here, so it needs access to the required credentials, whatever they are.  You can store them in the KVM of Apigee, or in some other secret store. One sort-of exception to this case is ... if the /token upstream is a Google cloud service, then you may be able to use the Authentication/GoogleAccessToken mechanism in the ServiceCallout. 
  • PopulateCache in Step 5.  You may need an AssignMessage/AssignVariable with a jsonPath function or ExtractVariables policy before that PopulateCache policy, to extract the token from the response payload.
  • AssignMessage in Step 6, to inject an Authorization Header into the outbound request to the upstream system. Presumably this will overwrite the Authorization header received on the inbound.  Attach this AssignMessage policy to the TargetEndpoint, in the request flow. As with the ServiceCallout, the TargetEndpoint can specify the keystore and truststore for the TLS handshake with the remote endpoint. This truststore/keystore may be different from the ones you used for the ServiceCallout policy.  
  • Steps 7 and 8 are just standard reverse proxy behavior, you don't need to do anything in Apigee to make that happen. 

Pretty straightforward. Using 2-way TLS for the upstream is a VERY common pattern. Using OAuth2 bearer tokens for the upstream is a common pattern. Combining them in the way you describe, is less common in my experience.  But in any case what you describe is pretty straightforward.

 

Thank you for the response and the explanation. It is helpful.

Just adding up little flow & can work with you if you face any issues but it is mostly easy to implement..

1. Proxy Pre-Flow:

Step 0 (optional - follow your existing security policies flow)

- Shared flow - you may want to enable basic security guard rails (enable few regex,rate limiting etc) with in the shared flow

https://cloud.google.com/apigee/docs/api-platform/reference/policies/reference-overview-policy

Step 1 - Shared flow which does  verify of access token (assuming you have oauth enabled) & throw error if it is invalid 

Sample verification

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OA-VerifyAccessToken">
<DisplayName>OA-VerifyAccessToken</DisplayName>
<Properties/>
<Attributes/>
<ExternalAuthorization>false</ExternalAuthorization>
<Operation>VerifyAccessToken</Operation>
<SupportedGrantTypes/>
<GenerateResponse enabled="true"/>
<Tokens/>
</OAuthV2>

 

Step -2 - Remove the header which you don't want to propagate further using Assign Message as it is not required..

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage async="false" continueOnError="false" enabled="true" name="AM-RemoveHeaders">
    <DisplayName>AM-RemoveHeaders</DisplayName>
    <Properties/>
    <Remove>
        <Headers>
           <Header name="Authorization"/>
        </Headers>
    </Remove>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

 

Step 3 - You can have standard conditional flow to forward the req (eg: GET/POST what ever is the functionality)

Step 4 - Now add the backend related calls/caching mechanism with in PostFlow ( the proxy endpoint PostFlow executes before the target endpoint PreFlow). It looks simple,clean and no duplicate on different conditional flow..

 https://cloud.google.com/apigee/docs/api-platform/cache/persistence-tools

in Apigee X, the named caches get implicitly created at runtime when you reference one within a cache policy..

Proxy PostFlow:

First do the lookup in cache for a token

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<LookupCache async="false" continueOnError="false" enabled="true" name="LC-Sample">
    <DisplayName>LC-Sample</DisplayName>
    <CacheResource>Sample</CacheResource>
    <AssignTo>access_token</AssignTo>
    <Scope>Exclusive</Scope>
    <CacheKey>
        <KeyFragment>access_token</KeyFragment>
    </CacheKey>
</LookupCache>

 

 Next perform service callout on below condition (make sure you are reading the variables from kvm in pre-flow itself after step 0 - didn't add it and assuming you know how to read the encrypted kvm values) .

 

<Condition>lookupcache.LC-Sample.cachehit = false</Condition>

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ServiceCallout async="false" continueOnError="false" enabled="true" name="SC-Sample">
    <DisplayName>SC-Sample</DisplayName>
    <Properties/>
    <Request clearPayload="true" variable="sampleRequest">
        <Set>
            <FormParams>
                <FormParam name="grant_type">{private.sample_grant_type}</FormParam>
                <FormParam name="client_id">{private.sample_client_id}</FormParam>
                <FormParam name="client_secret">{private.sample_client_secret}</FormParam>
            </FormParams>
            <Headers>
                <Header name="Content-Type">application/x-www-form-urlencoded</Header>
            </Headers>
            <Verb>POST</Verb>
        </Set>
        <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
    </Request>
    <Response>sampleResponse</Response>
    <HTTPTargetConnection>
        <Properties/>
        <URL>https://{private.sample_tokenUrl}</URL>
 <SSLInfo>
        <Enabled>true</Enabled>
        <ClientAuthEnabled>true</ClientAuthEnabled>
        <KeyStore>{var-for-keystore}</KeyStore>
        <KeyAlias>{var-for-keyalias}</KeyAlias>
        <TrustStore>{var-for-truststore}</TrustStore>
        <IgnoreValidationErrors>false</IgnoreValidationErrors>
    </SSLInfo>
    </HTTPTargetConnection>
</ServiceCallout>

 

 Now extract the response from the service callout using below condition

<Condition>lookupcache.LC-Sample.cachehit = false and sampleResponse.content != null</Condition>

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ExtractVariables async="false" continueOnError="false" enabled="true" name="EV-Sample">
    <DisplayName>EV-Sample</DisplayName>
    <Properties/>
    <JSONPayload>
        <Variable name="access_token">
            <JSONPath>$.access_token</JSONPath>
        </Variable>
    </JSONPayload>
    <Source clearPayload="false">sampleResponse.content</Source>
</ExtractVariables>

 

 Now you populate the token using below condition

<Condition>lookupcache.LC-Sample.cachehit = false</Condition>

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<PopulateCache async="false" continueOnError="false" enabled="true" name="PC-Sample">
    <DisplayName>PC-Sample</DisplayName>
    <CacheResource>Sample</CacheResource>
    <Source>access_token</Source>
    <Scope>Exclusive</Scope>
    <CacheKey>
        <KeyFragment>access_token</KeyFragment>
    </CacheKey>
    <ExpirySettings>
        <TimeoutInSeconds ref="private.sampleTimeOut">12345</TimeoutInSec>
    </ExpirySettings>
</PopulateCache>

 

 Step 5 - Final assign target url & access token in  Target PreFlow

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage async="false" continueOnError="false" enabled="true" name="AM-SampleTarget">
    <DisplayName>AM-SampleTarget</DisplayName>
    <Properties/>
    <AssignVariable>
        <Name>target.url</Name>
        <Template>https://{private.sample_targetUrl}</Template>
    </AssignVariable>
    <Set>
        <Headers>
            <Header name="Authorization">Bearer {access_token}</Header>
        </Headers>
    </Set>
    <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

 

 Error handling & logging follow your standard process..

Good Luck.

Thank you for the response and detailed explanation. I will give this a try and surely reach out in case of any further questions.