OCI API Gateway: JWT Authentication, Rate Limiting, and Backend Routing with Terraform

OCI API Gateway sits in front of your backend services and handles authentication, request validation, rate limiting, and routing before a single request reaches your application code. Centralizing these concerns in the gateway removes them from each service and enforces consistent policies across your entire API surface. This post covers deploying a production API Gateway with Terraform, configuring JWT authentication, setting up rate limiting with usage plans, and routing to both OCI Functions and HTTP backends.

Step 1: IAM Policy and Gateway

resource "oci_identity_policy" "api_gateway_policy" {
  compartment_id = var.compartment_id
  name           = "api-gateway-policy"
  statements = [
    "Allow any-user to use functions-family in compartment id COMPARTMENT_OCID where all {request.principal.type = 'ApiGateway', request.resource.compartment.id = 'COMPARTMENT_OCID'}",
    "Allow any-user to read secret-bundle in compartment id COMPARTMENT_OCID where all {request.principal.type = 'ApiGateway'}"
  ]
}

resource "oci_apigateway_gateway" "production" {
  compartment_id             = var.compartment_id
  display_name               = "production-api-gateway"
  endpoint_type              = "PRIVATE"
  subnet_id                  = var.private_subnet_id
  network_security_group_ids = [var.api_gateway_nsg_id]
  certificate_id             = var.tls_certificate_id

  defined_tags = {
    "Operations.Environment" = "production"
    "Operations.ManagedBy"   = "terraform"
  }
}

output "gateway_hostname" {
  value = oci_apigateway_gateway.production.hostname
}

Step 2: Usage Plan for Rate Limiting

resource "oci_apigateway_usage_plan" "standard_tier" {
  compartment_id = var.compartment_id
  display_name   = "standard-api-tier"

  entitlements {
    name = "standard-entitlement"

    rate_limit {
      value  = 1000
      unit   = "MINUTE"
    }

    quota {
      value          = 100000
      unit           = "DAY"
      reset_policy   = "CALENDAR"
      operation_on_breach = "REJECT"
    }
  }
}

resource "oci_apigateway_usage_plan" "premium_tier" {
  compartment_id = var.compartment_id
  display_name   = "premium-api-tier"

  entitlements {
    name = "premium-entitlement"

    rate_limit {
      value = 10000
      unit  = "MINUTE"
    }

    quota {
      value               = 10000000
      unit                = "DAY"
      reset_policy        = "CALENDAR"
      operation_on_breach = "REJECT"
    }
  }
}

Step 3: API Deployment with JWT Auth and Rate Limiting

resource "oci_apigateway_deployment" "orders_api" {
  compartment_id = var.compartment_id
  gateway_id     = oci_apigateway_gateway.production.id
  display_name   = "orders-api-v1"
  path_prefix    = "/v1"

  specification {
    # Global request policies apply to all routes
    request_policies {
      authentication {
        type = "JWT_AUTHENTICATION"

        audiences        = ["https://api.example.com"]
        issuers          = ["https://identity.oraclecloud.com"]
        token_header     = "Authorization"
        token_auth_scheme = "Bearer"

        public_keys {
          type            = "REMOTE_JWKS"
          uri             = "https://identity.oraclecloud.com/.well-known/jwks.json"
          max_cache_duration_in_hours = 1
        }

        verify_claims {
          key       = "scope"
          values    = ["orders:read", "orders:write"]
          is_required = false
        }
      }

      rate_limiting {
        rate_in_requests_per_second = 100
        rate_key                    = "CLIENT_IP"
      }

      usage_plans {
        token_locations = ["request.headers[authorization]"]
      }

      cors {
        allowed_origins  = ["https://app.example.com"]
        allowed_methods  = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
        allowed_headers  = ["Authorization", "Content-Type", "X-Correlation-ID"]
        max_age_in_seconds = 3600
      }
    }

    # Routes
    routes {
      path    = "/orders"
      methods = ["GET", "POST"]

      backend {
        type = "HTTP_BACKEND"
        url  = "https://orders-api.internal.example.com/orders"

        is_ssl_verify_disabled = false

        connect_timeout_in_seconds = 5
        read_timeout_in_seconds    = 30
        send_timeout_in_seconds    = 30
      }

      request_policies {
        authorization {
          type           = "ANY_OF"
          allowed_scope  = ["orders:read", "orders:write"]
        }

        header_transformations {
          set_headers {
            items {
              name   = "X-Customer-ID"
              values = ["request.auth.claims[sub]"]
              if_exists = "OVERWRITE"
            }
          }
        }
      }
    }

    routes {
      path    = "/orders/process"
      methods = ["POST"]

      backend {
        type        = "ORACLE_FUNCTIONS_BACKEND"
        function_id = var.orders_processor_function_id
      }

      request_policies {
        authorization {
          type          = "ANY_OF"
          allowed_scope = ["orders:write"]
        }
      }
    }
  }

  defined_tags = {
    "Operations.Environment" = "production"
    "Operations.ManagedBy"   = "terraform"
  }
}

output "api_endpoint" {
  value = "https://${oci_apigateway_gateway.production.hostname}/v1"
}

Step 4: Request and Response Transformation

routes {
  path    = "/legacy/orders"
  methods = ["GET"]

  backend {
    type = "HTTP_BACKEND"
    url  = "https://legacy-orders.internal.example.com/api/orders"
  }

  request_policies {
    header_transformations {
      set_headers {
        # Translate modern header to legacy format
        items {
          name      = "X-Api-Version"
          values    = ["2024-01-01"]
          if_exists = "OVERWRITE"
        }
        items {
          name      = "X-Auth-Token"
          values    = ["request.auth.claims[sub]"]
          if_exists = "OVERWRITE"
        }
      }
      rename_headers {
        items {
          from = "X-Correlation-ID"
          to   = "X-Request-ID"
          if_exists = "OVERWRITE"
        }
      }
    }
  }

  response_policies {
    header_transformations {
      filter_headers {
        type = "BLOCK"
        items {
          name = "Server"
        }
        items {
          name = "X-Powered-By"
        }
      }
    }
  }
}

Operational Notes

JWT token validation happens at the gateway before the request reaches your backend. Set max_cache_duration_in_hours on the remote JWKS endpoint to reduce external calls for key rotation. The gateway caches the signing keys for the specified duration. When you rotate signing keys, allow for the cache duration to expire before revoking old keys, or reduce the cache duration temporarily before rotation.

Rate limiting with rate_key = CLIENT_IP applies per source IP. For APIs consumed by services rather than browsers, use TOTAL to apply a global rate limit across all callers, or implement usage plans with subscriber keys to get per-subscriber rate limiting that survives IP address changes.

Regards,
Osama

#OCI #OracleCloud #APIGateway #JWT #Terraform #IaC #CloudSecurity #TechBlog #Oracle #PlatformEngineering #APIManagement #DevOps #RateLimiting #Authentication #OracleCloudInfrastructure #Functions #CORS #UsagePlans #CloudNative #Microservices

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.