Skip to content

Commit b7e6252

Browse files
authored
enable jwksuri tests with keycloak (#7396)
1 parent b80a2b8 commit b7e6252

File tree

5 files changed

+190
-61
lines changed

5 files changed

+190
-61
lines changed

tests/data/jwt-policy-jwksuri/configmap/nginx-config.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@ metadata:
55
namespace: nginx-ingress
66
data:
77
resolver-addresses: "kube-dns.kube-system.svc.cluster.local"
8-
http-snippets: |
9-
subrequest_output_buffer_size 64k;

tests/data/jwt-policy-jwksuri/policies/jwt-policy-invalid.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ spec:
66
jwt:
77
realm: MyProductAPI
88
token: $$http_token
9-
jwksURI: https://login.microsoftonline.com/dd3dfd2f-6a3b-40d1-9be0-bf8327d81c50/discovery/v2.0/keys
9+
jwksURI: http://keycloak.default.svc.cluster.local:8080/realms/jwks-example/protocol/openid-connect/certs
1010
keyCache: 1h

tests/data/jwt-policy-jwksuri/policies/jwt-policy-valid.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ spec:
66
jwt:
77
realm: MyProductAPI
88
token: $http_token
9-
jwksURI: https://login.microsoftonline.com/dd3dfd2f-6a3b-40d1-9be0-bf8327d81c50/discovery/v2.0/keys
9+
jwksURI: http://keycloak.default.svc.cluster.local:8080/realms/jwks-example/protocol/openid-connect/certs
1010
keyCache: 1h

tests/suite/test_jwt_policies_jwksuri.py

Lines changed: 166 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
1+
import secrets
12
from unittest import mock
23

34
import pytest
45
import requests
6+
import yaml
57
from settings import TEST_DATA
6-
from suite.utils.policy_resources_utils import create_policy_from_yaml, delete_policy
7-
from suite.utils.resources_utils import replace_configmap_from_yaml, wait_before_test
8+
from suite.utils.policy_resources_utils import delete_policy
9+
from suite.utils.resources_utils import (
10+
create_example_app,
11+
create_secret_from_yaml,
12+
delete_common_app,
13+
delete_secret,
14+
replace_configmap_from_yaml,
15+
wait_before_test,
16+
wait_until_all_pods_are_ready,
17+
)
818
from suite.utils.vs_vsr_resources_utils import (
919
create_virtual_server_from_yaml,
1020
delete_and_create_vs_from_yaml,
1121
delete_virtual_server,
1222
)
1323

24+
username = "nginx-user-" + secrets.token_hex(4)
25+
password = secrets.token_hex(8)
26+
realm_name = "jwks-example"
1427
std_vs_src = f"{TEST_DATA}/virtual-server/standard/virtual-server.yaml"
1528
jwt_pol_valid_src = f"{TEST_DATA}/jwt-policy-jwksuri/policies/jwt-policy-valid.yaml"
1629
jwt_pol_invalid_src = f"{TEST_DATA}/jwt-policy-jwksuri/policies/jwt-policy-invalid.yaml"
@@ -27,34 +40,136 @@
2740
f"{TEST_DATA}/jwt-policy-jwksuri/virtual-server/virtual-server-invalid-policy-route-subpath.yaml"
2841
)
2942
jwt_cm_src = f"{TEST_DATA}/jwt-policy-jwksuri/configmap/nginx-config.yaml"
30-
ad_tenant = "dd3dfd2f-6a3b-40d1-9be0-bf8327d81c50"
31-
client_id = "8a172a83-a630-41a4-9ca6-1e5ef03cd7e7"
43+
keycloak_src = f"{TEST_DATA}/oidc/keycloak.yaml"
44+
keycloak_vs_src = f"{TEST_DATA}/oidc/virtual-server-idp.yaml"
3245

3346

