Skip to content

Commit 60ea2c2

Browse files
tippexsTimo Stark
andauthored
Enable PKCE Support (#25)
* Enable PKCE Support * Enable PKCE Support - Code cleanup and refactoring - New function for setting IdP Client Auth variables * Enable PKCE Support - Code cleanup and refactoring - New function for setting IdP Client Auth variables * PKCE Support - Added PKCE documentation to README - Adjusted pkce keyvalue zones size and timeout Co-authored-by: Timo Stark <[email protected]>
1 parent 3477ec7 commit 60ea2c2

File tree

4 files changed

+52
-18
lines changed

4 files changed

+52
-18
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This implementation assumes the following environment:
1515
* The identity provider (IdP) supports OpenID Connect 1.0
1616
* The authorization code flow is in use
1717
* NGINX Plus is configured as a relying party
18-
* The IdP knows NGINX Plus as a confidential client
18+
* The IdP knows NGINX Plus as a confidential client or a public client using PKCE
1919

2020
With this environment, both the client and NGINX Plus communicate directly with the IdP at different stages during the initial authentication event.
2121

@@ -89,8 +89,8 @@ When NGINX Plus is deployed behind another proxy, the original protocol and port
8989
* Create an OpenID Connect client to represent your NGINX Plus instance
9090
* Choose the **authorization code flow**
9191
* Set the **redirect URI** to the address of your NGINX Plus instance (including the port number), with `/_codexch` as the path, e.g. `https://my-nginx.example.com:443/_codexch`
92-
* Ensure NGINX Plus is configured as a confidential client (with a client secret)
93-
* Make a note of the `client ID` and `client secret`
92+
* Ensure NGINX Plus is configured as a confidential client (with a client secret) or a public client (with PKCE S256 enabled)
93+
* Make a note of the `client ID` and `client secret` if set
9494

9595
* If your IdP supports OpenID Connect Discovery (usually at the URI `/.well-known/openid-configuration`) then use the `configure.sh` script to complete configuration. In this case you can skip the next section. Otherwise:
9696
* Obtain the URL for `jwks_uri` or download the JWK file to your NGINX Plus instance
@@ -130,6 +130,7 @@ The key-value store is used to maintain persistent storage for ID tokens and ref
130130
```nginx
131131
keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h;
132132
keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h;
133+
keyval_zone zone=oidc_pkce:128K timeout=90s;
133134
```
134135

135136
Each of the `keyval_zone` parameters are described below.
@@ -229,3 +230,4 @@ This reference implementation for OpenID Connect is supported for NGINX Plus sub
229230
* **R18** Opaque session tokens now used by default. Added support for refresh tokens. Added `/logout` location.
230231
* **R19** Minor bug fixes
231232
* **R22** Separate configuration file, supports multiple IdPs. Configurable scopes and cookie flags. JavaScript is imported as an indepedent module with `js_import`. Container-friendly logging. Additional metrics for OIDC activity.
233+
* **R23** PKCE support. Added support for deployments behind another proxy or load balancer.

openid_connect.js

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,8 @@ function auth(r) {
2424
r.return(500, r.variables.internal_error_message);
2525
return;
2626
}
27-
28-
// Choose a nonce for this flow for the client, and hash it for the IdP
29-
var noncePlain = r.variables.request_id;
30-
var c = require('crypto');
31-
var h = c.createHmac('sha256', r.variables.oidc_hmac_key).update(noncePlain);
32-
var nonceHash = h.digest('base64url');
33-
3427
// Redirect the client to the IdP login page with the cookies we need for state
35-
r.headersOut['Set-Cookie'] = [
36-
"auth_redir=" + r.variables.request_uri + "; " + r.variables.oidc_cookie_flags,
37-
"auth_nonce=" + noncePlain + "; " + r.variables.oidc_cookie_flags ];
38-
r.return(302, r.variables.oidc_authz_endpoint + "?response_type=code&scope=" + r.variables.oidc_scopes + "&client_id=" + r.variables.oidc_client + "&state=0&redirect_uri="+ r.variables.redirect_base + r.variables.redir_location + "&nonce=" + nonceHash);
28+
r.return(302, r.variables.oidc_authz_endpoint + getAuthZArgs(r));
3929
return;
4030
}
4131

