Skip to content

Add OIDC PKCE configuration through policy #7765

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ae6561a
Fix leading whitespace in tmpl files
javorszky Apr 30, 2025
a1143db
Add PKCE Enabled flag
javorszky Apr 30, 2025
b79734d
Implement pkce in configs
javorszky May 8, 2025
3b49cfa
Add check for OIDC to guard for a nil pointer
javorszky May 8, 2025
fb41f5a
Fix some whitespace alignment in tmpl files
javorszky May 8, 2025
d2c925c
Update snapshots to realign with whitespaces
javorszky May 8, 2025
f660f55
Add tests for PKCE enabled true
javorszky May 8, 2025
246781c
Update CRDs based on policy files
javorszky May 8, 2025
d455320
Add pkceEnabled to oidc pytest setup yaml
javorszky May 13, 2025
745c9c0
Terminate include directive with a ;
javorszky May 13, 2025
ef9bbe3
Set pkce enabled to an int instead of a string
javorszky May 14, 2025
ee4af70
OIDC test doesn't need pkce enabled
javorszky May 16, 2025
ff1367e
Add PKCE pytest
javorszky May 16, 2025
b321042
Update snapshot after changing a str -> int
javorszky May 16, 2025
dc28293
oidc and pkce pytest fixture scope to function
javorszky May 16, 2025
eaf255a
OIDC tests should be class fixtured
javorszky May 16, 2025
7fdcdab
Remove a parameter from pkce test
javorszky May 16, 2025
122a68c
pkce test fixture should also be class scoped
javorszky May 16, 2025
b58b413
Add debug prints
javorszky May 16, 2025
63382f3
Merge pkce test into oidc test file
javorszky May 20, 2025
dd329ef
Add unit tests for the bool to int util function
javorszky May 20, 2025
6157e2b
Add docs to create keycloak client via api
javorszky May 27, 2025
6a5dc61
Reword options because no tabs
javorszky May 27, 2025
3f02842
OIDC example deploy keycloak into nginx-ingress ns
javorszky May 28, 2025
f800126
Add plus-mgmt-configmap.yaml to instructions
javorszky May 28, 2025
f7f42ff
Redo list numbers in oidc example readme
javorszky May 28, 2025
bf05969
Reset keycloak to be in default namespace
javorszky May 28, 2025
72f4ef7
Add note on not using client secret for PKCE
javorszky May 28, 2025
56e6c20
Move applying the plus mgmt to common resources
javorszky May 28, 2025
20a632b
Add pkceEnabled to policy resource doc
javorszky May 28, 2025
e4ff105
Rename pkceEnabled from past to present tense
javorszky May 28, 2025
bd287c4
Fix product name in example readme
javorszky May 29, 2025
e66df56
Change console code type to shell
javorszky May 29, 2025
a87153c
Turn choice into unordered list
javorszky May 29, 2025
9cbb395
Replace const default pkce secret with init val
javorszky Jun 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ spec:
type: string
jwksURI:
type: string
pkceEnable:
type: boolean
postLogoutRedirectURI:
type: string
redirectURI:
Expand Down
2 changes: 2 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ spec:
type: string
jwksURI:
type: string
pkceEnable:
type: boolean
postLogoutRedirectURI:
type: string
redirectURI:
Expand Down
57 changes: 36 additions & 21 deletions examples/custom-resources/oidc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ application using an OpenID Connect policy and [Keycloak](https://www.keycloak.o

**Note**: The KeyCloak container does not support IPv6 environments.

**Note**: This example assumes that your default namespace is set to `default`. You can check this with

```shell
kubectl config view --minify | grep namespace
```

If it's not empty, and anything other than `default`, you can set to `default` with the following command:

```shell
kubectl config set-context --namespace default --current
```

## Prerequisites

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
instructions to deploy the Ingress Controller. This example requires that the HTTPS port of the Ingress Controller is
`443`.
1. Save the public IP address of the Ingress Controller into `/etc/hosts` of your machine:
instructions to deploy NGINX Ingress Controller. This example requires that the HTTPS port of the Ingress
Controller is `443`.
2. Save the public IP address of the Ingress Controller into `/etc/hosts` of your machine:

```text
...
Expand All @@ -27,29 +39,29 @@ application using an OpenID Connect policy and [Keycloak](https://www.keycloak.o
Create a secret with the TLS certificate and key that will be used for TLS termination of the web application and
Keycloak:

```console
```shell
kubectl apply -f tls-secret.yaml
```

## Step 2 - Deploy a Web Application

Create the application deployment and service:

```console
```shell
kubectl apply -f webapp.yaml
```

## Step 3 - Deploy Keycloak

1. Create the Keycloak deployment and service:

```console
```shell
kubectl apply -f keycloak.yaml
```

1. Create a VirtualServer resource for Keycloak:
2. Create a VirtualServer resource for Keycloak:

```console
```shell
kubectl apply -f virtual-server-idp.yaml
```

Expand All @@ -59,27 +71,30 @@ To set up Keycloak:

1. Follow the steps in the "Configuring Keycloak" [section of the documentation](https://docs.nginx.com/nginx/deployment-guides/single-sign-on/keycloak/#configuring-keycloak):
1. To connect to Keycloak, use `https://keycloak.example.com`.
1. Make sure to save the client secret for NGINX-Plus client to the `SECRET` shell variable:
2. Make sure to save the client secret for NGINX-Plus client to the `SECRET` shell variable:

```console
```shell
SECRET=value
```

1. Alternatively, [execute the commands](./keycloak_setup.md).
2. Alternatively, [execute the commands](./keycloak_setup.md).

## Step 5 - Deploy the Client Secret

**Note**: If you're using PKCE, skip this step. PKCE clients do not have client secrets. Applying this will result
in a broken deployment.

Comment on lines +84 to +86
Copy link
Collaborator

@pdabelf5 pdabelf5 May 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do our examples have branching logic in them? I thought they we single use examples.

1. Encode the secret, obtained in the previous step:

```console
```shell
echo -n $SECRET | base64
```

1. Edit `client-secret.yaml`, replacing `<insert-secret-here>` with the encoded secret.
2. Edit `client-secret.yaml`, replacing `<insert-secret-here>` with the encoded secret.

1. Create a secret with the name `oidc-secret` that will be used by the OIDC policy:
3. Create a secret with the name `oidc-secret` that will be used by the OIDC policy:

```console
```shell
kubectl apply -f client-secret.yaml
```

Expand All @@ -96,23 +111,23 @@ Steps:

1. Apply the ConfigMap `nginx-config.yaml`, which contains `zone-sync` configuration parameter that enable zone synchronization and the resolver using the kube-dns service.

```console
```shell
kubectl apply -f nginx-config.yaml
```

## Step 7 - Deploy the OIDC Policy

Create a policy with the name `oidc-policy` that references the secret from the previous step:

```console
```shell
kubectl apply -f oidc.yaml
```

## Step 8 - Configure Load Balancing

Create a VirtualServer resource for the web application:

```console
```shell
kubectl apply -f virtual-server.yaml
```

Expand All @@ -122,15 +137,15 @@ Note that the VirtualServer references the policy `oidc-policy` created in Step

1. Open a web browser and navigate to the URL of the web application: `https://webapp.example.com`. You will be
redirected to Keycloak.
1. Log in with the username and password for the user you created in Keycloak, `nginx-user` and `test`.
2. Log in with the username and password for the user you created in Keycloak, `nginx-user` and `test`.
![keycloak](./keycloak.png)
1. Once logged in, you will be redirected to the web application and get a response from it. Notice the field `User ID`
3. Once logged in, you will be redirected to the web application and get a response from it. Notice the field `User ID`
in the response, this will match the ID for your user in Keycloak. ![webapp](./webapp.png)

## Step 10 - Log Out

1. To log out, navigate to `https://webapp.example.com/logout`. Your session will be terminated, and you will be
redirected to the default post logout URI `https://webapp.example.com/_logout`.
![logout](./logout.png)
1. To confirm that you have been logged out, navigate to `https://webapp.example.com`. You will be redirected to
2. To confirm that you have been logged out, navigate to `https://webapp.example.com`. You will be redirected to
Keycloak to log in again.
58 changes: 41 additions & 17 deletions examples/custom-resources/oidc/keycloak_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,63 @@ Steps:

1. Save the address of Keycloak into a shell variable:

```console
```shell
KEYCLOAK_ADDRESS=keycloak.example.com
```

1. Retrieve the access token and store it into a shell variable:
2. Retrieve the access token and store it into a shell variable:

```console
```shell
TOKEN=`curl -sS -k --data "username=admin&password=admin&grant_type=password&client_id=admin-cli" "https://${KEYCLOAK_ADDRESS}/realms/master/protocol/openid-connect/token" | jq -r .access_token`
```

Ensure the request was successful and the token is stored in the shell variable by running:

```console
```shell
echo $TOKEN
```

***Note***: The access token lifespan is very short. If it expires between commands, retrieve it again with the
command above.

1. Create the user `nginx-user`:
3. Create the user `nginx-user`:

```console
```shell
curl -sS -k -X POST -d '{ "username": "nginx-user", "enabled": true, "credentials":[{"type": "password", "value": "test", "temporary": false}]}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/admin/realms/master/users
```

1. Create the client `nginx-plus` and retrieve the secret:

```console
SECRET=`curl -sS -k -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"], "attributes": {"post.logout.redirect.uris": "https://webapp.example.com:443/*"}}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/realms/master/clients-registrations/default | jq -r .secret`
```

If everything went well you should have the secret stored in $SECRET. To double check run:

```console
echo $SECRET
```
4. Create the client `nginx-plus`:

- If you are not using PKCE, use the following command to create an OIDC client that does not use PKCE:

```shell
SECRET=`curl -sS -k -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"], "attributes": {"post.logout.redirect.uris": "https://webapp.example.com:443/*"}}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/realms/master/clients-registrations/default | jq -r .secret`
```

If everything went well, you should have the secret stored in $SECRET. To double-check, run:

```shell
echo $SECRET
```

- Or if you are using PKCE with OIDC, use the following command to create the client:

```shell
curl -sS -k -H "Content-Type: application/json" -H "Authorization: Bearer ${TOKEN}" \
--data '{
"clientId": "nginx-plus",
"enabled": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"publicClient": true,
"redirectUris": [
"https://webapp.example.com:443/_codexch"
],
"attributes": {
"pkce.code.challenge.method":"S256",
"post.logout.redirect.uris": "https://webapp.example.com:443/*"
},
"protocol": "openid-connect"
}' \
https://${KEYCLOAK_ADDRESS}/admin/realms/master/clients
```
2 changes: 0 additions & 2 deletions internal/configs/oidc/oidc_common.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,13 @@ proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;
keyval_zone zone=oidc_id_tokens:1M timeout=1h sync;
keyval_zone zone=oidc_access_tokens:1M timeout=1h sync;
keyval_zone zone=refresh_tokens:1M timeout=8h sync;
#keyval_zone zone=oidc_pkce:128K timeout=90s sync; # Temporary storage for PKCE code verifier.

keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for ID token(JWT)
keyval $cookie_auth_token $access_token zone=oidc_access_tokens; # Exchange cookie for access token
keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token
keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation
keyval $request_id $new_access_token zone=oidc_access_tokens;
keyval $request_id $new_refresh zone=refresh_tokens; # ''
#keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;

auth_jwt_claim_set $jwt_audience aud; # In case aud is an array
js_import oidc from oidc/openid_connect.js;
8 changes: 8 additions & 0 deletions internal/configs/oidc/oidc_pkce_supplements.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# https://nginx.org/en/docs/http/ngx_http_keyval_module.html
# context: http

# keyval_zone keyval_zone zone=name:size [state=file] [timeout=time] [type=string|ip|prefix] [sync];
keyval_zone zone=oidc_pkce:128K timeout=90s sync; # Temporary storage for PKCE code verifier.

# keyval key $variable zone
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
64 changes: 56 additions & 8 deletions internal/configs/version2/__snapshots__/templates_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ server {
proxy_pass http://coffee-v2;
health_check uri=/ port=50 interval=5s jitter=0s fails=1 passes=1 mandatory persistent keepalive_time=60s;

}
}
location @hc-tea {

grpc_connect_timeout ;
Expand All @@ -344,7 +344,7 @@ server {
grpc_pass grpc://tea-v3;
health_check port=50 interval=5s jitter=0s fails=1 passes=1 type=grpc grpc_status=12 grpc_service=tea-servicev2;

}
}
location @vs_cafe_cafe_vsr_tea_tea_tea__tea_error_page_0 {

default_type "application/json";
Expand Down Expand Up @@ -761,7 +761,7 @@ server {
proxy_pass http://coffee-v2;
health_check uri=/ port=50 interval=5s jitter=0s fails=1 passes=1 mandatory persistent keepalive_time=60s;

}
}
location @hc-tea {

grpc_connect_timeout ;
Expand All @@ -770,7 +770,7 @@ server {
grpc_pass grpc://tea-v3;
health_check port=50 interval=5s jitter=0s fails=1 passes=1 type=grpc grpc_status=12 grpc_service=tea-servicev2;

}
}
location @vs_cafe_cafe_vsr_tea_tea_tea__tea_error_page_0 {

default_type "application/json";
Expand Down Expand Up @@ -1316,6 +1316,54 @@ server {

---

[TestExecuteVirtualServerTemplateWithOIDCAndPKCEPolicyNGINXPlus - 1]

include oidc/oidc_pkce_supplements.conf;

server {
listen 80 proxy_protocol;
listen [::]:80 proxy_protocol;


server_name example.com;
status_zone example.com;
set $resource_type "virtualserver";
set $resource_name "";
set $resource_namespace "";
include oidc/oidc.conf;

set $oidc_pkce_enable 1;
set $oidc_client_auth_method "client_secret_post";
set $oidc_logout_redirect "";
set $oidc_hmac_key "";
set $zone_sync_leeway 0;

set $oidc_authz_endpoint "";
set $oidc_authz_extra_args "";
set $oidc_token_endpoint "";
set $oidc_end_session_endpoint "";
set $oidc_jwt_keyfile "";
set $oidc_scopes "";
set $oidc_client "";
set $oidc_client_secret "";
set $redir_location "";

server_tokens "";




location / {
set $service "";
status_zone "";


set $default_connection_header close;
}
}

---

[TestExecuteVirtualServerTemplate_RendersOSSTemplateWithHTTP2Off - 1]

server {
Expand Down Expand Up @@ -1438,7 +1486,7 @@ server {
return 204;
}



}

Expand Down Expand Up @@ -2872,7 +2920,7 @@ server {
return 204;
}



}

Expand Down Expand Up @@ -2971,7 +3019,7 @@ server {
proxy_pass http://coffee-v2;
health_check uri=/ port=50 interval=5s jitter=0s fails=1 passes=1 mandatory persistent keepalive_time=60s;

}
}
location @hc-tea {

grpc_connect_timeout ;
Expand All @@ -2980,7 +3028,7 @@ server {
grpc_pass grpc://tea-v3;
health_check port=50 interval=5s jitter=0s fails=1 passes=1 type=grpc grpc_status=12 grpc_service=tea-servicev2;

}
}
location @vs_cafe_cafe_vsr_tea_tea_tea__tea_error_page_0 {

default_type "application/json";
Expand Down
Loading
Loading