34-
def get_token(request):
47+
class KeycloakSetup:
3548
"""
36-
get jwt token from azure ad endpoint
49+
Attributes:
50+
token (str):
3751
"""
52+
53+
def __init__(self, token):
54+
self.token = token
55+
56+
57+
@pytest.fixture(scope="class")
58+
def keycloak_setup(request, kube_apis, test_namespace, ingress_controller_endpoint):
59+
60+
# Create Keycloak resources and setup Keycloak idp
61+
62+
secret_name = create_secret_from_yaml(
63+
kube_apis.v1, test_namespace, f"{TEST_DATA}/virtual-server-tls/tls-secret.yaml"
64+
)
65+
keycloak_address = "keycloak.example.com"
66+
create_example_app(kube_apis, "keycloak", test_namespace)
67+
wait_before_test()
68+
wait_until_all_pods_are_ready(kube_apis.v1, test_namespace)
69+
keycloak_vs_name = create_virtual_server_from_yaml(kube_apis.custom_objects, keycloak_vs_src, test_namespace)
70+
wait_before_test()
71+
72+
# Get token
73+
url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/realms/master/protocol/openid-connect/token"
74+
headers = {"Host": keycloak_address, "Content-Type": "application/x-www-form-urlencoded"}
75+
data = {"username": "admin", "password": "admin", "grant_type": "password", "client_id": "admin-cli"}
76+
77+
response = requests.post(url, headers=headers, data=data, verify=False)
78+
admin_token = response.json()["access_token"]
79+
80+
# Create realm "jwks-example"
81+
create_realm_url = (
82+
f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms"
83+
)
84+
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {admin_token}", "Host": keycloak_address}
85+
payload = {
86+
"realm": realm_name,
87+
"enabled": True,
88+
}
89+
response = requests.post(create_realm_url, headers=headers, json=payload, verify=False)
90+
91+
# Create a user and set credentials
92+
create_user_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/{realm_name}/users"
93+
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {admin_token}", "Host": keycloak_address}
94+
user_payload = {
95+
"username": username,
96+
"enabled": True,
97+
"email": "[email protected]",
98+
"emailVerified": True,
99+
"firstName": "Jwks",
100+
"lastName": "User",
101+
"credentials": [{"type": "password", "value": password, "temporary": False}],
102+
"requiredActions": [],
103+
}
104+
response = requests.post(create_user_url, headers=headers, json=user_payload, verify=False)
105+
106+
# Create a client
107+
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {admin_token}", "Host": keycloak_address}
108+
create_client_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/{realm_name}/clients"
109+
client_payload = {
110+
"clientId": "jwks-client",
111+
"enabled": True,
112+
"protocol": "openid-connect",
113+
"publicClient": False,
114+
"directAccessGrantsEnabled": True,
115+
"standardFlowEnabled": True,
116+
"implicitFlowEnabled": False,
117+
"serviceAccountsEnabled": True,
118+
"authorizationServicesEnabled": True,
119+
"clientAuthenticatorType": "client-secret",
120+
"redirectUris": ["*"],
121+
"webOrigins": ["*"],
122+
"attributes": {
123+
"access.token.lifespan": "3600",
124+
"id.token.lifespan": "3600",
125+
"service.accounts.enabled": "true",
126+
},
127+
}
128+
client_resp = requests.post(create_client_url, headers=headers, json=client_payload, verify=False)
129+
if client_resp.status_code not in (200, 201):
130+
pytest.fail(f"Failed to create client: {client_resp.text}")
131+
location = client_resp.headers["Location"]
132+
client_id = location.split("/")[-1]
133+
134+
# Get the client secret
135+
get_secret_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/{realm_name}/clients/{client_id}/client-secret"
136+
secret_resp = requests.get(get_secret_url, headers=headers, verify=False)
137+
secret = secret_resp.json()["value"]
138+
38139
data = {
39-
"client_id": f"{client_id}",
40-
"scope": ".default",
41-
"client_secret": request.config.getoption("--ad-secret"),
42-
"grant_type": "client_credentials",
140+
"grant_type": "password",
141+
"scope": "openid",
142+
"client_id": "jwks-client",
143+
"client_secret": secret,
144+
"username": username,
145+
"password": password,
43146
}
44-
ad_response = requests.get(
45-
f"https://login.microsoftonline.com/{ad_tenant}/oauth2/token",
147+
url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/realms/{realm_name}/protocol/openid-connect/token"
148+
response = requests.post(
149+
url,
150+
headers={"Host": keycloak_address, "Content-Type": "application/x-www-form-urlencoded"},
46151
data=data,
47-
timeout=5,
48-
headers={"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Chrome/76.0.3809.100"},
152+
verify=False,
49153
)
50154

51-
if ad_response.status_code == 200:
52-
return ad_response.json()["access_token"]
53-
pytest.fail("Unable to request Azure token endpoint")
155+
if response.status_code != 200:
156+
pytest.fail(f"Failed to get token from Keycloak: {response.text}")
157+
158+
token = response.json().get("access_token")
159+
160+
def fin():
161+
if request.config.getoption("--skip-fixture-teardown") == "no":
162+
print("Delete Keycloak resources")
163+
delete_virtual_server(kube_apis.custom_objects, keycloak_vs_name, test_namespace)
164+
delete_common_app(kube_apis, "keycloak", test_namespace)
165+
delete_secret(kube_apis.v1, secret_name, test_namespace)
166+
167+
request.addfinalizer(fin)
168+
169+
return KeycloakSetup(token)
54170

55171

56172
@pytest.mark.skip_for_nginx_oss
57-
@pytest.mark.skip(reason="issues with IdP communication")
58173
@pytest.mark.policies
59174
@pytest.mark.parametrize(
60175
"crd_ingress_controller, virtual_server_setup",
@@ -85,6 +200,7 @@ def test_jwt_policy_jwksuri(
85200
crd_ingress_controller,
86201
virtual_server_setup,
87202
test_namespace,
203+
keycloak_setup,
88204
jwt_virtual_server,
89205
):
90206
"""
@@ -96,7 +212,12 @@ def test_jwt_policy_jwksuri(
96212
ingress_controller_prerequisites.namespace,
97213
jwt_cm_src,
98214
)
99-
pol_name = create_policy_from_yaml(kube_apis.custom_objects, jwt_pol_valid_src, test_namespace)
215+
with open(jwt_pol_valid_src) as f:
216+
doc = yaml.safe_load(f)
217+
pol_name = doc["metadata"]["name"]
218+
doc["spec"]["jwt"]["jwksURI"] = doc["spec"]["jwt"]["jwksURI"].replace("default", test_namespace)
219+
kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc)
220+
print(f"Policy created with name {pol_name}")
100221
wait_before_test()
101222

102223
print(f"Patch vs with policy: {jwt_virtual_server}")
@@ -106,6 +227,7 @@ def test_jwt_policy_jwksuri(
106227
jwt_virtual_server,
107228
virtual_server_setup.namespace,
108229
)
230+
wait_before_test()
109231
resp_no_token = mock.Mock()
110232
resp_no_token.status_code == 502
111233
counter = 0
@@ -118,14 +240,13 @@ def test_jwt_policy_jwksuri(
118240
wait_before_test()
119241
counter += 1
120242

121-
token = get_token(request)
243+
token = keycloak_setup.token
122244

123245
resp_valid_token = requests.get(
124246
virtual_server_setup.backend_1_url,
125247
headers={"host": virtual_server_setup.vs_host, "token": token},
126248
timeout=5,
127249
)
128-
129250
delete_policy(kube_apis.custom_objects, pol_name, test_namespace)
130251
wait_before_test()
131252

@@ -148,6 +269,7 @@ def test_jwt_invalid_policy_jwksuri(
148269
crd_ingress_controller,
149270
virtual_server_setup,
150271
test_namespace,
272+
keycloak_setup,
151273
jwt_virtual_server,
152274
):
153275
"""
@@ -159,7 +281,12 @@ def test_jwt_invalid_policy_jwksuri(
159281
ingress_controller_prerequisites.namespace,
160282
jwt_cm_src,
161283
)
162-
pol_name = create_policy_from_yaml(kube_apis.custom_objects, jwt_pol_invalid_src, test_namespace)
284+
with open(jwt_pol_invalid_src) as f:
285+
doc = yaml.safe_load(f)
286+
pol_name = doc["metadata"]["name"]
287+
doc["spec"]["jwt"]["jwksURI"] = doc["spec"]["jwt"]["jwksURI"].replace("default", test_namespace)
288+
kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc)
289+
print(f"Policy created with name {pol_name}")
163290
wait_before_test()
164291

165292
print(f"Patch vs with policy: {jwt_virtual_server}")
@@ -176,7 +303,7 @@ def test_jwt_invalid_policy_jwksuri(
176303
headers={"host": virtual_server_setup.vs_host},
177304
)
178305

179-
token = get_token(request)
306+
token = keycloak_setup.token
180307

181308
resp2 = requests.get(
182309
virtual_server_setup.backend_1_url,
@@ -204,6 +331,7 @@ def test_jwt_policy_subroute_jwksuri(
204331
crd_ingress_controller,
205332
virtual_server_setup,
206333
test_namespace,
334+
keycloak_setup,
207335
jwt_virtual_server,
208336
):
209337
"""
@@ -215,7 +343,12 @@ def test_jwt_policy_subroute_jwksuri(
215343
ingress_controller_prerequisites.namespace,
216344
jwt_cm_src,
217345
)
218-
pol_name = create_policy_from_yaml(kube_apis.custom_objects, jwt_pol_valid_src, test_namespace)
346+
with open(jwt_pol_valid_src) as f:
347+
doc = yaml.safe_load(f)
348+
pol_name = doc["metadata"]["name"]
349+
doc["spec"]["jwt"]["jwksURI"] = doc["spec"]["jwt"]["jwksURI"].replace("default", test_namespace)
350+
kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc)
351+
print(f"Policy created with name {pol_name}")
219352
wait_before_test()
220353

221354
print(f"Patch vs with policy: {jwt_virtual_server}")
@@ -237,7 +370,7 @@ def test_jwt_policy_subroute_jwksuri(
237370
wait_before_test()
238371
counter += 1
239372

240-
token = get_token(request)
373+
token = keycloak_setup.token
241374

242375
resp_valid_token = requests.get(
243376
virtual_server_setup.backend_1_url + "/subpath1",
@@ -265,6 +398,7 @@ def test_jwt_policy_subroute_jwksuri_multiple_vs(
265398
ingress_controller_prerequisites,
266399
crd_ingress_controller,
267400
virtual_server_setup,
401+
keycloak_setup,
268402
test_namespace,
269403
):
270404
"""
@@ -276,7 +410,12 @@ def test_jwt_policy_subroute_jwksuri_multiple_vs(
276410
ingress_controller_prerequisites.namespace,
277411
jwt_cm_src,
278412
)
279-
pol_name = create_policy_from_yaml(kube_apis.custom_objects, jwt_pol_valid_src, test_namespace)
413+
with open(jwt_pol_valid_src) as f:
414+
doc = yaml.safe_load(f)
415+
pol_name = doc["metadata"]["name"]
416+
doc["spec"]["jwt"]["jwksURI"] = doc["spec"]["jwt"]["jwksURI"].replace("default", test_namespace)
417+
kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc)
418+
print(f"Policy created with name {pol_name}")
280419
wait_before_test()
281420

282421
print(f"Patch first vs with policy: {jwt_vs_route_subpath_src}")
@@ -321,7 +460,7 @@ def test_jwt_policy_subroute_jwksuri_multiple_vs(
321460
wait_before_test()
322461
counter += 1
323462

324-
token = get_token(request)
463+
token = keycloak_setup.token
325464

326465
resp_1_valid_token = requests.get(
327466
virtual_server_setup.backend_1_url + "/subpath1",

0 commit comments

Comments
 (0)