Skip to content

Commit 4f9da38

Browse files
authored
Added support for client_secret_basic as a client authentication method (#97)
- Updated token exchange to use the Authorization header for client_secret_basic. - Refactored logic for generating POST request parameters for token retrieval and refresh. - Added "oidc_client_auth_method" variable to select client authentication method.
1 parent 6ea7364 commit 4f9da38

File tree

4 files changed

+58
-13
lines changed

4 files changed

+58
-13
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ Subsequent requests to protected resources are authenticated by exchanging the s
7070

7171
For more information on OpenID Connect and JWT validation with NGINX Plus, see [Authenticating Users to Existing Applications with OpenID Connect and NGINX Plus](https://www.nginx.com/blog/authenticating-users-existing-applications-openid-connect-nginx-plus/).
7272

73+
### Client Authentication Methods
74+
75+
When configuring NGINX Plus as an OpenID Connect client, it supports multiple client authentication methods:
76+
77+
* **client_secret_basic**:
78+
* The `client_id` and `client_secret` are sent in the Authorization header as a Base64-encoded string.
79+
* **client_secret_post**:
80+
* The `client_id` and `client_secret` are sent in the body of the POST request.
81+
* **none** (PKCE):
82+
* For public clients that cannot protect a client secret, the `code_verifier` is used instead of a `client_secret`.
83+
* PKCE is particularly useful for mobile and single-page applications.
84+
7385
### Access Tokens
7486

7587
[Access tokens](https://openid.net/specs/openid-connect-core-1_0.html#AccessTokenDisclosure) are used in token-based authentication to allow OIDC client to access a protected resource on behalf of the user. NGINX Plus receives an access token after a user successfully authenticates and authorizes access, and then stores it in the key-value store. NGINX Plus can pass that token on the HTTP Authorization header as a [Bearer token](https://oauth.net/2/bearer-tokens/) for every request that is sent to the downstream application.
@@ -140,6 +152,7 @@ When NGINX Plus is deployed behind another proxy, the original protocol and port
140152
* Choose the **authorization code flow**
141153
* 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`
142154
* Ensure NGINX Plus is configured as a confidential client (with a client secret) or a public client (with PKCE S256 enabled)
155+
* If NGINX Plus is configured as a confidential client, choose the appropriate authentication method: **client_secret_basic** or **client_secret_post**.
143156
* Make a note of the `client ID` and `client secret` if set
144157
* Set the **post logout redirect URI** to the address of your NGINX Plus instance (including the port number), with `/_logout` as the path, e.g. `https://my-nginx.example.com:443/_logout`
145158

@@ -300,3 +313,4 @@ This reference implementation for OpenID Connect is supported for NGINX Plus sub
300313
* **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.
301314
* **R23** PKCE support. Added support for deployments behind another proxy or load balancer.
302315
* **R28** Access token support. Added support for access token to authorize NGINX to access protected backend.
316+
* **R32** Added support for `client_secret_basic` client authentication method.

openid_connect.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function auth(r, afterSyncCheck) {
5454

5555
// Pass the refresh token to the /_refresh location so that it can be
5656
// proxied to the IdP in exchange for a new id_token
57-
r.subrequest("/_refresh", "token=" + r.variables.refresh_token,
57+
r.subrequest("/_refresh", generateTokenRequestParams(r, "refresh_token"),
5858
function(reply) {
5959
if (reply.status != 200) {
6060
// Refresh request failed, log the reason
@@ -142,7 +142,7 @@ function codeExchange(r) {
142142

143143
// Pass the authorization code to the /_token location so that it can be
144144
// proxied to the IdP in exchange for a JWT
145-
r.subrequest("/_token",idpClientAuth(r), function(reply) {
145+
r.subrequest("/_token", generateTokenRequestParams(r, "authorization_code"), function(reply) {
146146
if (reply.status == 504) {
147147
r.error("OIDC timeout connecting to IdP when sending authorization code");
148148
r.return(504);
@@ -337,12 +337,38 @@ function getAuthZArgs(r) {
337337
return authZArgs;
338338
}
339339

340-
function idpClientAuth(r) {
341-
// If PKCE is enabled we have to use the code_verifier
342-
if ( r.variables.oidc_pkce_enable == 1 ) {
343-
r.variables.pkce_id = r.variables.arg_state;
344-
return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier;
345-
} else {
346-
return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret;
340+
function generateTokenRequestParams(r, grant_type) {
341+
var body = "grant_type=" + grant_type + "&client_id=" + r.variables.oidc_client;
342+
343+
switch(grant_type) {
344+
case "authorization_code":
345+
body += "&code=" + r.variables.arg_code + "&redirect_uri=" + r.variables.redirect_base + r.variables.redir_location;
346+
if (r.variables.oidc_pkce_enable == 1) {
347+
r.variables.pkce_id = r.variables.arg_state;
348+
body += "&code_verifier=" + r.variables.pkce_code_verifier;
349+
}
350+
break;
351+
case "refresh_token":
352+
body += "&refresh_token=" + r.variables.refresh_token;
353+
break;
354+
default:
355+
r.error("Unsupported grant type: " + grant_type);
356+
return;
347357
}
358+
359+
var options = {
360+
body: body,
361+
method: "POST"
362+
};
363+
364+
if (r.variables.oidc_pkce_enable != 1) {
365+
if (r.variables.oidc_client_auth_method === "client_secret_basic") {
366+
let auth_basic = "Basic " + Buffer.from(r.variables.oidc_client + ":" + r.variables.oidc_client_secret).toString('base64');
367+
options.args = "secret_basic=" + auth_basic;
368+
} else {
369+
options.body += "&client_secret=" + r.variables.oidc_client_secret;
370+
}
371+
}
372+
373+
return options;
348374
}

openid_connect.server_conf

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@
3939
internal;
4040
proxy_ssl_server_name on; # For SNI to the IdP
4141
proxy_set_header Content-Type "application/x-www-form-urlencoded";
42-
proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location";
43-
proxy_method POST;
42+
proxy_set_header Authorization $arg_secret_basic;
4443
proxy_pass $oidc_token_endpoint;
4544
}
4645

@@ -51,8 +50,7 @@
5150
internal;
5251
proxy_ssl_server_name on; # For SNI to the IdP
5352
proxy_set_header Content-Type "application/x-www-form-urlencoded";
54-
proxy_set_body "grant_type=refresh_token&refresh_token=$arg_token&client_id=$oidc_client&client_secret=$oidc_client_secret";
55-
proxy_method POST;
53+
proxy_set_header Authorization $arg_secret_basic;
5654
proxy_pass $oidc_token_endpoint;
5755
}
5856

openid_connect_configuration.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ map $host $oidc_client_secret {
4747
default "my-client-secret";
4848
}
4949

50+
map $host $oidc_client_auth_method {
51+
# Choose either "client_secret_basic" for sending client credentials in the
52+
# Authorization header, or "client_secret_post" for sending them in the
53+
# body of the POST request. This setting is used for confidential clients.
54+
default "client_secret_post";
55+
}
56+
5057
map $host $oidc_scopes {
5158
default "openid+profile+email+offline_access";
5259
}

0 commit comments

Comments
 (0)