Protect OIC REST APIs with OCI API Gateway and OAuth2 – 2/2

Protect OIC REST APIs with OCI API Gateway and OAuth2 – 2/2

This article is part of a series about OAuth 2.0 Authorization on OCI API Gateway:

  1. Complete Guide: How to configure OAuth 2.0 with JWT & IDCS on OCI API Gateway
  2. Limit access to your APIs with OCI API Gateway using OAuth 2.0 Scopes
  3. Protect OIC REST APIs with OCI API Gateway and OAuth2 – 1/2
  4. Protect OIC REST APIs with OCI API Gateway and OAuth2 – 2/2

Intro

Last time we saw why using OCI API Gateway in front of OIC is a great idea and also we saw a simple way of using OCI API Gateway in front of Oracle Integration Cloud by using OAuth on the API Gateway layer with IDCS and forwarding the Basic Authentication for OIC in the route.

Let’s look at how to use OCI API Gateway in front of OIC with OAuth2 authorization on both layers.

While I was writing this one, there was this great article published on Oracle Blogs about this subject by Prakash Masand, so I will not go through all the process again myself. I will just go over the use case, how it can be used, and provide a code sample for the Custom Authorizer Function.

So, please use this article as a reference to this subject.

Use Case

We know from the last article that we cannot “turn off” the authentication for OIC and we must use either Basic Authentication or OAuth2. We saw how to use OAuth2 + Basic Authentication, now we’ll look at how to use OAuth2 on both layers.

In this article mentioned above, the example is using OAuth2 on the API Gateway layer with Microsoft AD and OAuth2 on the OIC layer with Oracle IDCS, but you could also use Oracle IDCS on both layers or as a matter of fact, any provider.

Description of the process:

  • Create Microsoft AD OAuth2 authorization configuration (or the equivalent in another provider – for example an IDCS Confidential Application) that will be used by the caller of the API for authorization
  • Create IDCS OAuth2 authorization configuration for OIC
  • The caller will generate a token and it will use it when calling the API Gateway endpoint
  • On the API Gateway, your Custom Authorizer Function will firstly call the provider (Microsoft AD in the example) to validate the token used by the caller
  • If the token is valid, the Custom Authorizer Function will then move to the next step which is to generate a new token for your backend. In this case that is OIC
    • It will call Oracle IDCS to generate a token for you
  • Once generated, the token will be returned by the Function in the auth_context and you can forward it to OIC using the Request Header Transformations from the API Gateway Route using this variable: ${request.auth[back_end_token]}
  • The call will be made toward OIC and this token will now be validated by IDCS
  • A response will be returned from the Integration Flow to the original caller

You can have a look at this article on how to configure OAuth2 for OIC with Oracle IDCS.

Source Code

You can use this sample source code presented in the article mentioned above for your OCI API Gateway – Custom Authorizer Function.

import datetime
import io
import json
import logging
import oci
import base64
from datetime import timedelta

import requests
from fdk import response
from requests.auth import HTTPBasicAuth


oauth_apps = {}

def initContext(context):
    # This method takes elements from the Application Context and from OCI Vault to create the OAuth App Clients object.
    if (len(oauth_apps) < 2):
        logging.getLogger().info('Retriving details about the API and backend OAuth Apps')
        try:
            logging.getLogger().info('initContext: Initializing context')

            oauth_apps['idcs'] = {'introspection_endpoint': context['idcs_introspection_endpoint'], 
                                  'client_id': context['idcs_app_client_id'], 
                                  'client_secret': getSecret(context['idcs_app_client_secret_ocid'])}
            oauth_apps['oic'] = {'token_endpoint': context['back_end_token_endpoint'], 
                                  'client_id': context['back_end_app_client_id'], 
                                  'client_secret': getSecret(context['back_end_client_secret_ocid'])}

        except Exception as ex:
            logging.getLogger().error('initContext: Failed to get config or secrets')
            print("ERROR [initContext]: Failed to get the configs", ex, flush=True)
            raise
    else:
        logging.getLogger().info('OAuth Apps already stored')
        
def getSecret(ocid):
    signer = oci.auth.signers.get_resource_principals_signer()
    try:
        client = oci.secrets.SecretsClient({}, signer=signer)
        secret_content = client.get_secret_bundle(ocid).data.secret_bundle_content.content.encode('utf-8')
        decrypted_secret_content = base64.b64decode(secret_content).decode('utf-8')
    except Exception as ex:
        logging.getLogger().error("getSecret: Failed to get Secret" + ex)
        print("Error [getSecret]: failed to retrieve", ex, flush=True)
        raise
    return decrypted_secret_content