@@ -125,8 +115,7 @@ function codeExchange(r) {
125115

126116
// Pass the authorization code to the /_token location so that it can be
127117
// proxied to the IdP in exchange for a JWT
128-
r.subrequest("/_token", "code=" + r.variables.arg_code,
129-
function(reply) {
118+
r.subrequest("/_token",idpClientAuth(r), function(reply) {
130119
if (reply.status == 504) {
131120
r.error("OIDC timeout connecting to IdP when sending authorization code");
132121
r.return(504);
@@ -248,3 +237,39 @@ function logout(r) {
248237
r.variables.refresh_token = "-";
249238
r.return(302, r.variables.oidc_logout_redirect);
250239
}
240+
241+
function getAuthZArgs(r) {
242+
// Choose a nonce for this flow for the client, and hash it for the IdP
243+
var noncePlain = r.variables.request_id;
244+
var c = require('crypto');
245+
var h = c.createHmac('sha256', r.variables.oidc_hmac_key).update(noncePlain);
246+
var nonceHash = h.digest('base64url');
247+
var authZArgs = "?response_type=code&scope=" + r.variables.oidc_scopes + "&client_id=" + r.variables.oidc_client + "&redirect_uri="+ r.variables.redirect_base + r.variables.redir_location + "&nonce=" + nonceHash;
248+
249+
r.headersOut['Set-Cookie'] = [
250+
"auth_redir=" + r.variables.request_uri + "; " + r.variables.oidc_cookie_flags,
251+
"auth_nonce=" + noncePlain + "; " + r.variables.oidc_cookie_flags
252+
];
253+
254+
if ( r.variables.oidc_pkce_enable == 1 ) {
255+
var pkce_code_verifier = c.createHmac('sha256', r.variables.oidc_hmac_key).update(String(Math.random())).digest('hex');
256+
r.variables.pkce_id = c.createHash('sha256').update(String(Math.random())).digest('base64url');
257+
var pkce_code_challenge = c.createHash('sha256').update(pkce_code_verifier).digest('base64url');
258+
r.variables.pkce_code_verifier = pkce_code_verifier;
259+
260+
authZArgs += "&code_challenge_method=S256&code_challenge=" + pkce_code_challenge + "&state=" + r.variables.pkce_id;
261+
} else {
262+
authZArgs += "&state=0";
263+
}
264+
return authZArgs;
265+
}
266+
267+
function idpClientAuth(r) {
268+
// If PKCE is enabled we have to use the code_verifier
269+
if ( r.variables.oidc_pkce_enable == 1 ) {
270+
r.variables.pkce_id = r.variables.arg_state;
271+
return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier;
272+
} else {
273+
return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret;
274+
}
275+
}

openid_connect.server_conf

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Advanced configuration START
22
set $internal_error_message "NGINX / OpenID Connect login failure\n";
3+
set $pkce_id "";
34
resolver 8.8.8.8; # For DNS lookup of IdP endpoints;
45
subrequest_output_buffer_size 32k; # To fit a complete tokenset response
56
gunzip on; # Decompress IdP responses if necessary
@@ -30,15 +31,15 @@
3031
js_content oidc.codeExchange;
3132
error_page 500 502 504 @oidc_error;
3233
}
33-
34+
3435
location = /_token {
3536
# This location is called by oidcCodeExchange(). We use the proxy_ directives
3637
# to construct the OpenID Connect token request, as per:
3738
# http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
3839
internal;
3940
proxy_ssl_server_name on; # For SNI to the IdP
4041
proxy_set_header Content-Type "application/x-www-form-urlencoded";
41-
proxy_set_body "grant_type=authorization_code&code=$arg_code&client_id=$oidc_client&client_secret=$oidc_client_secret&redirect_uri=$redirect_base$redir_location";
42+
proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location";
4243
proxy_method POST;
4344
proxy_pass $oidc_token_endpoint;
4445
}

openid_connect_configuration.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ map $host $oidc_client {
2020
default "my-client-id";
2121
}
2222

23+
map $host $oidc_pkce_enable {
24+
default 0;
25+
}
26+
2327
map $host $oidc_client_secret {
2428
default "my-client-secret";
2529
}
@@ -63,11 +67,13 @@ proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;
6367
# Change timeout values to at least the validity period of each token type
6468
keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h;
6569
keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h;
70+
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.
6671

6772
keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT
6873
keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token
6974
keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation
7075
keyval $request_id $new_refresh zone=refresh_tokens; # ''
76+
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
7177

7278
auth_jwt_claim_set $jwt_audience aud; # In case aud is an array
7379
js_import oidc from conf.d/openid_connect.js;

0 commit comments

Comments
 (0)