Skip to content

Commit cb93a06

Browse files
committed
Add PKCE pytest
1 parent 3df8b44 commit cb93a06

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed

tests/data/oidc/pkce.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: Policy
3+
metadata:
4+
name: oidc-policy
5+
spec:
6+
oidc:
7+
clientID: nginx-plus-pkce
8+
clientSecret: oidc-secret
9+
authEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/auth
10+
tokenEndpoint: http://keycloak.default.svc.cluster.local:8080/realms/master/protocol/openid-connect/token
11+
jwksURI: http://keycloak.default.svc.cluster.local:8080/realms/master/protocol/openid-connect/certs
12+
endSessionEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/logout
13+
scope: openid+profile+email
14+
accessTokenEnable: true
15+
pkceEnabled: true

tests/suite/test_pkce.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import secrets
2+
3+
import pytest
4+
import requests
5+
import yaml
6+
from playwright.sync_api import Error, sync_playwright
7+
from settings import DEPLOYMENTS, TEST_DATA
8+
from suite.utils.policy_resources_utils import delete_policy
9+
from suite.utils.resources_utils import (
10+
create_example_app,
11+
create_items_from_yaml,
12+
create_secret,
13+
create_secret_from_yaml,
14+
delete_common_app,
15+
delete_secret,
16+
replace_configmap_from_yaml,
17+
wait_before_test,
18+
wait_until_all_pods_are_ready,
19+
)
20+
from suite.utils.vs_vsr_resources_utils import (
21+
create_virtual_server_from_yaml,
22+
delete_virtual_server,
23+
patch_virtual_server_from_yaml,
24+
)
25+
26+
username = "nginx-user-" + secrets.token_hex(4)
27+
password = secrets.token_hex(8)
28+
keycloak_src = f"{TEST_DATA}/oidc/keycloak.yaml"
29+
keycloak_vs_src = f"{TEST_DATA}/oidc/virtual-server-idp.yaml"
30+
oidc_secret_src = f"{TEST_DATA}/oidc/client-secret.yaml"
31+
pkce_pol_src = f"{TEST_DATA}/oidc/pkce.yaml"
32+
oidc_vs_src = f"{TEST_DATA}/oidc/virtual-server.yaml"
33+
orig_vs_src = f"{TEST_DATA}/virtual-server-tls/standard/virtual-server.yaml"
34+
cm_src = f"{TEST_DATA}/oidc/nginx-config.yaml"
35+
cm_zs_src = f"{TEST_DATA}/oidc/nginx-config-zs.yaml"
36+
orig_cm_src = f"{DEPLOYMENTS}/common/nginx-config.yaml"
37+
svc_src = f"{TEST_DATA}/oidc/nginx-ingress-headless.yaml"
38+
39+
40+
class KeycloakSetupPKCE:
41+
"""
42+
Attributes:
43+
secret (str):
44+
"""
45+
46+
def __init__(self, secret):
47+
self.secret = secret
48+
49+
50+
@pytest.fixture(scope="class")
51+
def keycloak_setup(request, kube_apis, test_namespace, ingress_controller_endpoint, virtual_server_setup):
52+
53+
# Create Keycloak resources and setup Keycloak idp with PKCE enabled
54+
secret_name = create_secret_from_yaml(
55+
kube_apis.v1, virtual_server_setup.namespace, f"{TEST_DATA}/virtual-server-tls/tls-secret.yaml"
56+
)
57+
keycloak_address = "keycloak.example.com"
58+
create_example_app(kube_apis, "keycloak", test_namespace)
59+
wait_before_test()
60+
wait_until_all_pods_are_ready(kube_apis.v1, test_namespace)
61+
keycloak_vs_name = create_virtual_server_from_yaml(kube_apis.custom_objects, keycloak_vs_src, test_namespace)
62+
wait_before_test()
63+
64+
# Get token
65+
url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/realms/master/protocol/openid-connect/token"
66+
headers = {"Host": keycloak_address, "Content-Type": "application/x-www-form-urlencoded"}
67+
data = {"username": "admin", "password": "admin", "grant_type": "password", "client_id": "admin-cli"}
68+
69+
response = requests.post(url, headers=headers, data=data, verify=False)
70+
token = response.json()["access_token"]
71+
72+
# Create a user and set credentials
73+
create_user_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/master/users"
74+
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "Host": keycloak_address}
75+
user_payload = {
76+
"username": username,
77+
"enabled": True,
78+
"credentials": [{"type": "password", "value": password, "temporary": False}],
79+
}
80+
response = requests.post(create_user_url, headers=headers, json=user_payload, verify=False)
81+
82+
# Create client "nginx-plus"
83+
create_client_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/master/clients"
84+
client_payload = {
85+
"clientId": "nginx-plus-pkce",
86+
"redirectUris": ["https://virtual-server-tls.example.com:443/_codexch"],
87+
"standardFlowEnabled": True,
88+
"directAccessGrantsEnabled": False,
89+
"publicClient": True,
90+
"attributes": {
91+
"post.logout.redirect.uris": "https://virtual-server-tls.example.com:443/*",
92+
"pkce.code.challenge.method": "S256",
93+
},
94+
"protocol": "openid-connect",
95+
}
96+
client_resp = requests.post(create_client_url, headers=headers, json=client_payload, verify=False)
97+
client_resp.raise_for_status()
98+
# let's check that it's a 201 somehow
99+
100+
print(f"Keycloak setup complete.")
101+
102+
def fin():
103+
if request.config.getoption("--skip-fixture-teardown") == "no":
104+
print("Delete Keycloak resources")
105+
delete_virtual_server(kube_apis.custom_objects, keycloak_vs_name, test_namespace)
106+
delete_common_app(kube_apis, "keycloak", test_namespace)
107+
delete_secret(kube_apis.v1, secret_name, test_namespace)
108+
109+
request.addfinalizer(fin)
110+
111+
# base64 encoded "pkce-doesnt-support-client-secrets"
112+
return KeycloakSetupPKCE("cGtjZS1kb2VzbnQtc3VwcG9ydC1jbGllbnQtc2VjcmV0cw==")
113+
114+
115+
@pytest.mark.oidc
116+
@pytest.mark.skip_for_nginx_oss
117+
@pytest.mark.parametrize(
118+
"crd_ingress_controller, virtual_server_setup",
119+
[
120+
(
121+
{
122+
"type": "complete",
123+
"extra_args": [
124+
f"-enable-oidc",
125+
],
126+
},
127+
{"example": "virtual-server-tls", "app_type": "simple"},
128+
)
129+
],
130+
indirect=True,
131+
)
132+
class TestPKCE:
133+
@pytest.mark.parametrize("configmap", [cm_src, cm_zs_src])
134+
def test_pkce(
135+
self,
136+
request,
137+
kube_apis,
138+
ingress_controller_endpoint,
139+
ingress_controller_prerequisites,
140+
crd_ingress_controller,
141+
test_namespace,
142+
virtual_server_setup,
143+
keycloak_setup,
144+
configmap,
145+
):
146+
147+
print(f"Create oidc secret")
148+
with open(oidc_secret_src) as f:
149+
secret_data = yaml.safe_load(f)
150+
secret_data["data"]["client-secret"] = keycloak_setup.secret
151+
secret_name = create_secret(kube_apis.v1, test_namespace, secret_data)
152+
153+
print(f"Create oidc policy")
154+
with open(pkce_pol_src) as f:
155+
doc = yaml.safe_load(f)
156+
pol = doc["metadata"]["name"]
157+
doc["spec"]["oidc"]["tokenEndpoint"] = doc["spec"]["oidc"]["tokenEndpoint"].replace("default", test_namespace)
158+
doc["spec"]["oidc"]["jwksURI"] = doc["spec"]["oidc"]["jwksURI"].replace("default", test_namespace)
159+
kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc)
160+
print(f"Policy created with name {pol}")
161+
wait_before_test()
162+
163+
print(f"Create virtual server")
164+
patch_virtual_server_from_yaml(
165+
kube_apis.custom_objects, virtual_server_setup.vs_name, oidc_vs_src, test_namespace
166+
)
167+
wait_before_test()
168+
print(f"Update nginx configmap")
169+
replace_configmap_from_yaml(
170+
kube_apis.v1,
171+
ingress_controller_prerequisites.config_map["metadata"]["name"],
172+
ingress_controller_prerequisites.namespace,
173+
configmap,
174+
)
175+
wait_before_test()
176+
177+
if configmap == cm_src:
178+
print(f"Create headless service")
179+
create_items_from_yaml(kube_apis, svc_src, ingress_controller_prerequisites.namespace)
180+
181+
with sync_playwright() as playwright:
182+
run_oidc(playwright.chromium, ingress_controller_endpoint.public_ip, ingress_controller_endpoint.port_ssl)
183+
184+
replace_configmap_from_yaml(
185+
kube_apis.v1,
186+
ingress_controller_prerequisites.config_map["metadata"]["name"],
187+
ingress_controller_prerequisites.namespace,
188+
cm_src,
189+
)
190+
delete_secret(kube_apis.v1, secret_name, test_namespace)
191+
delete_policy(kube_apis.custom_objects, pol, test_namespace)
192+
patch_virtual_server_from_yaml(
193+
kube_apis.custom_objects, virtual_server_setup.vs_name, orig_vs_src, test_namespace
194+
)
195+
196+
197+
def run_oidc(browser_type, ip_address, port):
198+
199+
browser = browser_type.launch(headless=True, args=[f"--host-resolver-rules=MAP * {ip_address}:{port}"])
200+
context = browser.new_context(ignore_https_errors=True)
201+
202+
try:
203+
page = context.new_page()
204+
205+
page.goto("https://virtual-server-tls.example.com")
206+
page.wait_for_selector('input[name="username"]')
207+
page.fill('input[name="username"]', username)
208+
page.wait_for_selector('input[name="password"]', timeout=5000)
209+
page.fill('input[name="password"]', password)
210+
211+
with page.expect_navigation():
212+
page.click('input[type="submit"]')
213+
page.wait_for_load_state("load")
214+
page_text = page.text_content("body")
215+
fields_to_check = [
216+
"Server address:",
217+
"Server name:",
218+
"Date:",
219+
"Request ID:",
220+
]
221+
for field in fields_to_check:
222+
assert field in page_text, f"'{field}' not found in page text"
223+
224+
except Error as e:
225+
assert False, f"Error: {e}"
226+
227+
finally:
228+
context.close()
229+
browser.close()

0 commit comments

Comments
 (0)