From 36ed2d177b9bc283969edf7245c42f8ce70ef73c Mon Sep 17 00:00:00 2001 From: Ivan Ovchinnikov Date: Tue, 18 Jun 2024 15:14:09 +0000 Subject: [PATCH] Added support for client_secret_basic as a client authentication method - 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. --- README.md | 14 ++++++++++ openid_connect.js | 46 ++++++++++++++++++++++++------- openid_connect.server_conf | 6 ++-- openid_connect_configuration.conf | 7 +++++ 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 22e5266..7a03d25 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,18 @@ Subsequent requests to protected resources are authenticated by exchanging the s 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/). +### Client Authentication Methods + +When configuring NGINX Plus as an OpenID Connect client, it supports multiple client authentication methods: + +* **client_secret_basic**: + * The `client_id` and `client_secret` are sent in the Authorization header as a Base64-encoded string. +* **client_secret_post**: + * The `client_id` and `client_secret` are sent in the body of the POST request. +* **none** (PKCE): + * For public clients that cannot protect a client secret, the `code_verifier` is used instead of a `client_secret`. + * PKCE is particularly useful for mobile and single-page applications. + ### Access Tokens [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. @@ -136,6 +148,7 @@ When NGINX Plus is deployed behind another proxy, the original protocol and port * Choose the **authorization code flow** * 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` * Ensure NGINX Plus is configured as a confidential client (with a client secret) or a public client (with PKCE S256 enabled) + * If NGINX Plus is configured as a confidential client, choose the appropriate authentication method: **client_secret_basic** or **client_secret_post**. * Make a note of the `client ID` and `client secret` if set * 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: @@ -294,3 +307,4 @@ This reference implementation for OpenID Connect is supported for NGINX Plus sub * **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. * **R23** PKCE support. Added support for deployments behind another proxy or load balancer. * **R28** Access token support. Added support for access token to authorize NGINX to access protected backend. + * **R32** Added support for `client_secret_basic` client authentication method. diff --git a/openid_connect.js b/openid_connect.js index 5ef1a80..ca551ae 100644 --- a/openid_connect.js +++ b/openid_connect.js @@ -54,7 +54,7 @@ function auth(r, afterSyncCheck) { // Pass the refresh token to the /_refresh location so that it can be // proxied to the IdP in exchange for a new id_token - r.subrequest("/_refresh", "token=" + r.variables.refresh_token, + r.subrequest("/_refresh", generateTokenRequestParams(r, "refresh_token"), function(reply) { if (reply.status != 200) { // Refresh request failed, log the reason @@ -142,7 +142,7 @@ function codeExchange(r) { // Pass the authorization code to the /_token location so that it can be // proxied to the IdP in exchange for a JWT - r.subrequest("/_token",idpClientAuth(r), function(reply) { + r.subrequest("/_token", generateTokenRequestParams(r, "authorization_code"), function(reply) { if (reply.status == 504) { r.error("OIDC timeout connecting to IdP when sending authorization code"); r.return(504); @@ -304,12 +304,38 @@ function getAuthZArgs(r) { return authZArgs; } -function idpClientAuth(r) { - // If PKCE is enabled we have to use the code_verifier - if ( r.variables.oidc_pkce_enable == 1 ) { - r.variables.pkce_id = r.variables.arg_state; - return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier; - } else { - return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret; - } +function generateTokenRequestParams(r, grant_type) { + var body = "grant_type=" + grant_type + "&client_id=" + r.variables.oidc_client; + + switch(grant_type) { + case "authorization_code": + body += "&code=" + r.variables.arg_code + "&redirect_uri=" + r.variables.redirect_base + r.variables.redir_location; + if (r.variables.oidc_pkce_enable == 1) { + r.variables.pkce_id = r.variables.arg_state; + body += "&code_verifier=" + r.variables.pkce_code_verifier; + } + break; + case "refresh_token": + body += "&refresh_token=" + r.variables.refresh_token; + break; + default: + r.error("Unsupported grant type: " + grant_type); + return; + } + + var options = { + body: body, + method: "POST" + }; + + if (r.variables.oidc_pkce_enable != 1) { + if (r.variables.oidc_client_auth_method === "client_secret_basic") { + let auth_basic = "Basic " + Buffer.from(r.variables.oidc_client + ":" + r.variables.oidc_client_secret).toString('base64'); + options.args = "secret_basic=" + auth_basic; + } else { + options.body += "&client_secret=" + r.variables.oidc_client_secret; + } + } + + return options; } diff --git a/openid_connect.server_conf b/openid_connect.server_conf index 13456d2..17da45e 100644 --- a/openid_connect.server_conf +++ b/openid_connect.server_conf @@ -39,8 +39,7 @@ internal; proxy_ssl_server_name on; # For SNI to the IdP proxy_set_header Content-Type "application/x-www-form-urlencoded"; - proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location"; - proxy_method POST; + proxy_set_header Authorization $arg_secret_basic; proxy_pass $oidc_token_endpoint; } @@ -51,8 +50,7 @@ internal; proxy_ssl_server_name on; # For SNI to the IdP proxy_set_header Content-Type "application/x-www-form-urlencoded"; - proxy_set_body "grant_type=refresh_token&refresh_token=$arg_token&client_id=$oidc_client&client_secret=$oidc_client_secret"; - proxy_method POST; + proxy_set_header Authorization $arg_secret_basic; proxy_pass $oidc_token_endpoint; } diff --git a/openid_connect_configuration.conf b/openid_connect_configuration.conf index c2b0a52..5f7624d 100644 --- a/openid_connect_configuration.conf +++ b/openid_connect_configuration.conf @@ -40,6 +40,13 @@ map $host $oidc_client_secret { default "my-client-secret"; } +map $host $oidc_client_auth_method { + # Choose either "client_secret_basic" for sending client credentials in the + # Authorization header, or "client_secret_post" for sending them in the + # body of the POST request. This setting is used for confidential clients. + default "client_secret_post"; +} + map $host $oidc_scopes { default "openid+profile+email+offline_access"; }