Skip to content

Commit 4758b7c

Browse files
authored
feat: add support for multiple signing algorithms (#6)
- plugin now supports both RSA (RS256/384/512) and EC (ES256/384/512) algorithms automatically. - Deprecated config.algorithm parameter - plugin validates against JWT's alg header instead - Enhanced test suite with comprehensive algorithm validation tests including security checks for 'none' and unsupported algorithms - Added detailed test documentation explaining test patterns, helper functions, and troubleshooting
1 parent e3d7387 commit 4758b7c

File tree

11 files changed

+368
-105
lines changed

11 files changed

+368
-105
lines changed

CLAUDE.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Key features:
1717
- Supports rotating public keys
1818
- Authorization based on token claims (scope, realm_access, resource_access)
1919
- Matches Keycloak users/clients to Kong consumers
20-
- Supports EC256/384/512 signature algorithms (feature branch)
20+
- Supports RSA (RS256, RS384, RS512) and EC (ES256, ES384, ES512) signature algorithms
2121

2222
## Development Environment
2323

@@ -136,7 +136,10 @@ The plugin supports these version combinations:
136136
- Postgres: 12.x and higher
137137
- Keycloak: 9.0.3 (RHSSO-7.4), 15.0.2 (RHSSO-7.5), and 26.2.0
138138

139-
## Branch Information
139+
## Algorithm Support
140140

141-
- Current branch `feature/ec-support` adds support for Elliptic Curve signature algorithms (ES256, ES384, ES512)
142-
- Main branch `main` is the stable version of the plugin
141+
The plugin automatically validates JWT tokens with the following signature algorithms:
142+
- **RSA algorithms**: RS256, RS384, RS512
143+
- **EC algorithms**: ES256, ES384, ES512
144+
145+
The algorithm is validated from the JWT's `alg` header field. The plugin no longer uses the `config.algorithm` parameter (deprecated), and instead validates that the token's algorithm is one of the supported algorithms listed above.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ curl -X POST http://localhost:8001/plugins \
160160
| config.run_on_preflight | no | `true` | A boolean value that indicates whether the plugin should run (and try to authenticate) on `OPTIONS` preflight requests, if set to false then `OPTIONS` requests will always be allowed. |
161161
| config.header_names | no | `authorization` | A list of HTTP header names that Kong will inspect to retrieve JWTs. `OPTIONS` requests will always be allowed. |
162162
| config.maximum_expiration | no | `0` | An integer limiting the lifetime of the JWT to `maximum_expiration` seconds in the future. Any JWT that has a longer lifetime will rejected (HTTP 403). If this value is specified, `exp` must be specified as well in the `claims_to_verify` property. The default value of `0` represents an indefinite period. Potential clock skew should be considered when configuring this value. |
163-
| config.algorithm | no | `RS256` | The algorithm used to verify the token’s signature. Can be `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, or `ES512`. |
163+
| config.algorithm | no | `RS256` | **Deprecated - No longer used.** The plugin now automatically validates that the JWT algorithm (`alg` header) is one of the supported algorithms: `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, or `ES512`. This field is kept for backwards compatibility but has no effect. |
164164
| config.allowed_iss | yes | | A list of allowed issuers for this route/service/api. Can be specified as a `string` or as a [Pattern](http://lua-users.org/wiki/PatternsTutorial). |
165165
| config.iss_key_grace_period | no | `10` | An integer that sets the number of seconds until public keys for an issuer can be updated after writing new keys to the cache. This is a guard so that the Kong cache will not invalidate every time a token signed with an invalid public key is sent to the plugin. |
166166
| config.well_known_template | false | *see description* | A string template that the well known endpoint for keycloak is created from. String formatting is applied on the template and `%s` is replaced by the issuer of the token. Default value is `%s/.well-known/openid-configuration` |

src/handler.lua

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local kong_meta = require "kong.meta"
77

88
local socket = require "socket"
99
local keycloak_keys = require("kong.plugins.jwt-keycloak.keycloak_keys")
10+
local plugin_schema = require("kong.plugins.jwt-keycloak.schema")
1011

1112
local validate_issuer = require("kong.plugins.jwt-keycloak.validators.issuers").validate_issuer
1213
local validate_scope = require("kong.plugins.jwt-keycloak.validators.scope").validate_scope
@@ -34,6 +35,26 @@ local JwtKeycloakHandler = {
3435
PRIORITY = priority
3536
}
3637

38+
-------------------------------------------------------------------------------
39+
-- custom helper function of the extended plugin "jwt-keycloak"
40+
-- --> extract allowed algorithms from schema
41+
-------------------------------------------------------------------------------
42+
local function get_allowed_algorithms_from_schema()
43+
-- Navigate through the schema structure to find the algorithm field
44+
for _, field in ipairs(plugin_schema.fields) do
45+
if field.config then
46+
for _, config_field in ipairs(field.config.fields) do
47+
if config_field.algorithm and config_field.algorithm.one_of then
48+
return config_field.algorithm.one_of
49+
end
50+
end
51+
end
52+
end
53+
end
54+
55+
-- Cache the allowed algorithms at module load time
56+
local ALLOWED_ALGORITHMS = get_allowed_algorithms_from_schema()
57+
3758
-------------------------------------------------------------------------------
3859
-- custom helper function of the extended plugin "jwt-keycloak"
3960
-- --> this is contained in jwt_parser, but has a breaking change in 3.5
@@ -416,12 +437,18 @@ local function do_authentication(conf)
416437
}
417438
end
418439

419-
local algorithm = conf.algorithm or "RS256"
420-
421-
-- Verify "alg"
422-
kong.log.debug("Expected JWT algorithm: " .. algorithm)
440+
-- Verify "alg" - check if algorithm is in allowed list (from schema)
441+
local is_valid_algorithm = false
442+
423443
kong.log.debug("Provided JWT algorithm: " .. jwt.header.alg)
424-
if jwt.header.alg ~= algorithm then
444+
for _, alg in ipairs(ALLOWED_ALGORITHMS) do
445+
if jwt.header.alg == alg then
446+
is_valid_algorithm = true
447+
break
448+
end
449+
end
450+
451+
if not is_valid_algorithm then
425452
security_event('ua201', 'ua, token integrity wrong')
426453
return false, {
427454
status = 401,

tests/README.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2025 Deutsche Telekom AG
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
-->
6+
7+
# Test Suite Documentation
8+
9+
## Overview
10+
This directory contains integration tests for the kong-plugin-jwt-keycloak plugin. Tests are executed in Docker containers with a full Kong + Keycloak environment.
11+
12+
## Running Tests
13+
```bash
14+
# Start all services
15+
docker compose up -d
16+
17+
# Run the complete test suite
18+
docker compose up tests
19+
```
20+
21+
## Test Files
22+
23+
### Core Test Scripts
24+
- **`_env.sh`**: Environment setup and helper functions
25+
- Sets up environment variables (Kong URL, Keycloak URL, etc.)
26+
- Provides `wait_for_kong()` and `wait_for_keycloak()` functions
27+
- Includes `retry_test_after_plugin_change()` helper for testing with retries
28+
29+
- **`run_tests.sh`**: Main test orchestrator
30+
- Runs all test phases in sequence
31+
- Exits on first failure
32+
33+
- **`run_unit_tests.sh`**: Lua unit test runner
34+
- Executes busted unit tests from `spec/` directory
35+
36+
### Setup Scripts
37+
- **`configure_keycloak.sh`**: Keycloak configuration
38+
Sets up Keycloak for testing:
39+
- Creates test realm with configured signing algorithm
40+
- Creates test client with OAuth2 client credentials flow
41+
- Configures client with ES256 signature algorithm by default
42+
43+
- **`configure_jwt_plugin.sh`**: Kong plugin configuration
44+
- Creates Kong service and route
45+
- Configures jwt-keycloak plugin
46+
- Sets up Kong consumer
47+
48+
### Test Suites
49+
50+
#### 1. `test_jwt_keycloak_plugin.sh`
51+
Basic JWT validation functionality:
52+
- Valid token acceptance
53+
- Invalid token rejection
54+
- Consumer matching
55+
56+
#### 2. `test_algorithms.sh`
57+
Algorithm validation and security tests:
58+
- **ES256 token validation** (Elliptic Curve)
59+
- Validates plugin accepts ES256 tokens from Keycloak
60+
- **'none' algorithm rejection** (Security)
61+
- Creates unsigned token with `"alg":"none"`
62+
- Validates plugin rejects with 401
63+
- Protects against "none algorithm attack"
64+
- **Unsupported algorithm rejection** (Security)
65+
- Creates token with `"alg":"HS256"`
66+
- Validates plugin rejects with 401
67+
- Ensures only supported algorithms are accepted
68+
69+
**Note:** The plugin automatically validates tokens against all supported algorithms:
70+
- RSA: RS256, RS384, RS512
71+
- EC: ES256, ES384, ES512
72+
73+
The `config.algorithm` parameter is deprecated and no longer used. RSA algorithm support is verified through the schema validation and the security tests demonstrate that the plugin correctly validates the algorithm header.
74+
75+
#### 3. `test_error_conditions.sh`
76+
Error handling scenarios:
77+
- Missing token (401)
78+
- Invalid token format (401)
79+
- Expired tokens (401)
80+
- Wrong issuer (401/403)
81+
- OPTIONS preflight requests
82+
83+
#### 4. `test_roles_scopes.sh`
84+
Authorization validation:
85+
- Scope-based authorization
86+
- Valid scope acceptance (200)
87+
- Invalid scope rejection (403)
88+
- Realm role validation
89+
- Valid realm role acceptance (200)
90+
- Invalid realm role rejection (403)
91+
- Client role validation
92+
- Valid client role acceptance (200)
93+
- Invalid client role rejection (403)
94+
95+
#### 5. `test_security_logging.sh`
96+
Security event logging:
97+
- Authentication failure events
98+
- Wrong issuer security events
99+
- Security event code validation (ua200, ua201, ua220, ua222)
100+
101+
## Test Environment Variables
102+
Set in `_env.sh`:
103+
- `KONG_ADMIN_URL`: Kong Admin API endpoint (http://kong:8001)
104+
- `KONG_PROXY_URL`: Kong Proxy endpoint (http://kong:8000)
105+
- `KC_URL`: Keycloak server URL (http://kc:8080)
106+
- `KC_CLIENT_ID`: Test client ID (test-user)
107+
- `KC_CLIENT_SECRET`: Generated client secret (random)
108+
- `KC_SIGNING_KEY_ALGORITHM`: Default signing algorithm (ES256)
109+
- `KC_REALM`: Keycloak realm name (default)
110+
111+
## Helper Functions
112+
113+
### `retry_test_after_plugin_change(description, expected_status, curl_command)`
114+
Retries a test up to 3 times with delays to allow Kong to apply plugin changes.
115+
116+
**Example:**
117+
```bash
118+
if ! retry_test_after_plugin_change "Valid token test" "200" \
119+
"curl -s -w \"%{http_code}\" -X GET $KONG_PROXY_URL/example/get -H \"Authorization: Bearer $TOKEN\" -o /dev/null"; then
120+
exit 1
121+
fi
122+
```
123+
124+
**Parameters:**
125+
- `description`: Human-readable test description
126+
- `expected_status`: Expected HTTP status code(s), can be multiple like "200|201"
127+
- `curl_command`: The curl command to execute
128+
129+
## Test Patterns
130+
131+
### Plugin Reconfiguration Pattern
132+
When testing different plugin configurations:
133+
134+
```bash
135+
# Delete existing plugin
136+
curl -s -X DELETE $KONG_ADMIN_URL/plugins/$(curl -s $KONG_ADMIN_URL/plugins | jq -r '.data[] | select(.name=="jwt-keycloak") | .id') > /dev/null
137+
138+
# Create new plugin configuration
139+
curl -s -X POST $KONG_ADMIN_URL/plugins \
140+
--data "name=jwt-keycloak" \
141+
--data "config.allowed_iss=$KC_URL/auth/realms/$KC_REALM" \
142+
--data "config.scope[]=email" \
143+
--data "route.id=$(curl -s $KONG_ADMIN_URL/routes/example-route | jq -r '.id')"
144+
145+
# Test with retry helper
146+
if ! retry_test_after_plugin_change "Test description" "200" "curl command"; then
147+
exit 1
148+
fi
149+
```
150+
151+
### Token Retrieval Pattern
152+
```bash
153+
TOKEN_ENDPOINT="$KC_URL/auth/realms/$KC_REALM/protocol/openid-connect/token"
154+
155+
ACCESS_TOKEN=$(curl -s -X POST $TOKEN_ENDPOINT \
156+
-H "Content-Type: application/x-www-form-urlencoded" \
157+
-d "client_id=$KC_CLIENT_ID" \
158+
-d "client_secret=$KC_CLIENT_SECRET" \
159+
-d "grant_type=client_credentials" | jq -r '.access_token')
160+
161+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
162+
echo "❌ Failed to obtain access token"
163+
exit 1
164+
fi
165+
```
166+
167+
### Creating Test Tokens with Specific Algorithms
168+
169+
#### Unsigned Token (none algorithm)
170+
```bash
171+
# Header: {"alg":"none","typ":"JWT"}
172+
NONE_HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
173+
NONE_PAYLOAD=$(echo -n '{"iss":"http://keycloak/realm","sub":"test","exp":9999999999}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
174+
NONE_TOKEN="${NONE_HEADER}.${NONE_PAYLOAD}."
175+
```
176+
177+
#### Unsupported Algorithm Token (HS256)
178+
```bash
179+
# Header: {"alg":"HS256","typ":"JWT"}
180+
HS256_HEADER=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
181+
HS256_PAYLOAD=$(echo -n '{"iss":"http://keycloak/realm","sub":"test","exp":9999999999}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
182+
HS256_SIGNATURE=$(echo -n "dummy_signature" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
183+
HS256_TOKEN="${HS256_HEADER}.${HS256_PAYLOAD}.${HS256_SIGNATURE}"
184+
```
185+
186+
## Adding New Tests
187+
1. Create a new test file: `test_<feature>.sh`
188+
2. Source `_env.sh` for helper functions
189+
3. Add test execution to `run_tests.sh`
190+
4. Make the script executable: `chmod +x test_<feature>.sh`
191+
5. Document the test in this README
192+
193+
**Example test structure:**
194+
```bash
195+
#!/bin/bash
196+
# SPDX-FileCopyrightText: 2025 Deutsche Telekom AG
197+
#
198+
# SPDX-License-Identifier: Apache-2.0
199+
200+
# Source environment helpers
201+
if [ -f ./_env.sh ]; then
202+
. ./_env.sh
203+
fi
204+
205+
echo "🧪 Testing [feature name]..."
206+
207+
# Your test logic here
208+
209+
echo "✅ All [feature] tests passed"
210+
```
211+
212+
## Troubleshooting
213+
214+
### Tests Fail Intermittently
215+
- Kong may take time to apply plugin changes
216+
- Use `retry_test_after_plugin_change()` helper
217+
- Increase retry count or wait time in `_env.sh`
218+
219+
### Services Not Ready
220+
- Increase timeout in `wait_for_kong()` or `wait_for_keycloak()`
221+
- Check service logs: `docker compose logs kong` or `docker compose logs kc`
222+
223+
### Token Validation Fails
224+
- Verify Keycloak configuration in `configure_keycloak.sh`
225+
- Check that token's algorithm is supported (RS256/384/512 or ES256/384/512)
226+
- Inspect token: `echo $ACCESS_TOKEN | cut -d. -f2 | base64 -d | jq .`
227+
- Verify token's `alg` header matches a supported algorithm
228+
229+
### Algorithm-Related Issues
230+
- The plugin automatically validates against all supported algorithms
231+
- `config.algorithm` parameter is deprecated and ignored
232+
- Supported algorithms: RS256, RS384, RS512, ES256, ES384, ES512
233+
- Any other algorithm (including 'none', HS256, etc.) will be rejected with 401

tests/configure_jwt_plugin.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ curl -i -X DELETE $KONG_ADMIN_URL/plugins/$(curl -s $KONG_ADMIN_URL/plugins | jq
2727
curl -i -X POST $KONG_ADMIN_URL/plugins \
2828
--data "name=jwt-keycloak" \
2929
--data "config.allowed_iss=$KC_URL/auth/realms/$KC_REALM" \
30-
--data "config.algorithm=$KC_SIGNING_KEY_ALGORITHM" \
3130
--data "config.consumer_match_claim_custom_id=true" \
3231
--data "config.consumer_match=true" \
3332
--data "route.id=$(curl -s $KONG_ADMIN_URL/routes/example-route | jq -r '.id')"

tests/run_tests.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ else
6565
exit 1
6666
fi
6767

68-
# Test 2: Test EC algorithms
69-
echo "🧪 Phase 2: Testing EC algorithms..."
70-
if [ -f ./test_ec_algorithms.sh ]; then
71-
. ./test_ec_algorithms.sh
68+
# Test 2: Test algorithms (RSA and EC)
69+
echo "🧪 Phase 2: Testing algorithms..."
70+
if [ -f ./test_algorithms.sh ]; then
71+
. ./test_algorithms.sh
7272
else
73-
echo "Error: test_ec_algorithms.sh not found"
73+
echo "Error: test_algorithms.sh not found"
7474
exit 1
7575
fi
7676

0 commit comments

Comments
 (0)