Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -140,6 +152,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
* 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`

Expand Down Expand Up @@ -300,3 +313,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.
44 changes: 35 additions & 9 deletions openid_connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -337,12 +337,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;
}
6 changes: 2 additions & 4 deletions openid_connect.server_conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
7 changes: 7 additions & 0 deletions openid_connect_configuration.conf
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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";
}
Expand Down