Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/fabriziofiorucci/nginx-api-steering
NGINX Plus API Gateway configuration to publish REST APIs, authenticate, authorize and manipulate JSON payloads
https://github.com/fabriziofiorucci/nginx-api-steering
api-gateway docker-compose nginx rest-api
Last synced: 11 days ago
JSON representation
NGINX Plus API Gateway configuration to publish REST APIs, authenticate, authorize and manipulate JSON payloads
- Host: GitHub
- URL: https://github.com/fabriziofiorucci/nginx-api-steering
- Owner: fabriziofiorucci
- Created: 2023-11-01T17:37:47.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2024-05-15T14:03:47.000Z (6 months ago)
- Last Synced: 2024-05-16T03:20:54.530Z (6 months ago)
- Topics: api-gateway, docker-compose, nginx, rest-api
- Language: JavaScript
- Homepage:
- Size: 32.2 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# NGINX API Steering
## Description
This is a sample NGINX Plus API Gateway configuration to publish REST APIs and provide:
- Authentication based on Java Web Tokens (JWT)
- Authorization based on HTTP method and JWT role match
- Reverse proxying with URL rewriting
- Template-based JSON payload manipulation (client-to-server and server-to-client)
- Template-based JSON payload format validation (mandatory parameters, parameters types)An external REST API-enabled backend is used to store JSON service definitions that include authorization and rewriting rules.
The service definition JSON is defined as:```
{
"id": 2, <-- Unique ID
"enabled": true, <-- Flag to enable/disable the definition
"uri": "v1.0/api_post", <-- URI to match against client request
"matchRules": {
"method": "POST", <-- HTTP method to match against client request
"roles": "devops" <-- JWT role to match
},
"operation": {
"url": "https://api-server-2:5000/echo_data" <-- URL to reverse proxy the client request
},
"json": { <-- JSON payload manipulation (optional)
"to_server": { <-- client-to-server payload manipulation rules
"set": [ <-- key:value array to add/replace in request JSON payload
{
"field1": "value1"
},
{
"field2": "value2"
}
],
"del": [ <-- Array of keys to delete from request JSON payload
"group"
]
},
"to_client": { <-- server-to-client payload manipulation rules
"set": [ <-- key:value array to add/replace in response JSON payload
{
"new_response_field": "ADDED"
}
],
"del": [ <-- Array of keys to delete from response JSON payload
"hostname"
]
}
},
template": { <-- Template for JSON format validation
"name": "", <-- Mandatory parameter, type is string
"age": 0, <-- Mandatory parameter, type is integer
"address": { <-- Nested parameters
"street": "",
"city": ""
}
}
}
```The provided sample backend can be queried using the request URI as the lookup key.
The sample backend provides the `jwks.json` endpoint to return the JWT secret.## Prerequisites
- Linux VM with Docker-compose v2.20.3+ (tested on Ubuntu 20.04 and 22.04) or a running Kubernetes cluster
- NGINX Plus certificate and key to build the relevant docker image (tested with NGINX Plus R30-p1 and above)## High level architecture
```mermaid
sequenceDiagram
Client->>NGINX Plus: REST request (with JWT token)
NGINX Plus->>NGINX Plus: JWT Authentication
NGINX Plus->>Source of truth: Service definition JSON request
Source of truth->>NGINX Plus: Service definition JSON reply
NGINX Plus->>NGINX Plus: Authorization
NGINX Plus->>NGINX Plus: Optional JSON request rewriting
NGINX Plus->>Backend: REST request
Backend->>NGINX Plus: REST response
NGINX Plus->>NGINX Plus: Optional JSON response rewriting
NGINX Plus->>Client: Response
```## Deploying this repository
1. Clone the repository
```
git clone https://github.com/fabriziofiorucci/NGINX-API-Steering
```2. cd to the newly created directory
```
cd NGINX-API-Steering
```## Running with docker-compose
1. 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
```
./nginx-api-steering.sh -o start -C /etc/ssl/nginx/nginx-repo.crt -K /etc/ssl/nginx/nginx-repo.key
```2. Check running containers:
```
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f6f53f9786d nginx-api-steering "nginx -g 'daemon of…" 45 seconds ago Up 43 seconds 0.0.0.0:10080->80/tcp, :::10080->80/tcp, 0.0.0.0:20080->8080/tcp, :::20080->8080/tcp nginx
998ddb007210 api-server "python apiserver.py" 45 seconds ago Up 43 seconds 0.0.0.0:5001->5000/tcp, :::5001->5000/tcp api-server-1
bd13b5e4ecf1 api-server "python apiserver.py" 45 seconds ago Up 43 seconds 0.0.0.0:5002->5000/tcp, :::5002->5000/tcp api-server-2
46f25f772f63 backend "python backend.py" 45 seconds ago Up 43 seconds 0.0.0.0:10000->5000/tcp, :::10000->5000/tcp backend
```## Running on Kubernetes
1. Build the docker images:
```
./nginx-api-steering.sh -o build -C /etc/ssl/nginx/nginx-repo.crt -K /etc/ssl/nginx/nginx-repo.key
```2. Make sure all images have been correctly built:
```
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
api-server latest bc502c140891 2 minutes ago 159MB
backend latest ac783909638e 2 minutes ago 145MB
nginx-api-steering latest 0752ea9e5400 2 minutes ago 33.1MB
```3. Tag and push the docker images:
```
docker tag api-server:latest registry.k8s.ie.ff.lan:31005/nginx-api-steering:api-server
docker tag backend:latest registry.k8s.ie.ff.lan:31005/nginx-api-steering:backend
docker tag nginx-api-steering:latest registry.k8s.ie.ff.lan:31005/nginx-api-steering:nginx-api-steering
``````
docker push registry.k8s.ie.ff.lan:31005/nginx-api-steering:api-server
docker push registry.k8s.ie.ff.lan:31005/nginx-api-steering:backend
docker push registry.k8s.ie.ff.lan:31005/nginx-api-steering:nginx-api-steering
```4. Deploy all manifests:
```
./nginx-api-steering.sh -o start-k8s
```5. Make sure all relevant objects have been created:
```
$ kubectl get all -n nginx-api-steering
NAME READY STATUS RESTARTS AGE
pod/api-server-1-6596f78694-nfjvm 1/1 Running 0 20s
pod/api-server-2-847868565b-5lwk8 1/1 Running 0 20s
pod/backend-74b87464fd-cmc6h 1/1 Running 0 20s
pod/nginx-9cbcb8bcd-hz6xm 1/1 Running 0 20sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/api-server-1 ClusterIP 10.103.76.109 5000/TCP 20s
service/api-server-2 ClusterIP 10.111.202.8 5000/TCP 20s
service/backend ClusterIP 10.107.14.237 5000/TCP 20s
service/nginx ClusterIP 10.97.140.134 80/TCP 20sNAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api-server-1 1/1 1 1 20s
deployment.apps/api-server-2 1/1 1 1 20s
deployment.apps/backend 1/1 1 1 20s
deployment.apps/nginx 1/1 1 1 20sNAME DESIRED CURRENT READY AGE
replicaset.apps/api-server-1-6596f78694 1 1 1 20s
replicaset.apps/api-server-2-847868565b 1 1 1 20s
replicaset.apps/backend-74b87464fd 1 1 1 20s
replicaset.apps/nginx-9cbcb8bcd 1 1 1 20s
```## NGINX Plus dashboard (docker-compose only)
Using your favourite browser open https://127.0.0.1:20080/dashboard.html
## Creating JWT tokens
(see https://www.nginx.com/blog/authenticating-api-clients-jwt-nginx-plus/)
This repository's backend DB uses a JWT secret defined as
```
$ cat jwt/jwks.json
{
"keys": [
{
"k":"ZmFudGFzdGljand0",
"kty":"oct",
"kid":"0001"
}
]
}
```the 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:
```
$ echo -n "fantasticjwt" | base64 | tr '+/' '-_' | tr -d '='
ZmFudGFzdGljand0
```Create the test JWT tokens using:
```
$ cd jwt
$ ./jwtEncoder.sh
```Two tokens are created:
```
jwt.devops - Token with "devops" role
jwt.guest - Token with "guest" role
```The decoded tokens are:
```
$ cat jwt.guest | ./jwtDecoder.sh
Header
{
"typ": "JWT",
"alg": "HS256",
"kid": "0001",
"iss": "Bash JWT Generator",
"iat": 1698145320,
"exp": 1698145321
}
Payload
{
"name": "Alice Guest",
"sub": "JWT sub claim",
"iss": "JWT iss claim",
"roles": [
"guest"
]
}
Signature is valid
``````
$ cat jwt.devops | ./jwtDecoder.sh
Header
{
"typ": "JWT",
"alg": "HS256",
"kid": "0001",
"iss": "Bash JWT Generator",
"iat": 1698145320,
"exp": 1698145321
}
Payload
{
"name": "Bob DevOps",
"sub": "JWT sub claim",
"iss": "JWT iss claim",
"roles": [
"devops"
]
}
Signature is valid
```## Backend DB test (docker-compose only)
Backend DB, fetching the JWT secret:
```
$ curl -s 127.0.0.1:10000/jwks.json | jq
{
"keys": [
{
"k": "ZmFudGFzdGljand0",
"kid": "0001",
"kty": "oct"
}
]
}
```Backend DB, fetching all keys:
```
$ curl -s 127.0.0.1:10000/backend/fetchallkeys | jq
{
"rules": [
{
"enabled": true,
"id": 1,
"matchRules": {
"method": "GET",
"roles": "guest"
},
"operation": {
"url": "https://api-server-1:5000/get_data"
},
"uri": "v1.0/api_get"
},
{
"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"
},
{
"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"
}
]
}
```Backend DB, fetching a specific key:
```
$ curl -s http://127.0.0.1:10000/backend/fetchkey/v1.0/api_post | jq
{
"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"
}
}
```## Direct backend API access (docker-compose only):
GET test:
```
curl -ks -X GET https://127.0.0.1:5001/get_data | jq
```Output:
```
{
"hostname": "be4e709e5957",
"timestamp": "2023-10-24 10:32:55"
}
```POST test: the client payload is echoed back in the `payload` field
```
curl -ks -X POST https://127.0.0.1:5001/echo_data -d '{"var":123}' -H "Content-Type: application/json" | jq
```Output:
```
{
"hostname": "be4e709e5957",
"payload": {
"var": 123
},
"timestamp": "2023-10-24 10:32:18"
}
```## REST API access test - for docker-compose
Display NGINX Plus logs:
```
docker logs nginx -f
```### Test with valid HTTP method with no JWT token
```
curl -X GET -ki http://127.0.0.1:10080/v1.0/api_get
```Output:
```
HTTP/1.1 401 Unauthorized
Server: nginx/1.25.1
Date: Tue, 24 Oct 2023 08:38:39 GMT
Content-Type: text/html
Content-Length: 179
Connection: keep-alive
WWW-Authenticate: Bearer realm="authentication required"401 Authorization Required
401 Authorization Required
nginx/1.25.1```
### Test with valid JWT token, HTTP method and URI
```
curl -X GET -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://127.0.0.1:10080/v1.0/api_get
```Output:
```
HTTP/1.1 200 OK
Server: nginx/1.25.1
Date: Tue, 24 Oct 2023 10:51:59 GMT
Content-Type: application/json
Connection: keep-alive
Content-Length: 62{"hostname":"6f0e4de1fa9b","timestamp":"2023-10-24 10:51:59"}
```### Test with valid JWT token and invalid HTTP method
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://127.0.0.1:10080/v1.0/api_get
```Output:
```
HTTP/1.1 403 Forbidden
Server: nginx/1.25.1
Date: Tue, 24 Oct 2023 10:53:29 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-alive403 Forbidden
403 Forbidden
nginx/1.25.1```
### Test with valid JWT token and invalid role (`guest` instead of `devops`)
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://127.0.0.1:10080/v1.0/api_post -d '{"username": "[email protected]"}' -H "Content-Type: application/json"
```Output:
```
HTTP/1.1 403 Forbidden
Server: nginx/1.25.1
Date: Tue, 24 Oct 2023 10:55:17 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-alive403 Forbidden
403 Forbidden
nginx/1.25.1```
### Same request with a valid JWT token and `devops` role
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.devops`" http://127.0.0.1:10080/v1.0/api_post -d '{"username": "[email protected]", "group": "guest"}' -H "Content-Type: application/json"
```Output:
```
HTTP/1.1 200 OK
Server: nginx/1.25.1
Date: Wed, 01 Nov 2023 14:55:06 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive{"payload":{"field1":"value1","field2":"value2","username":"[email protected]"},"timestamp":"2023-11-01 14:59:06","new_response_field":"ADDED"}
```The JSON service definition retrieved from the backend is:
```
{
"id": 2,
"enabled": true,
"uri": "v1.0/api_post",
"matchRules": {
"method": "POST",
"roles": "devops"
},
"operation": {
"url": "https://api-server-2:5000/echo_data"
},
"json": {
"to_server": {
"set": [
{
"field1": "value1"
},
{
"field2": "value2"
}
],
"del": [
"group"
]
},
"to_client": {
"set": [
{
"new_response_field": "ADDED"
},
"del": [
"hostname"
]
}
}
}
```Client request payload is:
```
{"username": "[email protected]", "group": "guest"}
```Based on the JSON service definition `to_server` section:
- "field1": "value1" is added
- "field2": "value2" is added
- "group" is removedThe updated payload:
```
{"field1":"value1","field2":"value2","username":"[email protected]"}
```is sent to the upstream. Upstream response is:
```
{"hostname":"cf97af39bbd6","payload":{"field1":"value1","field2":"value2","username":"[email protected]"},"timestamp":"2023-11-01 14:59:06"}
```The response payload is updated based on the JSON service definition `to_client` section:
- "new_response_field": "ADDED" is added
- "hostname" is removedThe resulting payload is returned to the client as:
```
{"payload":{"field1":"value1","field2":"value2","username":"[email protected]"},"timestamp":"2023-11-01 14:59:06","new_response_field":"ADDED"}
```NGINX logs:
```
$ docker logs nginx -f
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- CLIENT REQUEST ---------------------------
2023/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": "[email protected]", "group": "guest"}]
2023/11/01 14:59:06 [warn] 6#6: *9 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/api_post]
2023/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"}}
]
2023/11/01 14:59:06 [warn] 6#6: *9 js: Rewriting request [127.0.0.1:10080/v1.0/api_post] -> [https://api-server-2:5000/echo_data]
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Checking authorization
2023/11/01 14:59:06 [warn] 6#6: *9 js: - HTTP method received [POST] -> needed [POST]
2023/11/01 14:59:06 [warn] 6#6: *9 js: - JWT roles received [devops] -> needed [devops]
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Authorization successful
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- JSON payload client -> server : being updated
2023/11/01 14:59:06 [warn] 6#6: *9 js: Updating JSON payload [{"username":"[email protected]","group":"guest"}] with template [{"del":["group"],"set":[{"field1":"value1"},{"field2":"value2"}]}]
2023/11/01 14:59:06 [warn] 6#6: *9 js: - Updating [field1 = value1]
2023/11/01 14:59:06 [warn] 6#6: *9 js: - Updating [field2 = value2]
2023/11/01 14:59:06 [warn] 6#6: *9 js: - Deleting [group]
2023/11/01 14:59:06 [warn] 6#6: *9 js: Done updating JSON payload [{"username":"[email protected]","field1":"value1","field2":"value2"}]
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Proxying request to upstream
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- Upstream returned HTTP [200] payload [{"hostname":"cf97af39bbd6","payload":{"field1":"value1","field2":"value2","username":"[email protected]"},"timestamp":"2023-11-01 14:59:06"}
]
2023/11/01 14:59:06 [warn] 6#6: *9 js: --- JSON payload server -> client : being updated
2023/11/01 14:59:06 [warn] 6#6: *9 js: Updating JSON payload [{"hostname":"cf97af39bbd6","payload":{"field1":"value1","field2":"value2","username":"[email protected]"},"timestamp":"2023-11-01 14:59:06"}] with template [{"del":["hostname"],"set":[{"new_response_field":"ADDED"}]}]
2023/11/01 14:59:06 [warn] 6#6: *9 js: - Updating [new_response_field = ADDED]
2023/11/01 14:59:06 [warn] 6#6: *9 js: - Deleting [hostname]
2023/11/01 14:59:06 [warn] 6#6: *9 js: Done updating JSON payload [{"payload":{"field1":"value1","field2":"value2","username":"[email protected]"},"timestamp":"2023-11-01 14:59:06","new_response_field":"ADDED"}]
```### Test with no payload rewriting
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.devops`" http://127.0.0.1:10080/v1.0/api_post_no_change -d '{"username": "[email protected]", "group": "guest"}' -H "Content-Type: application/json"
```Output:
```
HTTP/1.1 200 OK
Server: nginx/1.25.1
Date: Wed, 01 Nov 2023 14:22:47 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive{"hostname":"b93ecf5e10c5","payload":{"group":"guest","username":"[email protected]"},"timestamp":"2023-11-01 14:22:47"}
```NGINX logs:
```
$ docker logs nginx -f
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- CLIENT REQUEST ---------------------------
2023/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": "[email protected]", "group": "guest"}]
2023/11/01 14:22:47 [warn] 7#7: *13 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/api_post_no_change]
2023/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"}}
]
2023/11/01 14:22:47 [warn] 7#7: *13 js: Rewriting request [127.0.0.1:10080/v1.0/api_post_no_change] -> [https://api-server-2:5000/echo_data]
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Checking authorization
2023/11/01 14:22:47 [warn] 7#7: *13 js: - HTTP method received [POST] -> needed [POST]
2023/11/01 14:22:47 [warn] 7#7: *13 js: - JWT roles received [devops] -> needed [devops]
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Authorization successful
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- JSON payload client -> server : no changes
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Proxying request to upstream
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- Upstream returned HTTP [200] payload [{"hostname":"b93ecf5e10c5","payload":{"group":"guest","username":"[email protected]"},"timestamp":"2023-11-01 14:22:47"}
]
2023/11/01 14:22:47 [warn] 7#7: *13 js: --- JSON payload server -> client : no changes
10.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" "-"
2023/11/01 14:22:47 [info] 7#7: *13 client 10.5.0.1 closed keepalive connection
```### Test with JSON payload checked against template
```
curl -w '\n' -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://127.0.0.1:10080/v1.0/template_test -d '
{
"name": "John",
"age": 30,
"address": {
"street": "123 Main St",
"city": "New York"
}
}
' -H "Content-Type: application/json"
```The JSON service definition retrieved from the backend is:
```
{
"id": 4,
"enabled": true,
"uri": "v1.0/template_test",
"matchRules": {
"method": "POST",
"roles": "guest"
},
"operation": {
"url": "https://api-server-2:5000/echo_data"
},
"template": {
"name": "",
"age": 0,
"address": {
"street": "",
"city": ""
}
}
}
```Output:
```
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Thu, 28 Mar 2024 16:38:41 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive{"hostname":"6c8c15e6b178","payload":{"address":{"city":"New York","street":"123 Main St"},"age":30,"name":"John"},"timestamp":"2024-03-28 16:38:41"}
```NGINX logs:
```
2024/03/28 17:13:33 [warn] 6#6: *45 js: --- CLIENT REQUEST ---------------------------
2024/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[
{
"name": "John",
"age": 30,
"address": {
"street": "123 Main St",
"city": "New York"
}
}
]
2024/03/28 17:13:33 [warn] 6#6: *45 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/template_test]
2024/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"}}
]
2024/03/28 17:13:33 [warn] 6#6: *45 js: Rewriting request [127.0.0.1:10080/v1.0/template_test] -> [https://api-server-2:5000/echo_data]
2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Checking authorization
2024/03/28 17:13:33 [warn] 6#6: *45 js: - HTTP method received [POST] -> needed [POST]
2024/03/28 17:13:33 [warn] 6#6: *45 js: - JWT roles received [guest] -> needed [guest]
2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Authorization successful
2024/03/28 17:13:33 [warn] 6#6: *45 js: +-- JSON template validation [{"address":{"city":"","street":""},"age":0,"name":""}]
2024/03/28 17:13:33 [warn] 6#6: *45 js: |-- Checking JSON payload [[object Object]]
2024/03/28 17:13:33 [warn] 6#6: *45 js: |-- Checking JSON payload [[object Object]]
2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [city] ok
2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [street] ok
2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [address] ok
2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [age] ok
2024/03/28 17:13:33 [warn] 6#6: *45 js: |---- Property [name] ok
2024/03/28 17:13:33 [warn] 6#6: *45 js: +-- JSON template validation successful
2024/03/28 17:13:33 [warn] 6#6: *45 js: --- JSON payload client -> server : no changes
2024/03/28 17:13:33 [warn] 6#6: *45 js: --- Proxying request to upstream
2024/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"}
]
2024/03/28 17:13:33 [warn] 6#6: *45 js: --- JSON payload server -> client : no changes
2024/03/28 17:13:33 [info] 6#6: *45 client 10.5.0.1 closed keepalive connection
```### Test with invalid JSON payload check against template
```
curl -w '\n' -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://127.0.0.1:10080/v1.0/template_test -d '
{
"name": "John",
"address": {
"street": "123 Main St",
"city": "New York"
}
}
' -H "Content-Type: application/json"
```Output:
```
HTTP/1.1 422
Server: nginx/1.25.3
Date: Thu, 28 Mar 2024 17:15:29 GMT
Content-Length: 0
Connection: keep-alive
```NGINX logs:
```
2024/03/28 17:15:29 [warn] 6#6: *49 js: --- CLIENT REQUEST ---------------------------
2024/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[
{
"name": "John",
"address": {
"street": "123 Main St",
"city": "New York"
}
}
]
2024/03/28 17:15:29 [warn] 6#6: *49 js: Subrequest [/dbQuery/backend/fetchkey/v1.0/template_test]
2024/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"}}
]
2024/03/28 17:15:29 [warn] 6#6: *49 js: Rewriting request [127.0.0.1:10080/v1.0/template_test] -> [https://api-server-2:5000/echo_data]
2024/03/28 17:15:29 [warn] 6#6: *49 js: --- Checking authorization
2024/03/28 17:15:29 [warn] 6#6: *49 js: - HTTP method received [POST] -> needed [POST]
2024/03/28 17:15:29 [warn] 6#6: *49 js: - JWT roles received [guest] -> needed [guest]
2024/03/28 17:15:29 [warn] 6#6: *49 js: --- Authorization successful
2024/03/28 17:15:29 [warn] 6#6: *49 js: +-- JSON template validation [{"address":{"city":"","street":""},"age":0,"name":""}]
2024/03/28 17:15:29 [warn] 6#6: *49 js: |-- Checking JSON payload [[object Object]]
2024/03/28 17:15:29 [warn] 6#6: *49 js: |-- Checking JSON payload [[object Object]]
2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [city] ok
2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [street] ok
2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [address] ok
2024/03/28 17:15:29 [warn] 6#6: *49 js: |---- Property [age] missing
2024/03/28 17:15:29 [warn] 6#6: *49 js: +-- JSON template validation failed
2024/03/28 17:15:29 [info] 6#6: *49 client 10.5.0.1 closed keepalive connection
```## REST API access test - for Kubernetes
Note: the example FQDN `nginx-api-steering.k8s.f5.ff.lan` is used. This is defined in the `kubernetes.yaml` file.
Display NGINX Plus logs:
```
kubectl logs -l app=nginx -n nginx-api-steering -f
```Test with valid HTTP method with no JWT token (returns 401):
```
curl -X GET -ki http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_get
```Test with valid JWT token, HTTP method and URI (returns 200):
```
curl -X GET -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_get
```Test with valid JWT token and invalid HTTP method (return 403):
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_get
```Test with valid JWT token and invalid `guest` role (returns 403):
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.guest`" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_post -d '{"username": "[email protected]"}' -H "Content-Type: application/json"
```Test with valid JWT token and valid `devops` role (returns 200):
```
curl -X POST -ki -H "Authorization: Bearer `cat jwt/jwt.devops`" http://nginx-api-steering.k8s.f5.ff.lan/v1.0/api_post -d '{"username": "[email protected]", "group": "guest"}' -H "Content-Type: application/json"
```Test with no payload rewriting (returns 200):
```
curl -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": "[email protected]", "group": "guest"}' -H "Content-Type: application/json"
```Test with JSON payload checked against template (returns 200):
```
curl -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 '
{
"name": "John",
"age": 30,
"address": {
"street": "123 Main St",
"city": "New York"
}
}
' -H "Content-Type: application/json"
```Test with invalid JSON payload check against template (returns 422):
```
curl -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 '
{
"name": "John",
"address": {
"street": "123 Main St",
"city": "New York"
}
}
' -H "Content-Type: application/json"
```## Deployment removal
For docker-compose:
```
./nginx-api-steering.sh -o stop
```For Kubernetes:
```
./nginx-api-steering.sh -o stop-k8s
```