def introspectToken(access_token, introspection_endpoint, client_id, client_secret):
    # This method handles the introspection of the received auth token to IDCS.  
    payload = {'token': access_token}
    headers = {'Content-Type' : 'application/x-www-form-urlencoded;charset=UTF-8', 
               'Accept': 'application/json'}
               
    try:
        token = requests.post(introspection_endpoint, 
                              data=payload, 
                              headers=headers, 
                              auth=HTTPBasicAuth(client_id, 
                              client_secret))

    except Exception as ex:
        logging.getLogger().error("introspectToken: Failed to introspect token" + ex)
        raise

    return token.json()

def getBackEndAuthToken(token_endpoint, client_id, client_secret):
    # This method gets the token from the back-end system (oic in this case)
    payload = {'grant_type': 'client_credentials'}
    headers = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}
    try:
        backend_token = json.loads(requests.post(token_endpoint, 
                                                 data=payload, 
                                                 headers=headers, 
                                                 auth=HTTPBasicAuth(client_id, client_secret)).text)

    except Exception as ex:
        logging.getLogger().error("getBackEndAuthToken: Failed to get oic token" + ex)
        raise
    
    return backend_token

def getAuthContext(token, client_apps):
    # This method populates the Auth Context that will be returned to the gateway.
    auth_context = {}

    # Calling IDCS to validate the token and retrieve the client info
    try:
        token_info = introspectToken(token[len('Bearer '):], client_apps['idcs']['introspection_endpoint'], client_apps['idcs']['client_id'], client_apps['idcs']['client_secret'])

    except Exception as ex:
            logging.getLogger().error("getAuthContext: Failed to introspect token" + ex)
            raise

    # If IDCS confirmed the token valid and active, we can proceed to populate the auth context
    if (token_info['active'] == True):
        auth_context['active'] = True
        auth_context['principal'] = token_info['sub']
        auth_context['scope'] = token_info['scope']
        # Retrieving the back-end Token
        backend_token = getBackEndAuthToken(client_apps['oic']['token_endpoint'], client_apps['oic']['client_id'], client_apps['oic']['client_secret'])
        
        # The maximum TTL for this auth is the lesser of the API Client Auth (IDCS) and the Gateway Client Auth (oic)
        if (datetime.datetime.fromtimestamp(token_info['exp']) < (datetime.datetime.utcnow() + timedelta(seconds=backend_token['expires_in']))):
            auth_context['expiresAt'] = (datetime.datetime.fromtimestamp(token_info['exp'])).replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
        else:
            auth_context['expiresAt'] = (datetime.datetime.utcnow() + timedelta(seconds=backend_token['expires_in'])).replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
        # Storing the back_end_token in the context of the auth decision so we can map it to Authorization header using the request/response transformation policy
        auth_context['context'] = {'back_end_token': ('Bearer ' + str(backend_token['access_token']))}

    else:
        # API Client token is not active, so we will go ahead and respond with the wwwAuthenticate header
        auth_context['active'] = False
        auth_context['wwwAuthenticate'] = 'Bearer realm=\"identity.oraclecloud.com\"'

    return(auth_context)

def handler(ctx, data: io.BytesIO=None):
    logging.getLogger().info('Entered Handler')
    initContext(dict(ctx.Config()))
      
    auth_context = {}
    try:
        gateway_auth = json.loads(data.getvalue())

        auth_context = getAuthContext(gateway_auth['token'], oauth_apps)

        if (auth_context['active']):
            logging.getLogger().info('Authorizer returning 200...')
            return response.Response(
                ctx,
                response_data=json.dumps(auth_context),
                status_code = 200,
                headers={"Content-Type": "application/json"}
                )
        else:
            logging.getLogger().info('Authorizer returning 401...')
            return response.Response(
                ctx,
                response_data=json.dumps(str(auth_context)),
                status_code = 401,
                headers={"Content-Type": "application/json"}
                )

    except (Exception, ValueError) as ex:
        logging.getLogger().info('error parsing json payload: ' + str(ex))

        return response.Response(
            ctx,
            response_data=json.dumps(str(auth_context)),
            status_code = 401,
            headers={"Content-Type": "application/json"}
            )

GitHub Repo with all necessary files.

Always test before use.

Ionut Adrian Vladu

I enjoy building python scripts for…everything! I am a Cloud enthusiast and I like to keep up with technology. When I'm not behind a computer, I like taking photos -- Visit My 500px profile -- or sit back and enjoy Formula 1 race weekends. Currently, working as a Tech Cloud Specialist @ Oracle
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments