{"id":19200425,"url":"https://github.com/fabriziofiorucci/nginx-api-steering","last_synced_at":"2025-06-21T06:05:08.622Z","repository":{"id":205289992,"uuid":"713029804","full_name":"fabriziofiorucci/NGINX-API-Steering","owner":"fabriziofiorucci","description":"NGINX Plus API Gateway configuration to publish REST APIs, authenticate, authorize and manipulate JSON payloads","archived":false,"fork":false,"pushed_at":"2024-05-15T14:03:47.000Z","size":33,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-20T10:45:28.134Z","etag":null,"topics":["api-gateway","docker-compose","nginx","rest-api"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fabriziofiorucci.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-01T17:37:47.000Z","updated_at":"2025-02-05T09:42:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"52110a8f-73c8-4191-adb5-54d87e68da07","html_url":"https://github.com/fabriziofiorucci/NGINX-API-Steering","commit_stats":null,"previous_names":["fabriziofiorucci/nginx-api-steering"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/fabriziofiorucci/NGINX-API-Steering","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabriziofiorucci%2FNGINX-API-Steering","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabriziofiorucci%2FNGINX-API-Steering/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabriziofiorucci%2FNGINX-API-Steering/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabriziofiorucci%2FNGINX-API-Steering/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fabriziofiorucci","download_url":"https://codeload.github.com/fabriziofiorucci/NGINX-API-Steering/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabriziofiorucci%2FNGINX-API-Steering/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261073341,"owners_count":23105638,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-gateway","docker-compose","nginx","rest-api"],"created_at":"2024-11-09T12:32:55.148Z","updated_at":"2025-06-21T06:05:03.604Z","avatar_url":"https://github.com/fabriziofiorucci.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NGINX API Steering\n\n## Description\n\nThis is a sample NGINX Plus API Gateway configuration to publish REST APIs and provide:\n\n- Authentication based on Java Web Tokens (JWT)\n- Authorization based on HTTP method and JWT role match\n- Reverse proxying with URL rewriting\n- Template-based JSON payload manipulation (client-to-server and server-to-client)\n- Template-based JSON payload format validation (mandatory parameters, parameters types)\n\nAn external REST API-enabled backend is used to store JSON service definitions that include authorization and rewriting rules.\nThe service definition JSON is defined as:\n\n```\n  {\n    \"id\": 2,                                        \u003c-- Unique ID\n    \"enabled\": true,                                \u003c-- Flag to enable/disable the definition\n    \"uri\": \"v1.0/api_post\",                         \u003c-- URI to match against client request\n    \"matchRules\": {\n      \"method\": \"POST\",                             \u003c-- HTTP method to match against client request\n      \"roles\": \"devops\"                             \u003c-- JWT role to match\n    },\n    \"operation\": {\n      \"url\": \"https://api-server-2:5000/echo_data\"  \u003c-- URL to reverse proxy the client request\n    },\n    \"json\": {                                       \u003c-- JSON payload manipulation (optional)\n      \"to_server\": {                                \u003c-- client-to-server payload manipulation rules\n        \"set\": [                                    \u003c-- key:value array to add/replace in request JSON payload\n          {\n            \"field1\": \"value1\"\n          },\n          {\n            \"field2\": \"value2\"\n          }\n        ],\n        \"del\": [                                    \u003c-- Array of keys to delete from request JSON payload\n          \"group\"\n        ]\n      },\n      \"to_client\": {                                \u003c-- server-to-client payload manipulation rules\n        \"set\": [                                    \u003c-- key:value array to add/replace in response JSON payload\n          {\n            \"new_response_field\": \"ADDED\"\n          } \n        ],\n        \"del\": [                                    \u003c-- Array of keys to delete from response JSON payload\n          \"hostname\"\n        ]\n      }\n    },\n    template\": {                                    \u003c-- Template for JSON format validation\n      \"name\": \"\",                                   \u003c-- Mandatory parameter, type is string\n      \"age\": 0,                                     \u003c-- Mandatory parameter, type is integer\n      \"address\": {                                  \u003c-- Nested parameters\n        \"street\": \"\",\n        \"city\": \"\"\n      }\n    }\n  }\n```\n\nThe provided sample backend can be queried using the request URI as the lookup key.\nThe sample backend provides the `jwks.json` endpoint to return the JWT secret.\n\n## Prerequisites\n\n- Linux VM with Docker-compose v2.20.3+ (tested on Ubuntu 20.04 and 22.04) or a running Kubernetes cluster\n- NGINX Plus certificate and key to build the relevant docker image (tested with NGINX Plus R30-p1 and above)\n\n## High level architecture\n\n```mermaid\nsequenceDiagram\n    Client-\u003e\u003eNGINX Plus: REST request (with JWT token)\n    NGINX Plus-\u003e\u003eNGINX Plus: JWT Authentication\n    NGINX Plus-\u003e\u003eSource of truth: Service definition JSON request\n    Source of truth-\u003e\u003eNGINX Plus: Service definition JSON reply\n    NGINX Plus-\u003e\u003eNGINX Plus: Authorization\n    NGINX Plus-\u003e\u003eNGINX Plus: Optional JSON request rewriting\n    NGINX Plus-\u003e\u003eBackend: REST request\n    Backend-\u003e\u003eNGINX Plus: REST response\n    NGINX Plus-\u003e\u003eNGINX Plus: Optional JSON response rewriting\n    NGINX Plus-\u003e\u003eClient: Response\n```\n\n## Deploying this repository\n\n1. Clone the repository\n\n```\ngit clone https://github.com/fabriziofiorucci/NGINX-API-Steering\n```\n\n2. cd to the newly created directory\n\n```\ncd NGINX-API-Steering\n```\n\n## Running with docker-compose\n\n1. Run the startup script to build all docker images and spin up the docker-compose deployment. You will need to use a valid NGINX Plus certificate and key to fetch all software packages from the NGINX private registry\n\n```\n./nginx-api-steering.sh -o start -C /etc/ssl/nginx/nginx-repo.crt -K /etc/ssl/nginx/nginx-repo.key\n```\n\n2. Check running containers:\n\n```\n$ docker ps\nCONTAINER ID   IMAGE                COMMAND                  CREATED          STATUS          PORTS                                                                                  NAMES\n3f6f53f9786d   nginx-api-steering   \"nginx -g 'daemon of…\"   45 seconds ago   Up 43 seconds   0.0.0.0:10080-\u003e80/tcp, :::10080-\u003e80/tcp, 0.0.0.0:20080-\u003e8080/tcp, :::20080-\u003e8080/tcp   nginx\n998ddb007210   api-server           \"python apiserver.py\"    45 seconds ago   Up 43 seconds   0.0.0.0:5001-\u003e5000/tcp, :::5001-\u003e5000/tcp                                              api-server-1\nbd13b5e4ecf1   api-server           \"python apiserver.py\"    45 seconds ago   Up 43 seconds   0.0.0.0:5002-\u003e5000/tcp, :::5002-\u003e5000/tcp                                              api-server-2\n46f25f772f63   backend              \"python backend.py\"      45 seconds ago   Up 43 seconds   0.0.0.0:10000-\u003e5000/tcp, :::10000-\u003e5000/tcp                                            backend\n```\n\n## Running on Kubernetes\n\n1. Build the docker images:\n\n```\n./nginx-api-steering.sh -o build -C /etc/ssl/nginx/nginx-repo.crt -K /etc/ssl/nginx/nginx-repo.key \n```\n\n2. Make sure all images have been correctly built:\n\n```\n$ docker images\nREPOSITORY                        TAG       IMAGE ID       CREATED         SIZE\napi-server                        latest    bc502c140891   2 minutes ago   159MB\nbackend                           latest    ac783909638e   2 minutes ago   145MB\nnginx-api-steering                latest    0752ea9e5400   2 minutes ago   33.1MB\n```\n\n3. Tag and push the docker images:\n\n```\ndocker tag api-server:latest registry.k8s.ie.ff.lan:31005/nginx-api-steering:api-server\ndocker tag backend:latest registry.k8s.ie.ff.lan:31005/nginx-api-steering:backend\ndocker tag nginx-api-steering:latest registry.k8s.ie.ff.lan:31005/nginx-api-steering:nginx-api-steering\n```\n\n```\ndocker push registry.k8s.ie.ff.lan:31005/nginx-api-steering:api-server\ndocker push registry.k8s.ie.ff.lan:31005/nginx-api-steering:backend\ndocker push registry.k8s.ie.ff.lan:31005/nginx-api-steering:nginx-api-steering\n```\n\n4. Deploy all manifests:\n\n```\n./nginx-api-steering.sh -o start-k8s\n```\n\n5. Make sure all relevant objects have been created:\n\n```\n$ kubectl get all -n nginx-api-steering\nNAME                                READY   STATUS    RESTARTS   AGE\npod/api-server-1-6596f78694-nfjvm   1/1     Running   0          20s\npod/api-server-2-847868565b-5lwk8   1/1     Running   0          20s\npod/backend-74b87464fd-cmc6h        1/1     Running   0          20s\npod/nginx-9cbcb8bcd-hz6xm           1/1     Running   0          20s\n\nNAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE\nservice/api-server-1   ClusterIP   10.103.76.109   \u003cnone\u003e        5000/TCP   20s\nservice/api-server-2   ClusterIP   10.111.202.8    \u003cnone\u003e        5000/TCP   20s\nservice/backend        ClusterIP   10.107.14.237   \u003cnone\u003e        5000/TCP   20s\nservice/nginx          ClusterIP   10.97.140.134   \u003cnone\u003e        80/TCP     20s\n\nNAME                           READY   UP-TO-DATE   AVAILABLE   AGE\ndeployment.apps/api-server-1   1/1     1            1           20s\ndeployment.apps/api-server-2   1/1     1            1           20s\ndeployment.apps/backend        1/1     1            1           20s\ndeployment.apps/nginx          1/1     1            1           20s\n\nNAME                                      DESIRED   CURRENT   READY   AGE\nreplicaset.apps/api-server-1-6596f78694   1         1         1       20s\nreplicaset.apps/api-server-2-847868565b   1         1         1       20s\nreplicaset.apps/backend-74b87464fd        1         1         1       20s\nreplicaset.apps/nginx-9cbcb8bcd           1         1         1       20s\n```\n\n\n## NGINX Plus dashboard (docker-compose only)\n\nUsing your favourite browser open https://127.0.0.1:20080/dashboard.html\n\n## Creating JWT tokens\n\n(see https://www.nginx.com/blog/authenticating-api-clients-jwt-nginx-plus/)\n\nThis repository's backend DB uses a JWT secret defined as\n\n```\n$ cat jwt/jwks.json\n{\n  \"keys\": [\n    {\n      \"k\":\"ZmFudGFzdGljand0\",\n      \"kty\":\"oct\",\n      \"kid\":\"0001\"\n    }\n  ]\n}\n```\n\nthe k field is the generated symmetric key (base64url-encoded) basing on a secret (fantasticjwt in the example). The secret can be generated with the following command:\n\n```\n$ echo -n \"fantasticjwt\" | base64 | tr '+/' '-_' | tr -d '='\nZmFudGFzdGljand0\n```\n\nCreate the test JWT tokens using:\n\n```\n$ cd jwt\n$ ./jwtEncoder.sh\n```\n\nTwo tokens are created:\n\n```\njwt.devops - Token with \"devops\" role\njwt.guest - Token with \"guest\" role\n```\n\nThe decoded tokens are:\n\n```\n$ cat jwt.guest | ./jwtDecoder.sh \nHeader\n{\n  \"typ\": \"JWT\",\n  \"alg\": \"HS256\",\n  \"kid\": \"0001\",\n  \"iss\": \"Bash JWT Generator\",\n  \"iat\": 1698145320,\n  \"exp\": 1698145321\n}\nPayload\n{\n  \"name\": \"Alice Guest\",\n  \"sub\": \"JWT sub claim\",\n  \"iss\": \"JWT iss claim\",\n  \"roles\": [\n    \"guest\"\n  ]\n}\nSignature is valid\n```\n\n```\n$ cat jwt.devops | ./jwtDecoder.sh \nHeader\n{\n  \"typ\": \"JWT\",\n  \"alg\": \"HS256\",\n  \"kid\": \"0001\",\n  \"iss\": \"Bash JWT Generator\",\n  \"iat\": 1698145320,\n  \"exp\": 1698145321\n}\nPayload\n{\n  \"name\": \"Bob DevOps\",\n  \"sub\": \"JWT sub claim\",\n  \"iss\": \"JWT iss claim\",\n  \"roles\": [\n    \"devops\"\n  ]\n}\nSignature is valid\n```\n\n## Backend DB test (docker-compose only)\n\nBackend DB, fetching the JWT secret:\n\n```\n$ curl -s 127.0.0.1:10000/jwks.json | jq\n{\n  \"keys\": [\n    {\n      \"k\": \"ZmFudGFzdGljand0\",\n      \"kid\": \"0001\",\n      \"kty\": \"oct\"\n    }\n  ]\n}\n```\n\nBackend DB, fetching all keys:\n\n```\n$ curl -s 127.0.0.1:10000/backend/fetchallkeys | jq\n{\n  \"rules\": [\n    {\n      \"enabled\": true,\n      \"id\": 1,\n      \"matchRules\": {\n        \"method\": \"GET\",\n        \"roles\": \"guest\"\n      },\n      \"operation\": {\n        \"url\": \"https://api-server-1:5000/get_data\"\n      },\n      \"uri\": \"v1.0/api_get\"\n    },\n    {\n      \"enabled\": true,\n      \"id\": 2,\n      \"json\": {\n        \"to_client\": {\n          \"del\": [\n            \"hostname\"\n          ],\n          \"set\": [\n            {\n              \"new_response_field\": \"ADDED\"\n            }\n          ]\n        },\n        \"to_server\": {\n          \"del\": [\n            \"group\"\n          ],\n          \"set\": [\n            {\n              \"field1\": \"value1\"\n            },\n            {\n              \"field2\": \"value2\"\n            }\n          ]\n        }\n      },\n      \"matchRules\": {\n        \"method\": \"POST\",\n        \"roles\": \"devops\"\n      },\n      \"operation\": {\n        \"url\": \"https://api-server-2:5000/echo_data\"\n      },\n      \"uri\": \"v1.0/api_post\"\n    },\n    {\n      \"enabled\": true,\n      \"id\": 3,\n      \"matchRules\": {\n        \"method\": \"POST\",\n        \"roles\": \"devops\"\n      },\n      \"operation\": {\n        \"url\": \"https://api-server-2:5000/echo_data\"\n      },\n      \"uri\": \"v1.0/api_post_no_change\"\n    }\n  ]\n}\n```\n\nBackend DB, fetching a specific key:\n\n```\n$ curl -s http://127.0.0.1:10000/backend/fetchkey/v1.0/api_post | jq\n{\n  \"rule\": {\n    \"enabled\": true,\n    \"id\": 2,\n    \"json\": {\n      \"to_client\": {\n        \"del\": [\n          \"hostname\"\n        ],\n        \"set\": [\n          {\n            \"new_response_field\": \"ADDED\"\n          }\n        ]\n      },\n      \"to_server\": {\n        \"del\": [\n          \"group\"\n        ],\n        \"set\": [\n          {\n            \"field1\": \"value1\"\n          },\n          {\n            \"field2\": \"value2\"\n          }\n        ]\n      }\n    },\n    \"matchRules\": {\n      \"method\": \"POST\",\n      \"roles\": \"devops\"\n    },\n    \"operation\": {\n      \"url\": \"https://api-server-2:5000/echo_data\"\n    },\n    \"uri\": \"v1.0/api_post\"\n  }\n}\n```\n\n## Direct backend API access (docker-compose only):\n\nGET test:\n\n```\ncurl -ks -X GET https://127.0.0.1:5001/get_data | jq\n```\n\nOutput:\n\n```\n{\n  \"hostname\": \"be4e709e5957\",\n  \"timestamp\": \"2023-10-24 10:32:55\"\n}\n```\n\nPOST test: the client payload is echoed back in the `payload` field\n\n```\ncurl -ks -X POST https://127.0.0.1:5001/echo_data -d '{\"var\":123}' -H \"Content-Type: application/json\" | jq\n```\n\nOutput:\n\n```\n{\n  \"hostname\": \"be4e709e5957\",\n  \"payload\": {\n    \"var\": 123\n  },\n  \"timestamp\": \"2023-10-24 10:32:18\"\n}\n```\n\n## REST API access test - for docker-compose\n\nDisplay NGINX Plus logs:\n\n```\ndocker logs nginx -f\n```\n\n### Test with valid HTTP method with no JWT token\n\n```\ncurl -X GET -ki http://127.0.0.1:10080/v1.0/api_get\n```\n\nOutput:\n\n```\nHTTP/1.1 401 Unauthorized\nServer: nginx/1.25.1\nDate: Tue, 24 Oct 2023 08:38:39 GMT\nContent-Type: text/html\nContent-Length: 179\nConnection: keep-alive\nWWW-Authenticate: Bearer realm=\"authentication required\"\n\n\u003chtml\u003e\n\u003chead\u003e\u003ctitle\u003e401 Authorization Required\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n\u003ccenter\u003e\u003ch1\u003e401 Authorization Required\u003c/h1\u003e\u003c/center\u003e\n\u003chr\u003e\u003ccenter\u003enginx/1.25.1\u003c/center\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Test with valid JWT token, HTTP method and URI\n\n```\ncurl -X GET -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://127.0.0.1:10080/v1.0/api_get\n```\n\nOutput:\n\n```\nHTTP/1.1 200 OK\nServer: nginx/1.25.1\nDate: Tue, 24 Oct 2023 10:51:59 GMT\nContent-Type: application/json\nConnection: keep-alive\nContent-Length: 62\n\n{\"hostname\":\"6f0e4de1fa9b\",\"timestamp\":\"2023-10-24 10:51:59\"}\n```\n\n### Test with valid JWT token and invalid HTTP method\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://127.0.0.1:10080/v1.0/api_get\n```\n\nOutput:\n\n```\nHTTP/1.1 403 Forbidden\nServer: nginx/1.25.1\nDate: Tue, 24 Oct 2023 10:53:29 GMT\nContent-Type: text/html\nContent-Length: 153\nConnection: keep-alive\n\n\u003chtml\u003e\n\u003chead\u003e\u003ctitle\u003e403 Forbidden\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n\u003ccenter\u003e\u003ch1\u003e403 Forbidden\u003c/h1\u003e\u003c/center\u003e\n\u003chr\u003e\u003ccenter\u003enginx/1.25.1\u003c/center\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Test with valid JWT token and invalid role (`guest` instead of `devops`)\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://127.0.0.1:10080/v1.0/api_post -d '{\"username\": \"john.doe@acme.com\"}' -H \"Content-Type: application/json\"\n```\n\nOutput:\n\n```\nHTTP/1.1 403 Forbidden\nServer: nginx/1.25.1\nDate: Tue, 24 Oct 2023 10:55:17 GMT\nContent-Type: text/html\nContent-Length: 153\nConnection: keep-alive\n\n\u003chtml\u003e\n\u003chead\u003e\u003ctitle\u003e403 Forbidden\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n\u003ccenter\u003e\u003ch1\u003e403 Forbidden\u003c/h1\u003e\u003c/center\u003e\n\u003chr\u003e\u003ccenter\u003enginx/1.25.1\u003c/center\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Same request with a valid JWT token and `devops` role\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.devops`\" http://127.0.0.1:10080/v1.0/api_post -d '{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}' -H \"Content-Type: application/json\"\n```\n\nOutput:\n\n```\nHTTP/1.1 200 OK\nServer: nginx/1.25.1\nDate: Wed, 01 Nov 2023 14:55:06 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\n\n{\"payload\":{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:59:06\",\"new_response_field\":\"ADDED\"}\n```\n\nThe JSON service definition retrieved from the backend is:\n\n```\n  {\n    \"id\": 2,\n    \"enabled\": true,\n    \"uri\": \"v1.0/api_post\",\n    \"matchRules\": {\n      \"method\": \"POST\",\n      \"roles\": \"devops\"\n    },\n    \"operation\": {\n      \"url\": \"https://api-server-2:5000/echo_data\"\n    },\n    \"json\": {\n      \"to_server\": {\n        \"set\": [\n          {\n            \"field1\": \"value1\"\n          },\n          {\n            \"field2\": \"value2\"\n          }\n        ],\n        \"del\": [\n          \"group\"\n        ]\n      },\n      \"to_client\": {\n        \"set\": [\n          {\n            \"new_response_field\": \"ADDED\"\n          },\n        \"del\": [\n          \"hostname\"\n        ]\n      }\n    }\n  }\n```\n\nClient request payload is:\n\n```\n{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}\n```\n\nBased on the JSON service definition `to_server` section:\n\n- \"field1\": \"value1\" is added\n- \"field2\": \"value2\" is added\n- \"group\" is removed\n\nThe updated payload:\n\n```\n{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"}\n```\n\nis sent to the upstream. Upstream response is:\n\n```\n{\"hostname\":\"cf97af39bbd6\",\"payload\":{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:59:06\"}\n```\n\nThe response payload is updated based on the JSON service definition `to_client` section:\n\n- \"new_response_field\": \"ADDED\" is added\n- \"hostname\" is removed\n\nThe resulting payload is returned to the client as:\n\n```\n{\"payload\":{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:59:06\",\"new_response_field\":\"ADDED\"}\n```\n\nNGINX logs:\n\n```\n$ docker logs nginx -f\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- CLIENT REQUEST ---------------------------\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Client[10.5.0.1] Method[POST] Host[127.0.0.1:10080] URI [/v1.0/api_post] Body[{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/api_post]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Rule found: URI[/dbQuery/backend/fetchkey/v1.0/api_post] status[200] body[{\"rule\":{\"enabled\":true,\"id\":2,\"json\":{\"to_client\":{\"del\":[\"hostname\"],\"set\":[{\"new_response_field\":\"ADDED\"}]},\"to_server\":{\"del\":[\"group\"],\"set\":[{\"field1\":\"value1\"},{\"field2\":\"value2\"}]}},\"matchRules\":{\"method\":\"POST\",\"roles\":\"devops\"},\"operation\":{\"url\":\"https://api-server-2:5000/echo_data\"},\"uri\":\"v1.0/api_post\"}}\n]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Rewriting request [127.0.0.1:10080/v1.0/api_post] -\u003e [https://api-server-2:5000/echo_data]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Checking authorization\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - HTTP method received [POST] -\u003e needed [POST]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - JWT roles received [devops] -\u003e needed [devops]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Authorization successful\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- JSON payload client -\u003e server : being updated\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Updating JSON payload [{\"username\":\"john.doe@acme.com\",\"group\":\"guest\"}] with template [{\"del\":[\"group\"],\"set\":[{\"field1\":\"value1\"},{\"field2\":\"value2\"}]}]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - Updating [field1 = value1]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - Updating [field2 = value2]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - Deleting [group]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Done updating JSON payload [{\"username\":\"john.doe@acme.com\",\"field1\":\"value1\",\"field2\":\"value2\"}]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Proxying request to upstream\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Upstream returned HTTP [200] payload [{\"hostname\":\"cf97af39bbd6\",\"payload\":{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:59:06\"}\n]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: --- JSON payload server -\u003e client : being updated\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Updating JSON payload [{\"hostname\":\"cf97af39bbd6\",\"payload\":{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:59:06\"}] with template [{\"del\":[\"hostname\"],\"set\":[{\"new_response_field\":\"ADDED\"}]}]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - Updating [new_response_field = ADDED]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: - Deleting [hostname]\n2023/11/01 14:59:06 [warn] 6#6: *9 js: Done updating JSON payload [{\"payload\":{\"field1\":\"value1\",\"field2\":\"value2\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:59:06\",\"new_response_field\":\"ADDED\"}]\n```\n\n### Test with no payload rewriting\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.devops`\" http://127.0.0.1:10080/v1.0/api_post_no_change -d '{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}' -H \"Content-Type: application/json\"\n```\n\nOutput:\n\n```\nHTTP/1.1 200 OK\nServer: nginx/1.25.1\nDate: Wed, 01 Nov 2023 14:22:47 GMT\nContent-Type: application/octet-stream\nTransfer-Encoding: chunked\nConnection: keep-alive\n\n{\"hostname\":\"b93ecf5e10c5\",\"payload\":{\"group\":\"guest\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:22:47\"}\n```\n\nNGINX logs:\n\n```\n$ docker logs nginx -f\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- CLIENT REQUEST ---------------------------\n2023/11/01 14:22:47 [warn] 7#7: *13 js: Client[10.5.0.1] Method[POST] Host[127.0.0.1:10080] URI [/v1.0/api_post_no_change] Body[{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/api_post_no_change]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: Rule found: URI[/dbQuery/backend/fetchkey/v1.0/api_post_no_change] status[200] body[{\"rule\":{\"enabled\":true,\"id\":3,\"matchRules\":{\"method\":\"POST\",\"roles\":\"devops\"},\"operation\":{\"url\":\"https://api-server-2:5000/echo_data\"},\"uri\":\"v1.0/api_post_no_change\"}}\n]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: Rewriting request [127.0.0.1:10080/v1.0/api_post_no_change] -\u003e [https://api-server-2:5000/echo_data]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Checking authorization\n2023/11/01 14:22:47 [warn] 7#7: *13 js: - HTTP method received [POST] -\u003e needed [POST]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: - JWT roles received [devops] -\u003e needed [devops]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Authorization successful\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- JSON payload client -\u003e server : no changes\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Proxying request to upstream\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Upstream returned HTTP [200] payload [{\"hostname\":\"b93ecf5e10c5\",\"payload\":{\"group\":\"guest\",\"username\":\"john.doe@acme.com\"},\"timestamp\":\"2023-11-01 14:22:47\"}\n]\n2023/11/01 14:22:47 [warn] 7#7: *13 js: --- JSON payload server -\u003e client : no changes\n10.5.0.1 - - [01/Nov/2023:14:22:47 +0000] \"POST /v1.0/api_post_no_change HTTP/1.1\" 200 132 \"-\" \"curl/7.68.0\" \"-\"\n2023/11/01 14:22:47 [info] 7#7: *13 client 10.5.0.1 closed keepalive connection\n```\n\n### Test with JSON payload checked against template\n\n```\ncurl -w '\\n' -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://127.0.0.1:10080/v1.0/template_test -d '\n{\n  \"name\": \"John\",\n  \"age\": 30,\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"New York\"\n  }\n}\n' -H \"Content-Type: application/json\"\n```\n\nThe JSON service definition retrieved from the backend is:\n\n```\n {\n    \"id\": 4,\n    \"enabled\": true,\n    \"uri\": \"v1.0/template_test\",\n    \"matchRules\": {\n      \"method\": \"POST\",\n      \"roles\": \"guest\"\n    },\n    \"operation\": {\n      \"url\": \"https://api-server-2:5000/echo_data\"\n    },\n    \"template\": {\n      \"name\": \"\",\n      \"age\": 0,\n      \"address\": {\n        \"street\": \"\",\n        \"city\": \"\"\n      }\n    }\n  }\n```\n\nOutput:\n\n```\nHTTP/1.1 200 OK\nServer: nginx/1.25.3\nDate: Thu, 28 Mar 2024 16:38:41 GMT\nContent-Type: application/json\nTransfer-Encoding: chunked\nConnection: keep-alive\n\n{\"hostname\":\"6c8c15e6b178\",\"payload\":{\"address\":{\"city\":\"New York\",\"street\":\"123 Main St\"},\"age\":30,\"name\":\"John\"},\"timestamp\":\"2024-03-28 16:38:41\"}\n```\n\nNGINX logs:\n\n```\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- CLIENT REQUEST ---------------------------\n2024/03/28 17:13:33 [warn] 6#6: *45 js: Client[10.5.0.1] Method[POST] Host[127.0.0.1:10080] URI [/v1.0/template_test] Body[\n{\n  \"name\": \"John\",\n  \"age\": 30,\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"New York\"\n  }\n}\n]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/template_test]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: Rule found: URI[/dbQuery/backend/fetchkey/v1.0/template_test] status[200] body[{\"rule\":{\"enabled\":true,\"id\":4,\"matchRules\":{\"method\":\"POST\",\"roles\":\"guest\"},\"operation\":{\"url\":\"https://api-server-2:5000/echo_data\"},\"template\":{\"address\":{\"city\":\"\",\"street\":\"\"},\"age\":0,\"name\":\"\"},\"uri\":\"v1.0/template_test\"}}\n]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: Rewriting request [127.0.0.1:10080/v1.0/template_test] -\u003e [https://api-server-2:5000/echo_data]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Checking authorization\n2024/03/28 17:13:33 [warn] 6#6: *45 js: - HTTP method received [POST] -\u003e needed [POST]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: - JWT roles received [guest] -\u003e needed [guest]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Authorization successful\n2024/03/28 17:13:33 [warn] 6#6: *45 js: +-- JSON template validation [{\"address\":{\"city\":\"\",\"street\":\"\"},\"age\":0,\"name\":\"\"}]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |-- Checking JSON payload [[object Object]]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |-- Checking JSON payload [[object Object]]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [city] ok\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [street] ok\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [address] ok\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [age] ok\n2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [name] ok\n2024/03/28 17:13:33 [warn] 6#6: *45 js: +-- JSON template validation successful\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- JSON payload client -\u003e server : no changes\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Proxying request to upstream\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Upstream returned HTTP [200] payload [{\"hostname\":\"6c8c15e6b178\",\"payload\":{\"address\":{\"city\":\"New York\",\"street\":\"123 Main St\"},\"age\":30,\"name\":\"John\"},\"timestamp\":\"2024-03-28 17:13:33\"}\n]\n2024/03/28 17:13:33 [warn] 6#6: *45 js: --- JSON payload server -\u003e client : no changes\n2024/03/28 17:13:33 [info] 6#6: *45 client 10.5.0.1 closed keepalive connection\n```\n\n### Test with invalid JSON payload check against template\n\n```\ncurl -w '\\n' -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://127.0.0.1:10080/v1.0/template_test -d '\n{\n  \"name\": \"John\",\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"New York\"\n  }\n}\n' -H \"Content-Type: application/json\"\n```\n\nOutput:\n\n```\nHTTP/1.1 422 \nServer: nginx/1.25.3\nDate: Thu, 28 Mar 2024 17:15:29 GMT\nContent-Length: 0\nConnection: keep-alive\n```\n\nNGINX logs:\n\n```\n2024/03/28 17:15:29 [warn] 6#6: *49 js: --- CLIENT REQUEST ---------------------------\n2024/03/28 17:15:29 [warn] 6#6: *49 js: Client[10.5.0.1] Method[POST] Host[127.0.0.1:10080] URI [/v1.0/template_test] Body[\n{\n  \"name\": \"John\",\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"New York\"\n  }\n}\n]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/template_test]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: Rule found: URI[/dbQuery/backend/fetchkey/v1.0/template_test] status[200] body[{\"rule\":{\"enabled\":true,\"id\":4,\"matchRules\":{\"method\":\"POST\",\"roles\":\"guest\"},\"operation\":{\"url\":\"https://api-server-2:5000/echo_data\"},\"template\":{\"address\":{\"city\":\"\",\"street\":\"\"},\"age\":0,\"name\":\"\"},\"uri\":\"v1.0/template_test\"}}\n]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: Rewriting request [127.0.0.1:10080/v1.0/template_test] -\u003e [https://api-server-2:5000/echo_data]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: --- Checking authorization\n2024/03/28 17:15:29 [warn] 6#6: *49 js: - HTTP method received [POST] -\u003e needed [POST]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: - JWT roles received [guest] -\u003e needed [guest]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: --- Authorization successful\n2024/03/28 17:15:29 [warn] 6#6: *49 js: +-- JSON template validation [{\"address\":{\"city\":\"\",\"street\":\"\"},\"age\":0,\"name\":\"\"}]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: |-- Checking JSON payload [[object Object]]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: |-- Checking JSON payload [[object Object]]\n2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [city] ok\n2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [street] ok\n2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [address] ok\n2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [age] missing\n2024/03/28 17:15:29 [warn] 6#6: *49 js: +-- JSON template validation failed\n2024/03/28 17:15:29 [info] 6#6: *49 client 10.5.0.1 closed keepalive connection\n```\n\n## REST API access test - for Kubernetes\n\nNote: the example FQDN `nginx-api-steering.k8s.f5.ff.lan` is used. This is defined in the `kubernetes.yaml` file.\n\nDisplay NGINX Plus logs:\n\n```\nkubectl logs -l app=nginx -n nginx-api-steering -f\n```\n\nTest with valid HTTP method with no JWT token (returns 401):\n\n```\ncurl -X GET -ki http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_get\n```\n\nTest with valid JWT token, HTTP method and URI (returns 200):\n\n```\ncurl -X GET -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_get\n```\n\nTest with valid JWT token and invalid HTTP method (return 403):\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_get\n```\n\nTest with valid JWT token and invalid `guest` role (returns 403):\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_post -d '{\"username\": \"john.doe@acme.com\"}' -H \"Content-Type: application/json\"\n```\n\nTest with valid JWT token and valid `devops` role (returns 200):\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.devops`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_post -d '{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}' -H \"Content-Type: application/json\"\n```\n\nTest with no payload rewriting (returns 200):\n\n```\ncurl -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.devops`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_post_no_change -d '{\"username\": \"john.doe@acme.com\", \"group\": \"guest\"}' -H \"Content-Type: application/json\"\n```\n\nTest with JSON payload checked against template (returns 200):\n\n```\ncurl -w '\\n' -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/template_test -d '\n{\n  \"name\": \"John\",\n  \"age\": 30,\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"New York\"\n  }\n}\n' -H \"Content-Type: application/json\"\n```\n\nTest with invalid JSON payload check against template (returns 422):\n\n```\ncurl -w '\\n' -X POST -ki -H \"Authorization: Bearer `cat jwt/jwt.guest`\" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/template_test -d '\n{\n  \"name\": \"John\",\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"New York\"\n  }\n}\n' -H \"Content-Type: application/json\"\n```\n\n## Deployment removal\n\nFor docker-compose:\n\n```\n./nginx-api-steering.sh -o stop\n```\n\nFor Kubernetes:\n\n```\n./nginx-api-steering.sh -o stop-k8s\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabriziofiorucci%2Fnginx-api-steering","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffabriziofiorucci%2Fnginx-api-steering","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabriziofiorucci%2Fnginx-api-steering/lists"}