From de4bab2593c6257c2af198e5a75aa1736076f0b3 Mon Sep 17 00:00:00 2001 From: Shawn Kim Date: Thu, 3 Nov 2022 14:59:52 -0700 Subject: [PATCH 1/2] feat: access token & new endpoints (/login, /userinfo, /v2/logout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **1. access token** - Enhance the NJS Code to capture the `access_token` sent by the IdP. - Store the `access_token` in the k/v store as same as we store `id_token` and `refresh_token` **2. new endpoints** - Add `/userinfo` endpoint: - Add a map variable of `$oidc_userinfo_endpoint` as same as authz and token endpoints here (`openid_connect_configuration.conf`) . - Expose `/userinfo` endpoint here(`openid_connect.server_conf`) in a location block of NGINX Plus to interact with IdP's `userinfo_endpoint` which is defined in the endpoint of`well-known/openid-configuration`. - The nginx location block should proxy to the IdP’s `userinfo_endpoint` by adding `access_token` as a bearer token. ``` Authorization : Bearer ``` - The response coming from IdP should be returned back to the caller as it is. - Expose `/login` endpoint: - Expose the `/login` endpoint as a location block here (`openid_connect.server_conf`) - Proxy it to the IdP's `authorization_endpoint` configured in the map variable of `$oidc_authz_endpoint` in (`openid_connect_configuration.conf`). - This would outsource the login function to IdP as its configured. - Expose `/v2/logout` endpoint: - Expose the `/v2/logout` endpoint as a location block here (`openid_connect.server_conf`) - Add a map variable of `$oidc_end_session_endpoint` as same as authz and token endpoints here (`openid_connect_configuration.conf`) . - Proxy it to the IdP's `end_session_endpoint` to finish the session by IdP. - Expose `/v2/_logout` endpoint: - Expose `/v2/_logout` endpoint which is a callback from IdP as a location block here (`openid_connect.server_conf`) to handle the following sequences. - 1. Redirected by IdP when IdP successfully finished the session. - 2. NGINX Plus: Clear session cookies. - 3. NGINX Plus: Redirect to either the original landing page or the custom logout page by calling - Add a map of `$post_logout_return_uri`: After the successful logout from the IdP, NGINX Plus calls this URI to redirect to either the original page or a custom logout page. The default is original page based on the configuration of `$redirect_base`. **3. add endpoints in `configure.sh`** - IdP's userinfo endpoint - IdP's end session endpoint --- README.md | 218 ++++++++++++++++++++---------- configure.sh | 4 +- frontend.conf | 115 ++++++++++++++++ openid_connect.js | 80 ++++++++++- openid_connect.server_conf | 71 +++++++++- openid_connect_configuration.conf | 74 ++++++++-- 6 files changed, 477 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 2b3b006..71c2f0c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,22 @@ Reference implementation of NGINX Plus as relying party for OpenID Connect authentication +- [Description](#description) + - [Refresh Tokens](#refresh-tokens) + - [OpenID Connect Userinfo Endpoint](#openid-connect-userinfo-endpoint) + - [Login Behavior](#login-behavior) + - [Logout Behavior](#logout-behavior) + - [Multiple IdPs](#multiple-idps) +- [Quick Start Guide](#quick-start-guide) +- [Installation](#installation) +- [Configuring your IdP](#configuring-your-idp) +- [Configuring NGINX Plus](#configuring-nginx-plus) +- [Session Management](#session-management) +- [Real time monitoring](#real-time-monitoring) +- [Troubleshooting](#troubleshooting) +- [Support](#support) +- [Changelog](#changelog) + ## Description This repository describes how to enable OpenID Connect integration for [NGINX Plus](https://www.nginx.com/products/nginx/). The solution depends on NGINX Plus components ([auth_jwt module](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html) and [key-value store](http://nginx.org/en/docs/http/ngx_http_keyval_module.html)) and as such is not suitable for [open source NGINX](http://www.nginx.org/en). @@ -12,10 +28,10 @@ This repository describes how to enable OpenID Connect integration for [NGINX Pl This implementation assumes the following environment: - * The identity provider (IdP) supports OpenID Connect 1.0 - * The authorization code flow is in use - * NGINX Plus is configured as a relying party - * The IdP knows NGINX Plus as a confidential client or a public client using PKCE +- The identity provider (IdP) supports OpenID Connect 1.0 +- The authorization code flow is in use +- NGINX Plus is configured as a relying party +- The IdP knows NGINX Plus as a confidential client or a public client using PKCE With this environment, both the client and NGINX Plus communicate directly with the IdP at different stages during the initial authentication event. @@ -32,18 +48,50 @@ For more information on OpenID Connect and JWT validation with NGINX Plus, see [ ### Refresh Tokens -If a [refresh token](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens) was received from the IdP then it is also stored in the key-value store. When validation of the ID Token fails (typically upon expiry) then NGINX Plus sends the refresh token to the IdP. If the user's session is still valid at the IdP then a new ID token is received, validated, and updated in the key-value store. The refresh process is seamless to the client. +If a [refresh token](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens) was received from the IdP then it is also stored in the key-value store. When validation of the ID Token fails (typically upon expiry) then NGINX Plus sends the refresh token to the IdP. If the user's session is still valid at the IdP then new ID token and access token are received, validated, and updated in the key-value store. The refresh process is seamless to the client. The ID token is used for user authentication with login and logout, and the access is used for API authorization before proxing to IdP endpoint such as `/userinfo` or custom backend APIs. + +### OpenID Connect UserInfo Endpoint + +The [OpenID Connect UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) (/userinfo) provides details about the logged-in user. Requests to /userinfo must be authenticated using the access token provided as part of the authorization code flow. + +- When a user is not logged in, requests to the `/userinfo` endpoint return a `401` (unauthorized) response. +- When a user is logged in, requests to `/userinfo` return a `200` response with the requested user information (such as name). + +### Login Behavior + +When a client requests an application's `/login` location, NGINX Plus starts the following OIDC workflow: + +- When an End-User clicks a `Login` button, the frontend app calls `/login` endpoint +- The `/login` endpoint calls IdP's `authorization_endpoint`, and IdP issued the ID token, access token, and refresh token to NGINX Plus. +- The tokens are stored in the key-value store of NGINX Plus. +- NGINX Plus redirects to the frontend landing page with session cookie after these successful login processes. +- The frontend app shows user information in detail by calling `/userinfo` endpoint. -### Logout +### Logout Behavior Requests made to the `/logout` location invalidate both the ID token and refresh token by erasing them from the key-value store. Therefore, subsequent requests to protected resources will be treated as a first-time request and send the client to the IdP for authentication. Note that the IdP may issue cookies such that an authenticated session still exists at the IdP. +To avoid breaking changes of API endpoints to customers who use a `/logout` location, the `/v2/logout` location is added to interact with the IdP's `end_session_endpoint` which is to handle [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout) as the spec of OpenID Connect. The `$post_logout_return_uri` is the URI to which the RP is requesting that the End-User's User Agent be redirected after a logout has been performed. When setting up an IdP, `/v2/_logout` is used in this example, and you can change it per your preference. + ### Multiple IdPs Where NGINX Plus is configured to proxy requests for multiple websites or applications, or user groups, these may require authentication by different IdPs. Separate IdPs can be configured, with each one matching on an attribute of the HTTP request, e.g. hostname or part of the URI path. > **Note:** When validating OpenID Connect tokens, NGINX Plus can be configured to read the signing key (JWKS) from disk, or a URL. When using multiple IdPs, each one must be configured to use the same method. It is not possible to use a mix of both disk and URLs for the `map…$oidc_jwt_keyfile` variable. +## Quick Start Guide + +Set up a [local demo](https://github.com/nginx-openid-connect/nginx-oidc-examples/tree/main/001-oidc-local-test) that can be used for testing purposes: + +> **Note:** This link is subject to change. + +1. Running a Docker container +2. Checking If a Single Page App works in your browser +3. Setting up an Identity Provider (IdP) +4. Configuring NGINX Plus +5. Testing NGINX Plus OIDC +6. Stop and remove Docker containers + ## Installation Start by [installing NGINX Plus](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-plus/). In addition, the [NGINX JavaScript module](https://www.nginx.com/blog/introduction-nginscript/) (njs) is required for handling the interaction between NGINX Plus and the OpenID Connect provider (IdP). Install the njs module after installing NGINX Plus by running one of the following: @@ -67,6 +115,7 @@ Finally, create a clone of the GitHub repository. All files can be copied to **/etc/nginx/conf.d** ### Non-standard directories + The GitHub repository contains [`include`](http://nginx.org/en/docs/ngx_core_module.html#include) files for NGINX configuration, and JavaScript code for token exchange and initial token validation. These files are referenced with a relative path (relative to /etc/nginx). If NGINX Plus is running from a non-standard location then copy the files from the GitHub repository to `/path/to/conf/conf.d` and use the `-p` flag to start NGINX with a prefix path that specifies the location where the configuration files are located. ```shell @@ -74,6 +123,7 @@ $ nginx -p /path/to/conf -c /path/to/conf/nginx.conf ``` ### Running in containers + This implementation is suitable for running in a container provided that the [base image](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-docker/) includes the NGINX JavaScript module. The GitHub repository is designed to facilitate testing with a container by binding the cloned repository to a mount volume on the container. ```shell @@ -82,20 +132,24 @@ $ docker run -d -p 8010:8010 -v $PWD:/etc/nginx/conf.d nginx-plus nginx -g 'daem ``` ### Running behind another proxy or load balancer + When NGINX Plus is deployed behind another proxy, the original protocol and port number are not available. NGINX Plus needs this information to construct the URIs it passes to the IdP and for redirects. By default NGINX Plus looks for the X-Forwarded-Proto and X-Forwarded-Port request headers to construct these URIs. ## Configuring your IdP - * Create an OpenID Connect client to represent your NGINX Plus instance - * 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) - * Make a note of the `client ID` and `client secret` if set +- Create an OpenID Connect client to represent your NGINX Plus instance + + - 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) + - 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: - * Obtain the URL for `jwks_uri` or download the JWK file to your NGINX Plus instance - * Obtain the URL for the **authorization endpoint** - * Obtain the URL for the **token endpoint** +- 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: + - Obtain the URL for `jwks_uri` or download the JWK file to your NGINX Plus instance + - Obtain the URL for the **authorization endpoint** + - Obtain the URL for the **token endpoint** + - Obtain the URL for the **end session endpoint** + - Obtain the URL for the **user info endpoint** ## Configuring NGINX Plus @@ -103,45 +157,60 @@ Configuration can typically be completed automatically by using the `configure.s Manual configuration involves reviewing the following files so that they match your IdP(s) configuration. - * **openid_connect_configuration.conf** - this contains the primary configuration for one or more IdPs in `map{}` blocks - * Modify all of the `map…$oidc_` blocks to match your IdP configuration - * Modify the URI defined in `map…$oidc_logout_redirect` to specify an unprotected resource to be displayed after requesting the `/logout` location - * Set a unique value for `$oidc_hmac_key` to ensure nonce values are unpredictable - * If NGINX Plus is deployed behind another proxy or load balancer, modify the `map…$redirect_base` and `map…$proto` blocks to define how to obtain the original protocol and port number. +- **openid_connect_configuration.conf** - this contains the primary configuration for one or more IdPs in `map{}` blocks + + - Modify all of the `map…$oidc_` blocks to match your IdP configuration + - Modify the URI defined in `map…$oidc_logout_redirect` to specify an unprotected resource to be displayed after requesting the `/logout` location for customers who has been using R28. + - Modify the URI defined in `map…$oidc_logout_redirect_uri` to specify an unprotected resource to be displayed after requesting the `/v2/_logout` location for customers who start using R29 and wants to change from `map…$oidc_logout_redirect` to `map…$oidc_logout_redirect_uri` to use the feature of `OIDC RP-Initiated Logout` which is interact with IdP's `end_session_endpoint`. + - Set a unique value for `$oidc_hmac_key` to ensure nonce values are unpredictable + - If NGINX Plus is deployed behind another proxy or load balancer, modify the `map…$redirect_base` and `map…$proto` blocks to define how to obtain the original protocol and port number. + +- **frontend.conf** - this is the reverse proxy configuration + + - Modify the upstream group to match your backend site or app + - Configure the preferred listen port and [enable SSL/TLS configuration](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/) + - Modify the severity level of the `error_log` directive to suit the deployment environment + - Comment/uncomment the `auth_jwt_key_file` or `auth_jwt_key_request` directives based on whether `$oidc_jwt_keyfile` is a file or URI, respectively + - Examples + 1. **Basic Example:** Landing page starts OIDC flow without a login button. + 2. **Advanced Example:** Landing page, login/logout button to start/finish OIDC workflow. + - Landing page with `login` button + - `login` button to start OIDC flow by validating `id token` with the JWK of IdP. + - Landing page calls the `/userinfo` endpoint to show user information by validating `access token` with the JWK of IdP. + - `logout` button to close the OIDC session among frontend, NGINX Plus, and IdP. + - The proxied API authorization by validating `access token` with the JWK of IdP. + - Use `access token` for most of IdPs such as Amazon Cognito, Auth0, Keycloak, Okta, OneLogin and Ping Identity. + - Use `session_jwt` for Azure AD as for now. - * **frontend.conf** - this is the reverse proxy configuration - * Modify the upstream group to match your backend site or app - * Configure the preferred listen port and [enable SSL/TLS configuration](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/) - * Modify the severity level of the `error_log` directive to suit the deployment environment - * Comment/uncomment the `auth_jwt_key_file` or `auth_jwt_key_request` directives based on whether `$oidc_jwt_keyfile` is a file or URI, respectively +- **openid_connect.server_conf** - this is the NGINX configuration for handling the various stages of OpenID Connect authorization code flow - * **openid_connect.server_conf** - this is the NGINX configuration for handling the various stages of OpenID Connect authorization code flow - * No changes are usually required here - * Modify the `resolver` directive to match a DNS server that is capable of resolving the IdP defined in `$oidc_token_endpoint` - * If using [`auth_jwt_key_request`](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html#auth_jwt_key_request) to automatically fetch the JWK file from the IdP then modify the validity period and other caching options to suit your IdP + - No changes are usually required here + - Modify the `resolver` directive to match a DNS server that is capable of resolving the IdP defined in `$oidc_token_endpoint` + - If using [`auth_jwt_key_request`](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html#auth_jwt_key_request) to automatically fetch the JWK file from the IdP then modify the validity period and other caching options to suit your IdP - * **openid_connect.js** - this is the JavaScript code for performing the authorization code exchange and nonce hashing - * No changes are required unless modifying the code exchange or validation process +- **openid_connect.js** - this is the JavaScript code for performing the authorization code exchange and nonce hashing + - No changes are required unless modifying the code exchange or validation process ### Configuring the Key-Value Store The key-value store is used to maintain persistent storage for ID tokens and refresh tokens. The default configuration should be reviewed so that it suits the environment. This is part of the advanced configuration in **openid_connect_configuration.conf**. ```nginx -keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h; -keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h; -keyval_zone zone=oidc_pkce:128K timeout=90s; +keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h; +keyval_zone zone=oidc_access_tokens:1M state=conf.d/oidc_access_tokens.json timeout=1h; +keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h; +keyval_zone zone=oidc_pkce:128K timeout=90s; ``` Each of the `keyval_zone` parameters are described below. - * **zone** - Specifies the name of the key-value store and how much memory to allocate for it. Each session will typically occupy 1-2KB, depending on the size of the tokens, so scale this value to exceed the number of unique users that may authenticate. +- **zone** - Specifies the name of the key-value store and how much memory to allocate for it. Each session will typically occupy 1-2KB, depending on the size of the tokens, so scale this value to exceed the number of unique users that may authenticate. - * **state** (optional) - Specifies where all of the ID Tokens in the key-value store are saved, so that sessions will persist across restart or reboot of the NGINX host. The NGINX Plus user account, typically **nginx**, must have write permission to the directory where the state file is stored. Consider creating a dedicated directory for this purpose. +- **state** (optional) - Specifies where all of the ID Tokens in the key-value store are saved, so that sessions will persist across restart or reboot of the NGINX host. The NGINX Plus user account, typically **nginx**, must have write permission to the directory where the state file is stored. Consider creating a dedicated directory for this purpose. - * **timeout** - Expired tokens are removed from the key-value store after the `timeout` value. This should be set to value slightly longer than the JWT validity period. JWT validation occurs on each request, and will fail when the expiry date (`exp` claim) has elapsed. If JWTs are issued without an `exp` claim then set `timeout` to the desired session duration. If JWTs are issued with a range of validity periods then set `timeout` to exceed the longest period. +- **timeout** - Expired tokens are removed from the key-value store after the `timeout` value. This should be set to value slightly longer than the JWT validity period. JWT validation occurs on each request, and will fail when the expiry date (`exp` claim) has elapsed. If JWTs are issued without an `exp` claim then set `timeout` to the desired session duration. If JWTs are issued with a range of validity periods then set `timeout` to exceed the longest period. - * **sync** (optional) - If deployed in a cluster, the key-value store may be synchronized across all instances in the cluster, so that all instances are able to create and validate authenticated sessions. Each instance must be configured to participate in state sharing with the [zone_sync module](http://nginx.org/en/docs/stream/ngx_stream_zone_sync_module.html) and by adding the `sync` parameter to the `keyval_zone` directives above. +- **sync** (optional) - If deployed in a cluster, the key-value store may be synchronized across all instances in the cluster, so that all instances are able to create and validate authenticated sessions. Each instance must be configured to participate in state sharing with the [zone_sync module](http://nginx.org/en/docs/stream/ngx_stream_zone_sync_module.html) and by adding the `sync` parameter to the `keyval_zone` directives above. ## Session Management @@ -157,6 +226,7 @@ To delete a single session: ```shell $ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/oidc_id_tokens +$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/oidc_access_tokens $ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/refresh_tokens ``` @@ -164,6 +234,7 @@ To delete all sessions: ```shell $ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_id_tokens +$ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_access_tokens $ curl -iX DELETE localhost:8010/api/6/http/keyvals/refresh_tokens ``` @@ -171,17 +242,17 @@ $ curl -iX DELETE localhost:8010/api/6/http/keyvals/refresh_tokens The **openid_connect.server_conf** file defines several [`status_zone`](http://nginx.org/en/docs/http/ngx_http_api_module.html#status_zone) directives to collect metrics about OpenID Connect activity and errors. Separate metrics counters are recorded for: - * **OIDC start** - New sessions are counted here. See step 2 in Figure 2, above. Success is recorded as a 3xx response. +- **OIDC start** - New sessions are counted here. See step 2 in Figure 2, above. Success is recorded as a 3xx response. - * **OIDC code exchange** - Counters are incremented here when the browser returns to NGINX Plus after authentication. See steps 6-10 in Figure 2, above. Success is recorded as a 3xx response. +- **OIDC code exchange** - Counters are incremented here when the browser returns to NGINX Plus after authentication. See steps 6-10 in Figure 2, above. Success is recorded as a 3xx response. - * **OIDC logout** - Requests to the /logout URI are counted here. Success is recorded as a 3xx response. +- **OIDC logout** - Requests to the URIs of either `/logout` or `/v2/logout` are counted here. Success is recorded as a 3xx response. - * **OIDC error** - Counters are incremented here when errors in the code exchange process are actively detected. Typically there will be a corresponding error_log entry. +- **OIDC error** - Counters are incremented here when errors in the code exchange process are actively detected. Typically there will be a corresponding error_log entry. - To obtain the current set of metrics: +To obtain the current set of metrics: - ```shell +```shell $ curl localhost:8010/api/6/http/location_zones ``` @@ -191,33 +262,38 @@ In addition, the [NGINX Plus Dashboard](https://docs.nginx.com/nginx/admin-guide Any errors generated by the OpenID Connect flow are logged to the error log, `/var/log/nginx/error.log`. Check the contents of this file as it may include error responses received by the IdP. The level of detail recorded can be modified by adjusting the severity level of the `error_log` directive. - * **400 error from IdP** - * This is typically caused by incorrect configuration related to the client ID and client secret. - * Check the values of the `map…$oidc_client` and `map…$oidc_client_secret` variables against the IdP configuration. +- **400 error from IdP** + + - This is typically caused by incorrect configuration related to the client ID and client secret. + - Check the values of the `map…$oidc_client` and `map…$oidc_client_secret` variables against the IdP configuration. + +- **500 error from nginx after successful authentication** + - Check for `could not be resolved` and `empty JWK set while sending to client` messages in the error log. This is common when NGINX Plus cannot reach the IdP's `jwks_uri` endpoint. + - Check the `map…$oidc_jwt_keyfile` variable is correct. + - Check the `resolver` directive in **openid_connect.server_conf** is reachable from the NGINX Plus host. + - Check for `OIDC authorization code sent but token response is not JSON.` messages in the error log. This is common when NGINX Plus cannot decompress the IdP's response. Add the following configuration snippet to the `/_jwks_uri` and `/_token` locations to **openid_connect.server_conf**: - * **500 error from nginx after successful authentication** - * Check for `could not be resolved` and `empty JWK set while sending to client` messages in the error log. This is common when NGINX Plus cannot reach the IdP's `jwks_uri` endpoint. - * Check the `map…$oidc_jwt_keyfile` variable is correct. - * Check the `resolver` directive in **openid_connect.server_conf** is reachable from the NGINX Plus host. - * Check for `OIDC authorization code sent but token response is not JSON.` messages in the error log. This is common when NGINX Plus cannot decompress the IdP's response. Add the following configuration snippet to the `/_jwks_uri` and `/_token` locations to **openid_connect.server_conf**: ```nginx proxy_set_header Accept-Encoding "gzip"; ``` - * **Authentication is successful but browser shows too many redirects** - * This is typically because the JWT sent to the browser cannot be validated, resulting in 'authorization required' `401` response and starting the authentication process again. But the user is already authenticated so is redirected back to NGINX, hence the redirect loop. - * Avoid using `auth_jwt_require` directives in your configuration because this can also return a `401` which is indistinguishable from missing/expired JWT. - * Check the error log `/var/log/nginx/error.log` for JWT/JWK errors. - * Ensure that the JWK file (`map…$oidc_jwt_keyfile` variable) is correct and that the nginx user has permission to read it. +- **Authentication is successful but browser shows too many redirects** + + - This is typically because the JWT sent to the browser cannot be validated, resulting in 'authorization required' `401` response and starting the authentication process again. But the user is already authenticated so is redirected back to NGINX, hence the redirect loop. + - Avoid using `auth_jwt_require` directives in your configuration because this can also return a `401` which is indistinguishable from missing/expired JWT. + - Check the error log `/var/log/nginx/error.log` for JWT/JWK errors. + - Ensure that the JWK file (`map…$oidc_jwt_keyfile` variable) is correct and that the nginx user has permission to read it. + +- **Logged out but next request does not require authentication** + + - This is typically caused by the IdP issuing its own session cookie(s) to the client. NGINX Plus sends the request to the IdP for authentication and the IdP immediately sends back a new authorization code because the session is still valid. + - Check your IdP configuration if this behavior is not desired. - * **Logged out but next request does not require authentication** - * This is typically caused by the IdP issuing its own session cookie(s) to the client. NGINX Plus sends the request to the IdP for authentication and the IdP immediately sends back a new authorization code because the session is still valid. - * Check your IdP configuration if this behavior is not desired. +- **Failed SSL/TLS handshake to IdP** + - Indicated by error log messages including `peer closed connection in SSL handshake (104: Connection reset by peer) while SSL handshaking to upstream`. + - This can occur when the IdP requires Server Name Indication (SNI) information as part of the TLS handshake. Additional configuration is required to satisfy this requirement. + - Edit **openid_connect.server_conf** and for each of the `/_jwks_uri`, `/_token`, and `/_refresh` locations, add the following configuration snippet: - * **Failed SSL/TLS handshake to IdP** - * Indicated by error log messages including `peer closed connection in SSL handshake (104: Connection reset by peer) while SSL handshaking to upstream`. - * This can occur when the IdP requires Server Name Indication (SNI) information as part of the TLS handshake. Additional configuration is required to satisfy this requirement. - * Edit **openid_connect.server_conf** and for each of the `/_jwks_uri`, `/_token`, and `/_refresh` locations, add the following configuration snippet: ```nginx proxy_set_header Host ; proxy_ssl_name ; @@ -229,10 +305,10 @@ This reference implementation for OpenID Connect is supported for NGINX Plus sub ## Changelog - * **R15** Initial release of OpenID Connect reference implementation - * **R16** Added support for opaque session tokens using key-value store - * **R17** Configuration now supports JSON Web Key (JWK) set to be obtained by URI - * **R18** Opaque session tokens now used by default. Added support for refresh tokens. Added `/logout` location. - * **R19** Minor bug fixes - * **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. +- **R15** Initial release of OpenID Connect reference implementation +- **R16** Added support for opaque session tokens using key-value store +- **R17** Configuration now supports JSON Web Key (JWK) set to be obtained by URI +- **R18** Opaque session tokens now used by default. Added support for refresh tokens. Added `/logout` location. +- **R19** Minor bug fixes +- **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. diff --git a/configure.sh b/configure.sh index 17e8920..0637f4c 100755 --- a/configure.sh +++ b/configure.sh @@ -120,7 +120,7 @@ fi # Build an intermediate configuration file # File format is: # -jq -r '. | "$oidc_authz_endpoint \(.authorization_endpoint)\n$oidc_token_endpoint \(.token_endpoint)\n$oidc_jwks_uri \(.jwks_uri)"' < /tmp/${COMMAND}_$$_json > /tmp/${COMMAND}_$$_conf +jq -r '. | "$oidc_authz_endpoint \(.authorization_endpoint)\n$oidc_token_endpoint \(.token_endpoint)\n$oidc_jwks_uri \(.jwks_uri)\n$oidc_end_session_endpoint \(.end_session_endpoint)\n$oidc_userinfo_endpoint \(.userinfo_endpoint)"' < /tmp/${COMMAND}_$$_json > /tmp/${COMMAND}_$$_conf # Create a random value for HMAC key, adding to the intermediate configuration file echo "\$oidc_hmac_key `openssl rand -base64 18`" >> /tmp/${COMMAND}_$$_conf @@ -178,7 +178,7 @@ fi # Loop through each configuration variable echo "$COMMAND: NOTICE: Configuring $CONFDIR/openid_connect_configuration.conf" -for OIDC_VAR in \$oidc_authz_endpoint \$oidc_token_endpoint \$oidc_jwt_keyfile \$oidc_hmac_key $CLIENT_ID_VAR $CLIENT_SECRET_VAR $PKCE_ENABLE_VAR; do +for OIDC_VAR in \$oidc_authz_endpoint \$oidc_token_endpoint \$oidc_jwt_keyfile \$oidc_end_session_endpoint \$oidc_userinfo_endpoint \$oidc_hmac_key $CLIENT_ID_VAR $CLIENT_SECRET_VAR $PKCE_ENABLE_VAR; do # Pull the configuration value from the intermediate file VALUE=`grep "^$OIDC_VAR " /tmp/${COMMAND}_$$_conf | cut -f2 -d' '` echo -n "$COMMAND: NOTICE: - $OIDC_VAR ..." diff --git a/frontend.conf b/frontend.conf index d79f10d..903434a 100644 --- a/frontend.conf +++ b/frontend.conf @@ -1,3 +1,16 @@ +# -----------------------------------------------------------------------------# +# # +# Sample Reverse Proxy Configuration: Frontend Site, Backend App # +# (for Open ID Connect workflow) # +# # +# -----------------------------------------------------------------------------# + +# -----------------------------------------------------------------------------# +# # +# 1. Basic Example: Landing page starts OIDC workflow w/o login/logout button. # +# # +# -----------------------------------------------------------------------------# + # This is the backend application we are protecting with OpenID Connect upstream my_backend { zone my_backend 64k; @@ -33,4 +46,106 @@ server { } } +# -----------------------------------------------------------------------------# +# # +# 2. Advanced Example: Landing page, login/logout button to handle OIDC kflow # +# # +# - Landing page shows 'login' button # +# - 'login' button calls `/login` endpoint to start OIDC flow by validating +# 'id_token' w/ IdP's JWK. # +# - Landing page calls `/userinfo` to show user info using 'access_token`. # +# - 'logout' button to be finished OIDC session by IdP. # +# - API authorization by validating `access_token` w/ IdP's JWK # +# # +# -----------------------------------------------------------------------------# + +# +# Upstream server for proxing to the frontend site. +# - Example of a bundle frontend app to locally test NGINX Plus OIDC workflow. +# https://github.com/nginx-openid-connect/nginx-oidc-examples/blob/main/001-oidc-local-test/docker/build-context/nginx/sample/proxy_server_frontend.conf +# This link is subject to change. +# - Modify this configuration to match your frontend site. +# +upstream my_frontend_site { + zone my_frontend_site 64k; + server 127.0.0.1:9091; +} + +# +# Upstream sample for proxing to the backend API server. +# - Example of a bundle backend app to locally test an API using access token. +# + https://github.com/nginx-openid-connect/nginx-oidc-examples/blob/main/001-oidc-local-test/docker/build-context/nginx/sample/proxy_server_backend.conf +# This link is subject to change. +# - Modify this configuration to match your backend app. +# +upstream my_backend_app { + zone my_backend_app 64k; + server 127.0.0.1:9092; +} + +# +# Sample Frontend-site & backend-api-server for the OIDC workflow. +# +server { + # Enable when debugging is needed. + error_log /var/log/nginx/error.log debug; # Reduce severity level as required + access_log /var/log/nginx/access.log main; + + # Replace the following server name with your host name. + # + # [Example: if you want to locally test OIDC in your laptop] + # - Add '127.0.0.1 nginx.oidc.test` in your `/etc/hosts'. + # - Use the command like 'make start'. + # - Type 'https://nginx.oidc.test' in your browser. + # - You will see the sample landing page and 'Sign In' button. + # + listen 8020; # Use SSL/TLS in production + server_name nginx.oidc.test; + + # Replace the following files with your certificate. + ssl_certificate /etc/ssl/nginx/nginx-repo.crt; + ssl_certificate_key /etc/ssl/nginx/nginx-repo.key; + + # OIDC workflow + include conf.d/openid_connect.server_conf; + + # + # Frontend example: + # + # - Default landing page: no need OIDC workflow to show 'Sign In' button. + # - The site is protected with OpenID Connect(OIDC) by calling the API + # endpoint of `/login` when users click 'login' button. + # + location / { + proxy_pass http://my_frontend_site; + access_log /var/log/nginx/access.log main_jwt; + } + + # + # Backend API example to interact with proxied backend service: + # + # - This API resource is protected by access token which is received by IdP + # after successful signing-in among the frontend site, NGINX Plus and IdP. + # + # - To ensure that client requests access the API securely, access token is + # used for API authorization. + # + Most of IdP generate an access token for API authorization of IdP's + # endpoints (like /userinfo) as well as customer's endpoints. + # + But Azure AD generate two types of access token for API authorization + # of Microsoft graph API endpoints and customers' endpoints. + # + Therefore, we recommend that you use $session_jwt for Azure AD and + # $access_token for most of IdPs such as Cognito, Auth0, Keycloak, Okta, + # OneLogin, Ping Identity, etc as for now. + # + location /v1/api/example { + auth_jwt "" token=$access_token; # Use $session_jwt for Azure AD + auth_jwt_key_request /_jwks_uri; # Enable when using URL + #auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename + + proxy_set_header Authorization "Bearer $access_token"; + proxy_pass http://my_backend_app; + access_log /var/log/nginx/access.log main_jwt; + } +} + # vim: syntax=nginx diff --git a/openid_connect.js b/openid_connect.js index 4ec46ba..c16dfec 100644 --- a/openid_connect.js +++ b/openid_connect.js @@ -5,7 +5,15 @@ */ var newSession = false; // Used by oidcAuth() and validateIdToken() -export default {auth, codeExchange, validateIdToken, logout}; +export default { + auth, + codeExchange, + validateIdToken, + logout, + v2logout, + redirectPostLogin, + redirectPostLogout +}; function retryOriginalRequest(r) { delete r.headersOut["WWW-Authenticate"]; // Remove evidence of original failed auth_jwt @@ -104,6 +112,7 @@ function auth(r, afterSyncCheck) { // ID Token is valid, update keyval r.log("OIDC refresh success, updating id_token for " + r.variables.cookie_auth_token); r.variables.session_jwt = tokenset.id_token; // Update key-value store + r.variables.access_token = tokenset.access_token; // Update refresh token (if we got a new one) if (r.variables.refresh_token != tokenset.refresh_token) { @@ -187,6 +196,7 @@ function codeExchange(r) { // Add opaque token to keyval session store r.log("OIDC success, creating session " + r.variables.request_id); r.variables.new_session = tokenset.id_token; // Create key-value store entry + r.variables.new_access_token = tokenset.access_token; r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags; r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir); } @@ -256,6 +266,7 @@ function validateIdToken(r) { function logout(r) { r.log("OIDC logout for " + r.variables.cookie_auth_token); r.variables.session_jwt = "-"; + r.variables.access_token = "-"; r.variables.refresh_token = "-"; r.return(302, r.variables.oidc_logout_redirect); } @@ -294,4 +305,69 @@ function idpClientAuth(r) { } else { return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret; } -} \ No newline at end of file +} + +// +// Redirect URI after logging in the IDP. +function redirectPostLogin(r) { + r.return(302, r.variables.redirect_base + getIDTokenArgsAfterLogin(r)); +} + +// +// Get query parameter of ID token after sucessful login: +// +// - For the variable of `returnTokenToClientOnLogin` of the APIM, this config +// is only effective for /login endpoint. By default, our implementation MUST +// not return any token back to the client app. +// - If its configured it can send id_token in the request uri as +// `?id_token=sdfsdfdsfs` after successful login. +// +// +function getIDTokenArgsAfterLogin(r) { + if (r.variables.return_token_to_client_on_login == 'id_token') { + return '?id_token=' + r.variables.id_token; + } + return ''; +} + +// +// RP-Initiated or Custom Logout w/ Idp. +// +// - An RP requests that the Idp log out the end-user by redirecting the +// end-user's User Agent to the Idp's Logout endpoint. +// - TODO: Handle custom logout parameters if Idp doesn't support standard spec +// of 'OpenID Connect RP-Initiated Logout 1.0'. +// +// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout +// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout +// +function v2logout(r) { + r.log("OIDC logout for " + r.variables.cookie_auth_token); + var idToken = r.variables.session_jwt; + var queryParams = getRPInitiatedLogoutArgs(r, idToken); + + r.variables.request_id = '-'; + r.variables.session_jwt = '-'; + r.variables.access_token = '-'; + r.variables.refresh_token = '-'; + r.return(302, r.variables.oidc_end_session_endpoint + queryParams); +} + +// +// Get query params for RP-initiated logout: +// +// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout +// - https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout +// +function getRPInitiatedLogoutArgs(r, idToken) { + return '?post_logout_redirect_uri=' + r.variables.redirect_base + + r.variables.oidc_logout_redirect_uri + + '&id_token_hint=' + idToken; +} + +// +// Redirect URI after logged-out from the IDP. +// +function redirectPostLogout(r) { + r.return(302, r.variables.post_logout_return_uri); +} diff --git a/openid_connect.server_conf b/openid_connect.server_conf index 13456d2..656f223 100644 --- a/openid_connect.server_conf +++ b/openid_connect.server_conf @@ -1,7 +1,10 @@ # Advanced configuration START set $internal_error_message "NGINX / OpenID Connect login failure\n"; set $pkce_id ""; - resolver 8.8.8.8; # For DNS lookup of IdP endpoints; + resolver 8.8.8.8; # For global DNS lookup of IDP endpoint + # 127.0.0.11; # For local Docker DNS lookup + + resolver_timeout 10s; subrequest_output_buffer_size 32k; # To fit a complete tokenset response gunzip on; # Decompress IdP responses if necessary # Advanced configuration END @@ -42,7 +45,7 @@ proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location"; proxy_method POST; proxy_pass $oidc_token_endpoint; - } + } location = /_refresh { # This location is called by oidcAuth() when performing a token refresh. We @@ -66,6 +69,70 @@ error_page 500 502 504 @oidc_error; } + # + # User information endpoint for the following purposes: + # - Browser to periodically check if your are signed-in based on status code. + # - Browser to show the signed-in user information. + # - https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + # + location = /userinfo { + auth_jwt "" token=$access_token; # Access token for API authorization + #auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename + auth_jwt_key_request /_jwks_uri; # Enable when using URL + + proxy_ssl_server_name on; # For SNI to the IdP + proxy_set_header Authorization "Bearer $access_token"; + proxy_pass $oidc_userinfo_endpoint; + access_log /var/log/nginx/access.log main_jwt; + } + + # + # Login endpoint to start OIDC flow when a user clicks 'login' button in the + # landing page. + # + location = /login { + # This location is called by UI for logging-in IDP using OpenID Connect. + auth_jwt "" token=$session_jwt; # ID token for user authentication. + error_page 401 = @do_oidc_flow; + + #auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename + auth_jwt_key_request /_jwks_uri; # Enable when using URL + + # Redirect to the the original URI of UI after successful login to IDP. + js_content oidc.redirectPostLogin; + access_log /var/log/nginx/access.log main_jwt; + } + + # + # V2 Logout: The following features are added in the NGINX R29. + # - The spec of RP-Initiated Logout is added. + # - Sample logout page for your OIDC simulation. + # - TODO: Custom logout parameters will be separately added. + # + location = /v2/logout { + # This location is called by UI to handle OIDC logout with IDP as per: + # https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout + status_zone "OIDC logout"; + js_content oidc.v2logout; + } + + location = /v2/_logout { + # This location is the default value of $oidc_logout_redirect_uri (in case + # it wasn't configured) called by IdP after closing user session in IdP. + + # Clean cookies + add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie + add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie + add_header Set-Cookie "auth_nonce=; $oidc_cookie_flags"; + + # Redirect to either the original page or custom logout page. + js_content oidc.redirectPostLogout; + } + + # + # V1 Logout (NGINX R28): + # - Need to implement 'RP-Initiated or Custom Logout' by yourselves. + # location = /logout { status_zone "OIDC logout"; add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie diff --git a/openid_connect_configuration.conf b/openid_connect_configuration.conf index f5668ee..129eb65 100644 --- a/openid_connect_configuration.conf +++ b/openid_connect_configuration.conf @@ -16,6 +16,14 @@ map $host $oidc_jwt_keyfile { default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs"; } +map $host $oidc_end_session_endpoint { + default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/logout"; +} + +map $host $oidc_userinfo_endpoint { + default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/userinfo"; +} + map $host $oidc_client { default "my-client-id"; } @@ -35,7 +43,54 @@ map $host $oidc_scopes { map $host $oidc_logout_redirect { # Where to send browser after requesting /logout location. This can be # replaced with a custom logout page, or complete URL. - default "/_logout"; # Built-in, simple logout page + + default "/_logout"; # Built-in, simple logout page for NGINX R28 +} + +map $host $oidc_logout_redirect_uri { # use for NGINX R29+ versions + # This is the redirect URI which is called by IdP to erase cookies and + # redirect to the original page or custom logout page after successfully + # ending OIDC session from IdP. + default "/v2/_logout"; +} + +map $host $post_logout_return_uri { + # The following examples can be replaced with a custom logout page, or + # a complete URL to be redirected after successful logout from the IdP. + + # Example 1: Redirect to the original langding page. + # ./docker/build-context/nginx/sample/proxy_server_frontend.conf + # -> redirect to the '/' location block + # ./docker/build-context/content/index.html + # + default $redirect_base; + + # Example 2: Redirect to a custom logout page + # ./docker/build-context/nginx/sample/proxy_server_frontend.conf + # -> redirect to the '/signout' location block + # ./docker/build-context/content/signout.html + # + # default $redirect_base/signout; + + # Example 3: Redirect to an another URL + # default https://www.nginx.com; +} + +map $host $return_token_to_client_on_login { + # This is to return token as a query param to the app after successful login. + # + # - The NGINX Management Suite - API Connectivity Manager automatically + # configure this value. + # - You can manually edit this option. But it is not normally used for most + # of IdPs. + # + # +------------+-----------------------------------------------------------+ + # | options | example | + # +------------+-----------------------------------------------------------+ + # | id_token | http://my-nginx.example.com?id_token=sdfsdfdsfs | + # | none or "" | http://my-nginx.example.com (no query param) | + # +------------+-----------------------------------------------------------+ + default ""; } map $host $oidc_hmac_key { @@ -75,15 +130,18 @@ map $http_x_forwarded_proto $proto { proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m; # Change timeout values to at least the validity period of each token type -keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h; -keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h; +keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h; +keyval_zone zone=oidc_access_tokens:1M state=conf.d/oidc_access_tokens.json timeout=1h; +keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h; keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier. -keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT -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_refresh zone=refresh_tokens; # '' -keyval $pkce_id $pkce_code_verifier zone=oidc_pkce; +keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for 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 conf.d/openid_connect.js; From bc81b152121fc7f0609f26613ec2d763ebd706e8 Mon Sep 17 00:00:00 2001 From: Jodie Putrino Date: Wed, 9 Nov 2022 16:54:01 -0700 Subject: [PATCH 2/2] Edits to README complete added doctoc for automatic toc creation broke getting started info out into new doc --- README.md | 329 ++++++++----------------------------- docs/02-getting-started.md | 225 +++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 265 deletions(-) create mode 100644 docs/02-getting-started.md diff --git a/README.md b/README.md index 71c2f0c..cd20730 100644 --- a/README.md +++ b/README.md @@ -2,313 +2,112 @@ Reference implementation of NGINX Plus as relying party for OpenID Connect authentication -- [Description](#description) - - [Refresh Tokens](#refresh-tokens) - - [OpenID Connect Userinfo Endpoint](#openid-connect-userinfo-endpoint) - - [Login Behavior](#login-behavior) + + + +- [Overview](#overview) + - [Requirements](#requirements) + - [Authorization Code Grant Flow](#authorization-code-grant-flow) + - [OpenID Connect UserInfo Endpoint](#openid-connect-userinfo-endpoint) - [Logout Behavior](#logout-behavior) - [Multiple IdPs](#multiple-idps) -- [Quick Start Guide](#quick-start-guide) -- [Installation](#installation) -- [Configuring your IdP](#configuring-your-idp) -- [Configuring NGINX Plus](#configuring-nginx-plus) -- [Session Management](#session-management) -- [Real time monitoring](#real-time-monitoring) -- [Troubleshooting](#troubleshooting) +- [Documentation](#documentation) - [Support](#support) -- [Changelog](#changelog) - -## Description - -This repository describes how to enable OpenID Connect integration for [NGINX Plus](https://www.nginx.com/products/nginx/). The solution depends on NGINX Plus components ([auth_jwt module](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html) and [key-value store](http://nginx.org/en/docs/http/ngx_http_keyval_module.html)) and as such is not suitable for [open source NGINX](http://www.nginx.org/en). - -OpenID Connect components - -`Figure 1. High level components of an OpenID Connect environment` - -This implementation assumes the following environment: - -- The identity provider (IdP) supports OpenID Connect 1.0 -- The authorization code flow is in use -- NGINX Plus is configured as a relying party -- The IdP knows NGINX Plus as a confidential client or a public client using PKCE - -With this environment, both the client and NGINX Plus communicate directly with the IdP at different stages during the initial authentication event. - -![OpenID Connect protocol diagram](https://www.nginx.com/wp-content/uploads/2018/04/dia-LC-2018-03-30-OpenID-Connect-authentication-code-flow-detailed-800x840-03.svg) -`Figure 2. OpenID Connect authorization code flow protocol` - -NGINX Plus is configured to perform OpenID Connect authentication. Upon a first visit to a protected resource, NGINX Plus initiates the OpenID Connect authorization code flow and redirects the client to the OpenID Connect provider (IdP). When the client returns to NGINX Plus with an authorization code, NGINX Plus exchanges that code for a set of tokens by communicating directly with the IdP. - -The ID Token received from the IdP is [validated](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation). NGINX Plus then stores the ID token in the key-value store, issues a session cookie to the client using a random string, (which becomes the key to obtain the ID token from the key-value store) and redirects the client to the original URI requested prior to authentication. - -Subsequent requests to protected resources are authenticated by exchanging the session cookie for the ID Token in the key-value store. JWT validation is performed on each request, as normal, so that the ID Token validity period is enforced. - -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/). - -### Refresh Tokens - -If a [refresh token](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens) was received from the IdP then it is also stored in the key-value store. When validation of the ID Token fails (typically upon expiry) then NGINX Plus sends the refresh token to the IdP. If the user's session is still valid at the IdP then new ID token and access token are received, validated, and updated in the key-value store. The refresh process is seamless to the client. The ID token is used for user authentication with login and logout, and the access is used for API authorization before proxing to IdP endpoint such as `/userinfo` or custom backend APIs. - -### OpenID Connect UserInfo Endpoint - -The [OpenID Connect UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) (/userinfo) provides details about the logged-in user. Requests to /userinfo must be authenticated using the access token provided as part of the authorization code flow. - -- When a user is not logged in, requests to the `/userinfo` endpoint return a `401` (unauthorized) response. -- When a user is logged in, requests to `/userinfo` return a `200` response with the requested user information (such as name). - -### Login Behavior - -When a client requests an application's `/login` location, NGINX Plus starts the following OIDC workflow: - -- When an End-User clicks a `Login` button, the frontend app calls `/login` endpoint -- The `/login` endpoint calls IdP's `authorization_endpoint`, and IdP issued the ID token, access token, and refresh token to NGINX Plus. -- The tokens are stored in the key-value store of NGINX Plus. -- NGINX Plus redirects to the frontend landing page with session cookie after these successful login processes. -- The frontend app shows user information in detail by calling `/userinfo` endpoint. - -### Logout Behavior - -Requests made to the `/logout` location invalidate both the ID token and refresh token by erasing them from the key-value store. Therefore, subsequent requests to protected resources will be treated as a first-time request and send the client to the IdP for authentication. Note that the IdP may issue cookies such that an authenticated session still exists at the IdP. - -To avoid breaking changes of API endpoints to customers who use a `/logout` location, the `/v2/logout` location is added to interact with the IdP's `end_session_endpoint` which is to handle [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout) as the spec of OpenID Connect. The `$post_logout_return_uri` is the URI to which the RP is requesting that the End-User's User Agent be redirected after a logout has been performed. When setting up an IdP, `/v2/_logout` is used in this example, and you can change it per your preference. - -### Multiple IdPs - -Where NGINX Plus is configured to proxy requests for multiple websites or applications, or user groups, these may require authentication by different IdPs. Separate IdPs can be configured, with each one matching on an attribute of the HTTP request, e.g. hostname or part of the URI path. - -> **Note:** When validating OpenID Connect tokens, NGINX Plus can be configured to read the signing key (JWKS) from disk, or a URL. When using multiple IdPs, each one must be configured to use the same method. It is not possible to use a mix of both disk and URLs for the `map…$oidc_jwt_keyfile` variable. - -## Quick Start Guide - -Set up a [local demo](https://github.com/nginx-openid-connect/nginx-oidc-examples/tree/main/001-oidc-local-test) that can be used for testing purposes: - -> **Note:** This link is subject to change. - -1. Running a Docker container -2. Checking If a Single Page App works in your browser -3. Setting up an Identity Provider (IdP) -4. Configuring NGINX Plus -5. Testing NGINX Plus OIDC -6. Stop and remove Docker containers - -## Installation - -Start by [installing NGINX Plus](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-plus/). In addition, the [NGINX JavaScript module](https://www.nginx.com/blog/introduction-nginscript/) (njs) is required for handling the interaction between NGINX Plus and the OpenID Connect provider (IdP). Install the njs module after installing NGINX Plus by running one of the following: - -`$ sudo apt install nginx-plus-module-njs` for Debian/Ubuntu - -`$ sudo yum install nginx-plus-module-njs` for CentOS/RHEL - -The njs module needs to be loaded by adding the following configuration directive near the top of **nginx.conf**. - -```nginx -load_module modules/ngx_http_js_module.so; -``` - -Finally, create a clone of the GitHub repository. - -`$ git clone https://github.com/nginxinc/nginx-openid-connect` - -> **Note:** There is a branch for each NGINX Plus release. Switch to the correct branch to ensure compatibility with the features and syntax of each release. The main branch works with the most recent NGINX Plus and JavaScript module releases. - -All files can be copied to **/etc/nginx/conf.d** - -### Non-standard directories - -The GitHub repository contains [`include`](http://nginx.org/en/docs/ngx_core_module.html#include) files for NGINX configuration, and JavaScript code for token exchange and initial token validation. These files are referenced with a relative path (relative to /etc/nginx). If NGINX Plus is running from a non-standard location then copy the files from the GitHub repository to `/path/to/conf/conf.d` and use the `-p` flag to start NGINX with a prefix path that specifies the location where the configuration files are located. - -```shell -$ nginx -p /path/to/conf -c /path/to/conf/nginx.conf -``` - -### Running in containers -This implementation is suitable for running in a container provided that the [base image](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-docker/) includes the NGINX JavaScript module. The GitHub repository is designed to facilitate testing with a container by binding the cloned repository to a mount volume on the container. + -```shell -$ cd nginx-openid-connect -$ docker run -d -p 8010:8010 -v $PWD:/etc/nginx/conf.d nginx-plus nginx -g 'daemon off; load_module modules/ngx_http_js_module.so;' -``` - -### Running behind another proxy or load balancer - -When NGINX Plus is deployed behind another proxy, the original protocol and port number are not available. NGINX Plus needs this information to construct the URIs it passes to the IdP and for redirects. By default NGINX Plus looks for the X-Forwarded-Proto and X-Forwarded-Port request headers to construct these URIs. - -## Configuring your IdP - -- Create an OpenID Connect client to represent your NGINX Plus instance +## Overview - - 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) - - Make a note of the `client ID` and `client secret` if set +This repository provides a reference implementation for setting up [NGINX Plus](https://www.nginx.com/products/nginx/) integrations with OpenID Connect (OIDC). By implementing this solution, you can allow users to access your application by logging in with a supported Identity Provider (IdP). -- 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: - - Obtain the URL for `jwks_uri` or download the JWK file to your NGINX Plus instance - - Obtain the URL for the **authorization endpoint** - - Obtain the URL for the **token endpoint** - - Obtain the URL for the **end session endpoint** - - Obtain the URL for the **user info endpoint** +> **Note**: This solution requires modules that are only available in NGINX Plus: [auth_jwt module](https://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html) and [key-value store](https://nginx.org/en/docs/http/ngx_http_keyval_module.html). It cannot be used with [NGINX open source](http://www.nginx.org/en). -## Configuring NGINX Plus +The OpenID Connect solution's basic authorization flow is shown in Figure 1. In this flow, NGINX Plus acts as a relying party that uses the IdP's authorization to allow access to your backend application. -Configuration can typically be completed automatically by using the `configure.sh` script. +![OpenID Connect Authorization Flow](https://www.nginx.com/wp-content/uploads/2018/04/dia-LC-2018-03-30-OpenID-Connect-authorization-code-flow-NGINX-800x426-03.svg "Figure 1: High-level authorization flow with OpenID Connect and NGINX Plus") -Manual configuration involves reviewing the following files so that they match your IdP(s) configuration. +### Requirements -- **openid_connect_configuration.conf** - this contains the primary configuration for one or more IdPs in `map{}` blocks +This solution requires the following: - - Modify all of the `map…$oidc_` blocks to match your IdP configuration - - Modify the URI defined in `map…$oidc_logout_redirect` to specify an unprotected resource to be displayed after requesting the `/logout` location for customers who has been using R28. - - Modify the URI defined in `map…$oidc_logout_redirect_uri` to specify an unprotected resource to be displayed after requesting the `/v2/_logout` location for customers who start using R29 and wants to change from `map…$oidc_logout_redirect` to `map…$oidc_logout_redirect_uri` to use the feature of `OIDC RP-Initiated Logout` which is interact with IdP's `end_session_endpoint`. - - Set a unique value for `$oidc_hmac_key` to ensure nonce values are unpredictable - - If NGINX Plus is deployed behind another proxy or load balancer, modify the `map…$redirect_base` and `map…$proto` blocks to define how to obtain the original protocol and port number. +- NGINX Plus and the `njs` module +- An Identity Provider (IdP) that supports [OpenID Connect 1.0](https://openid.net/connect/). +- Your application and IdP support the OIDC authorization code grant flow shown in Figure 2. +- The IdP can recognize NGINX Plus as a confidential client or a public client using PKCE. -- **frontend.conf** - this is the reverse proxy configuration +## Documentation - - Modify the upstream group to match your backend site or app - - Configure the preferred listen port and [enable SSL/TLS configuration](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/) - - Modify the severity level of the `error_log` directive to suit the deployment environment - - Comment/uncomment the `auth_jwt_key_file` or `auth_jwt_key_request` directives based on whether `$oidc_jwt_keyfile` is a file or URI, respectively - - Examples - 1. **Basic Example:** Landing page starts OIDC flow without a login button. - 2. **Advanced Example:** Landing page, login/logout button to start/finish OIDC workflow. - - Landing page with `login` button - - `login` button to start OIDC flow by validating `id token` with the JWK of IdP. - - Landing page calls the `/userinfo` endpoint to show user information by validating `access token` with the JWK of IdP. - - `logout` button to close the OIDC session among frontend, NGINX Plus, and IdP. - - The proxied API authorization by validating `access token` with the JWK of IdP. - - Use `access token` for most of IdPs such as Amazon Cognito, Auth0, Keycloak, Okta, OneLogin and Ping Identity. - - Use `session_jwt` for Azure AD as for now. +This document provides an overview of the OIDC solution and things to take into consideration when designing your own solution. Consult the documentation for setup and testing instructions: -- **openid_connect.server_conf** - this is the NGINX configuration for handling the various stages of OpenID Connect authorization code flow +- [Quick Start Guide](https://github.com/nginx-openid-connect/nginx-oidc-examples/tree/main/001-oidc-local-test) - set up a local demo that can be used for testing purposes +- [Getting Started Guide](/docs/02-getting-started.md) - installation, configuration, and troubleshooting instructions for the OIDC reference implementation - - No changes are usually required here - - Modify the `resolver` directive to match a DNS server that is capable of resolving the IdP defined in `$oidc_token_endpoint` - - If using [`auth_jwt_key_request`](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html#auth_jwt_key_request) to automatically fetch the JWK file from the IdP then modify the validity period and other caching options to suit your IdP +## Authorization Code Grant Flow -- **openid_connect.js** - this is the JavaScript code for performing the authorization code exchange and nonce hashing - - No changes are required unless modifying the code exchange or validation process +The [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) consists of the exchange of an authorization code for an access token between confidential and public clients. In this solution, NGINX Plus acts as a relying party to handle the exchange and ultimately allow or deny access to the requested web application. -### Configuring the Key-Value Store +![OpenID Connect protocol diagram](https://www.nginx.com/wp-content/uploads/2018/04/dia-LC-2018-03-30-OpenID-Connect-authentication-code-flow-detailed-800x840-03.svg "Figure 2. OpenID Connect authorization code flow protocol") -The key-value store is used to maintain persistent storage for ID tokens and refresh tokens. The default configuration should be reviewed so that it suits the environment. This is part of the advanced configuration in **openid_connect_configuration.conf**. +> **Note:** The [openid_connect.server_conf](openid_connect.server_conf) configuration file sets up this authorization flow. -```nginx -keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h; -keyval_zone zone=oidc_access_tokens:1M state=conf.d/oidc_access_tokens.json timeout=1h; -keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h; -keyval_zone zone=oidc_pkce:128K timeout=90s; -``` - -Each of the `keyval_zone` parameters are described below. - -- **zone** - Specifies the name of the key-value store and how much memory to allocate for it. Each session will typically occupy 1-2KB, depending on the size of the tokens, so scale this value to exceed the number of unique users that may authenticate. +When a client requests access to a protected resource for the first time by calling a `/login` endpoint, NGINX Plus initiates the authorization code flow and redirects you to the configured OIDC IdP to log in. After you successfully log in, the IdP sends a redirect URL to the browser, along with an authorization code. The browser then redirects your request and the authorization code to the URL for your NGINX Plus instance, at the `/_codexch` location (for example: `http://myapp.example.com/_codexch). -- **state** (optional) - Specifies where all of the ID Tokens in the key-value store are saved, so that sessions will persist across restart or reboot of the NGINX host. The NGINX Plus user account, typically **nginx**, must have write permission to the directory where the state file is stored. Consider creating a dedicated directory for this purpose. +NGINX Plus then communicates with the IdP to exchange the authorization code for a set of authentication tokens. -- **timeout** - Expired tokens are removed from the key-value store after the `timeout` value. This should be set to value slightly longer than the JWT validity period. JWT validation occurs on each request, and will fail when the expiry date (`exp` claim) has elapsed. If JWTs are issued without an `exp` claim then set `timeout` to the desired session duration. If JWTs are issued with a range of validity periods then set `timeout` to exceed the longest period. +> **Note:** This generally includes an ID token and an access token, and may include a refresh token as well. The ID token is used for user authentication, while the access token authorizes access to IdP endpoints -- such as `/userinfo` -- or to custom backend APIs. We'll get to the refresh token soon. -- **sync** (optional) - If deployed in a cluster, the key-value store may be synchronized across all instances in the cluster, so that all instances are able to create and validate authenticated sessions. Each instance must be configured to participate in state sharing with the [zone_sync module](http://nginx.org/en/docs/stream/ngx_stream_zone_sync_module.html) and by adding the `sync` parameter to the `keyval_zone` directives above. +Next, NGINX Plus [validates the tokens](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) that it received from the IdP. It adds the tokens to the key-value store, then issues a session cookie to the client. The session cookie is used to access the key-value store on later visits, allowing authentication to persist across the lifetime of the session. Finally, the request is sent to the URL that was originally requested -- with the session cookie included in the header -- and the client is allowed to access the requested page. -## Session Management +When you request access to other pages in the same protected location, NGINX Plus uses the session cookie to retrieve the ID token from the key-value store. NGINX Plus performs JSON Web Token (JWT) validation on each request to enforce the ID token's validity period. -The [NGINX Plus API](http://nginx.org/en/docs/http/ngx_http_api_module.html) is enabled in **openid_connect.server_conf** so that sessions can be monitored. The API can also be used to manage the current set of active sessions. +As noted earlier, an IdP may also provide a [refresh token](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens). In these cases, NGINX Plus also adds the refresh token to the key-value store. When ID token validation fails -- which typically happens when the token expires -- NGINX Plus uses the refresh token to generate a new set of ID and access tokens. If the session with the IdP is still valid, the IdP sends a new ID token and access token to NGINX Plus. NGINX Plus then validates the tokens as usual and updates the key-value store. This refresh process is seamless to the client. -To query the current sessions in the key-value store: - -```shell -$ curl localhost:8010/api/6/http/keyvals/oidc_id_tokens -``` +## OpenID Connect UserInfo Endpoint -To delete a single session: +The [OpenID Connect UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) (`/userinfo`) provides details about the logged-in user. Requests to `/userinfo` must be authenticated using the access token provided as part of the authorization code flow. -```shell -$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/oidc_id_tokens -$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/oidc_access_tokens -$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/refresh_tokens -``` +- When a user is not logged in, requests to the `/userinfo` endpoint return a `401` (unauthorized) response. +- When a user is logged in, requests to `/userinfo` return a `200` response with the requested user information (such as name). -To delete all sessions: +The `/userinfo` endpoint location is stored in the NGINX Plus OIDC configuration as the `$oidc_userinfo_endpoint` variable. In the example configuration below, the `location` context provides a front-end application access to the IdP's `/userinfo` endpoint. The `access_token` value comes from the NGINX Plus exchange with the IdP, while the `oidc_jwt_keyfile` and `oidc_userinfo_endpoint` values come from the IdP configuration. -```shell -$ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_id_tokens -$ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_access_tokens -$ curl -iX DELETE localhost:8010/api/6/http/keyvals/refresh_tokens ``` - -## Real time monitoring - -The **openid_connect.server_conf** file defines several [`status_zone`](http://nginx.org/en/docs/http/ngx_http_api_module.html#status_zone) directives to collect metrics about OpenID Connect activity and errors. Separate metrics counters are recorded for: - -- **OIDC start** - New sessions are counted here. See step 2 in Figure 2, above. Success is recorded as a 3xx response. - -- **OIDC code exchange** - Counters are incremented here when the browser returns to NGINX Plus after authentication. See steps 6-10 in Figure 2, above. Success is recorded as a 3xx response. - -- **OIDC logout** - Requests to the URIs of either `/logout` or `/v2/logout` are counted here. Success is recorded as a 3xx response. - -- **OIDC error** - Counters are incremented here when errors in the code exchange process are actively detected. Typically there will be a corresponding error_log entry. - -To obtain the current set of metrics: - -```shell -$ curl localhost:8010/api/6/http/location_zones +# +# User information endpoint used for the following purposes: +# - Browser to periodically check if you are signed in, based on status code. +# - Browser to show the signed-in user information. +# - https://openid.net/specs/openid-connect-core-1_0.html#UserInfo +# +location = /userinfo { + auth_jwt "" token=$access_token; # Access token for API authorization + #auth_jwt_key_file $oidc_jwt_keyfile; # Enable when using filename + auth_jwt_key_request /_jwks_uri; # Enable when using URL + + proxy_ssl_server_name on; # For SNI to the IdP + proxy_set_header Authorization "Bearer $access_token"; + proxy_pass $oidc_userinfo_endpoint; + access_log /var/log/nginx/access.log oidc_jwt; +} ``` -In addition, the [NGINX Plus Dashboard](https://docs.nginx.com/nginx/admin-guide/monitoring/live-activity-monitoring/#dashboard) can be configured to visualize the monitoring metrics in a GUI. - -## Troubleshooting - -Any errors generated by the OpenID Connect flow are logged to the error log, `/var/log/nginx/error.log`. Check the contents of this file as it may include error responses received by the IdP. The level of detail recorded can be modified by adjusting the severity level of the `error_log` directive. +## Logout Behavior -- **400 error from IdP** +When a client requests an application's `/logout` location, NGINX Plus invalidates the ID, access, and refresh tokens by erasing them from the key-value store. Any additional client requests to protected resources will be redirected to the IdP for authentication. - - This is typically caused by incorrect configuration related to the client ID and client secret. - - Check the values of the `map…$oidc_client` and `map…$oidc_client_secret` variables against the IdP configuration. +> **Note:** When NGINX Plus -- which is a "Relying Party" (RP) -- performs a logout, an authenticated session may still exist with the IdP. OIDC provides a spec for [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout) to ensure the logout is also performed with the IdP. -- **500 error from nginx after successful authentication** - - Check for `could not be resolved` and `empty JWK set while sending to client` messages in the error log. This is common when NGINX Plus cannot reach the IdP's `jwks_uri` endpoint. - - Check the `map…$oidc_jwt_keyfile` variable is correct. - - Check the `resolver` directive in **openid_connect.server_conf** is reachable from the NGINX Plus host. - - Check for `OIDC authorization code sent but token response is not JSON.` messages in the error log. This is common when NGINX Plus cannot decompress the IdP's response. Add the following configuration snippet to the `/_jwks_uri` and `/_token` locations to **openid_connect.server_conf**: - -```nginx - proxy_set_header Accept-Encoding "gzip"; -``` +To avoid breaking changes of API endpoints to customers, the OIDC RP-Initiated Logout spec adds the `/v2/logout` location. This location is used to interact with the IdP's `end_session_endpoint`, which handles RP-Initiated logout requests. The `$post_logout_return_uri` is the URI to which the RP is requesting that the End-User's User Agent be redirected after a logout has been performed. -- **Authentication is successful but browser shows too many redirects** +The examples in this repository use the `/v2/_logout` for IdP configurations. You can change this to use `/v2/logout` according to your needs. - - This is typically because the JWT sent to the browser cannot be validated, resulting in 'authorization required' `401` response and starting the authentication process again. But the user is already authenticated so is redirected back to NGINX, hence the redirect loop. - - Avoid using `auth_jwt_require` directives in your configuration because this can also return a `401` which is indistinguishable from missing/expired JWT. - - Check the error log `/var/log/nginx/error.log` for JWT/JWK errors. - - Ensure that the JWK file (`map…$oidc_jwt_keyfile` variable) is correct and that the nginx user has permission to read it. +> Note: Support for the `/v2/logout` endpoint was introduced in NGINX Plus R29. -- **Logged out but next request does not require authentication** +## Using NGINX Plus with Multiple IdPs - - This is typically caused by the IdP issuing its own session cookie(s) to the client. NGINX Plus sends the request to the IdP for authentication and the IdP immediately sends back a new authorization code because the session is still valid. - - Check your IdP configuration if this behavior is not desired. +NGINX Plus can be configured to proxy requests for multiple websites or applications, or user groups, which may require authentication by different IdPs. You can configure NGINX Plus to use multiple IdPs, with each one matching on an attribute of the HTTP request (for example, hostname or part of the URI path). -- **Failed SSL/TLS handshake to IdP** - - Indicated by error log messages including `peer closed connection in SSL handshake (104: Connection reset by peer) while SSL handshaking to upstream`. - - This can occur when the IdP requires Server Name Indication (SNI) information as part of the TLS handshake. Additional configuration is required to satisfy this requirement. - - Edit **openid_connect.server_conf** and for each of the `/_jwks_uri`, `/_token`, and `/_refresh` locations, add the following configuration snippet: - -```nginx -proxy_set_header Host ; -proxy_ssl_name ; -``` +> **Note:** When validating OpenID Connect tokens, NGINX Plus can be configured to read the signing key (JWKS) from disk or via a URL. When using multiple IdPs, **each must be configured to use the same method**. Using a mix of both disk and URLs for the `map...$oidc_jwt_keyfile` variable is not supported. ## Support This reference implementation for OpenID Connect is supported for NGINX Plus subscribers. - -## Changelog - -- **R15** Initial release of OpenID Connect reference implementation -- **R16** Added support for opaque session tokens using key-value store -- **R17** Configuration now supports JSON Web Key (JWK) set to be obtained by URI -- **R18** Opaque session tokens now used by default. Added support for refresh tokens. Added `/logout` location. -- **R19** Minor bug fixes -- **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. diff --git a/docs/02-getting-started.md b/docs/02-getting-started.md new file mode 100644 index 0000000..ba02154 --- /dev/null +++ b/docs/02-getting-started.md @@ -0,0 +1,225 @@ +# 🏠 Getting Started Guide + + + + +- [🏠 Getting Started Guide](#-getting-started-guide) + - [Install NGINX Plus](#install-nginx-plus) + - [Non-standard directories](#non-standard-directories) + - [Running in containers](#running-in-containers) + - [Running behind another proxy or load balancer](#running-behind-another-proxy-or-load-balancer) + - [Configuring your IdP](#configuring-your-idp) + - [Configuring NGINX Plus](#configuring-nginx-plus) + - [Configuring the Key-Value Store](#configuring-the-key-value-store) + - [Session Management](#session-management) + - [Real time monitoring](#real-time-monitoring) + - [Troubleshooting](#troubleshooting) + + + +## Install NGINX Plus + +Start by [installing NGINX Plus](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-plus/). In addition, the [NGINX JavaScript module](https://www.nginx.com/blog/introduction-nginscript/) (njs) is required for handling the interaction between NGINX Plus and the OpenID Connect provider (IdP). Install the njs module after installing NGINX Plus by running one of the following: + +`$ sudo apt install nginx-plus-module-njs` for Debian/Ubuntu + +`$ sudo yum install nginx-plus-module-njs` for CentOS/RHEL + +The njs module needs to be loaded by adding the following configuration directive near the top of **nginx.conf**. + +```nginx +load_module modules/ngx_http_js_module.so; +``` + +Finally, create a clone of the GitHub repository. + +`$ git clone https://github.com/nginxinc/nginx-openid-connect` + +> **Note:** There is a branch for each NGINX Plus release. Switch to the correct branch to ensure compatibility with the features and syntax of each release. The main branch works with the most recent NGINX Plus and JavaScript module releases. + +All files can be copied to **/etc/nginx/conf.d** + +### Non-standard directories + +The GitHub repository contains [`include`](http://nginx.org/en/docs/ngx_core_module.html#include) files for NGINX configuration, and JavaScript code for token exchange and initial token validation. These files are referenced with a relative path (relative to /etc/nginx). If NGINX Plus is running from a non-standard location then copy the files from the GitHub repository to `/path/to/conf/conf.d` and use the `-p` flag to start NGINX with a prefix path that specifies the location where the configuration files are located. + +```shell +$ nginx -p /path/to/conf -c /path/to/conf/nginx.conf +``` + +### Running in containers + +This implementation is suitable for running in a container provided that the [base image](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-docker/) includes the NGINX JavaScript module. The GitHub repository is designed to facilitate testing with a container by binding the cloned repository to a mount volume on the container. + +```shell +$ cd nginx-openid-connect +$ docker run -d -p 8010:8010 -v $PWD:/etc/nginx/conf.d nginx-plus nginx -g 'daemon off; load_module modules/ngx_http_js_module.so;' +``` + +### Running behind another proxy or load balancer + +When NGINX Plus is deployed behind another proxy, the original protocol and port number are not available. NGINX Plus needs this information to construct the URIs it passes to the IdP and for redirects. By default NGINX Plus looks for the X-Forwarded-Proto and X-Forwarded-Port request headers to construct these URIs. + +## Configuring your IdP + +- Create an OpenID Connect client to represent your NGINX Plus instance + + - 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) + - 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: + - Obtain the URL for `jwks_uri` or download the JWK file to your NGINX Plus instance + - Obtain the URL for the **authorization endpoint** + - Obtain the URL for the **token endpoint** + - Obtain the URL for the **end session endpoint** + - Obtain the URL for the **user info endpoint** + +## Configuring NGINX Plus + +Configuration can typically be completed automatically by using the `configure.sh` script. + +Manual configuration involves reviewing the following files so that they match your IdP(s) configuration. + +- **openid_connect_configuration.conf** - this contains the primary configuration for one or more IdPs in `map{}` blocks + + - Modify all of the `map…$oidc_` blocks to match your IdP configuration + - Modify the URI defined in `map…$oidc_logout_redirect` to specify an unprotected resource to be displayed after requesting the `/logout` location for customers who has been using R28. + - Modify the URI defined in `map…$oidc_logout_redirect_uri` to specify an unprotected resource to be displayed after requesting the `/v2/_logout` location for customers who start using R29 and wants to change from `map…$oidc_logout_redirect` to `map…$oidc_logout_redirect_uri` to use the feature of `OIDC RP-Initiated Logout` which is interact with IdP's `end_session_endpoint`. + - Set a unique value for `$oidc_hmac_key` to ensure nonce values are unpredictable + - If NGINX Plus is deployed behind another proxy or load balancer, modify the `map…$redirect_base` and `map…$proto` blocks to define how to obtain the original protocol and port number. + +- **frontend.conf** - this is the reverse proxy configuration + + - Modify the upstream group to match your backend site or app + - Configure the preferred listen port and [enable SSL/TLS configuration](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/) + - Modify the severity level of the `error_log` directive to suit the deployment environment + - Comment/uncomment the `auth_jwt_key_file` or `auth_jwt_key_request` directives based on whether `$oidc_jwt_keyfile` is a file or URI, respectively + - Examples + 1. **Basic Example:** Landing page starts OIDC flow without a login button. + 2. **Advanced Example:** Landing page, login/logout button to start/finish OIDC workflow. + - Landing page with `login` button + - `login` button to start OIDC flow by validating `id token` with the JWK of IdP. + - Landing page calls the `/userinfo` endpoint to show user information by validating `access token` with the JWK of IdP. + - `logout` button to close the OIDC session among frontend, NGINX Plus, and IdP. + - The proxied API authorization by validating `access token` with the JWK of IdP. + - Use `access token` for most of IdPs such as Amazon Cognito, Auth0, Keycloak, Okta, OneLogin and Ping Identity. + - Use `session_jwt` for Azure AD as for now. + +- **openid_connect.server_conf** - this is the NGINX configuration for handling the various stages of OpenID Connect authorization code flow + + - No changes are usually required here + - Modify the `resolver` directive to match a DNS server that is capable of resolving the IdP defined in `$oidc_token_endpoint` + - If using [`auth_jwt_key_request`](http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html#auth_jwt_key_request) to automatically fetch the JWK file from the IdP then modify the validity period and other caching options to suit your IdP + +- **openid_connect.js** - this is the JavaScript code for performing the authorization code exchange and nonce hashing + - No changes are required unless modifying the code exchange or validation process + +### Configuring the Key-Value Store + +The key-value store is used to maintain persistent storage for ID tokens and refresh tokens. The default configuration should be reviewed so that it suits the environment. This is part of the advanced configuration in **openid_connect_configuration.conf**. + +```nginx +keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h; +keyval_zone zone=oidc_access_tokens:1M state=conf.d/oidc_access_tokens.json timeout=1h; +keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h; +keyval_zone zone=oidc_pkce:128K timeout=90s; +``` + +Each of the `keyval_zone` parameters are described below. + +- **zone** - Specifies the name of the key-value store and how much memory to allocate for it. Each session will typically occupy 1-2KB, depending on the size of the tokens, so scale this value to exceed the number of unique users that may authenticate. + +- **state** (optional) - Specifies where all of the ID Tokens in the key-value store are saved, so that sessions will persist across restart or reboot of the NGINX host. The NGINX Plus user account, typically **nginx**, must have write permission to the directory where the state file is stored. Consider creating a dedicated directory for this purpose. + +- **timeout** - Expired tokens are removed from the key-value store after the `timeout` value. This should be set to value slightly longer than the JWT validity period. JWT validation occurs on each request, and will fail when the expiry date (`exp` claim) has elapsed. If JWTs are issued without an `exp` claim then set `timeout` to the desired session duration. If JWTs are issued with a range of validity periods then set `timeout` to exceed the longest period. + +- **sync** (optional) - If deployed in a cluster, the key-value store may be synchronized across all instances in the cluster, so that all instances are able to create and validate authenticated sessions. Each instance must be configured to participate in state sharing with the [zone_sync module](http://nginx.org/en/docs/stream/ngx_stream_zone_sync_module.html) and by adding the `sync` parameter to the `keyval_zone` directives above. + +## Session Management + +The [NGINX Plus API](http://nginx.org/en/docs/http/ngx_http_api_module.html) is enabled in **openid_connect.server_conf** so that sessions can be monitored. The API can also be used to manage the current set of active sessions. + +To query the current sessions in the key-value store: + +```shell +$ curl localhost:8010/api/6/http/keyvals/oidc_id_tokens +``` + +To delete a single session: + +```shell +$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/oidc_id_tokens +$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/oidc_access_tokens +$ curl -iX PATCH -d '{"":null}' localhost:8010/api/6/http/keyvals/refresh_tokens +``` + +To delete all sessions: + +```shell +$ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_id_tokens +$ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_access_tokens +$ curl -iX DELETE localhost:8010/api/6/http/keyvals/refresh_tokens +``` + +## Real time monitoring + +The **openid_connect.server_conf** file defines several [`status_zone`](http://nginx.org/en/docs/http/ngx_http_api_module.html#status_zone) directives to collect metrics about OpenID Connect activity and errors. Separate metrics counters are recorded for: + +- **OIDC start** - New sessions are counted here. See step 2 in Figure 2, above. Success is recorded as a 3xx response. + +- **OIDC code exchange** - Counters are incremented here when the browser returns to NGINX Plus after authentication. See steps 6-10 in Figure 2, above. Success is recorded as a 3xx response. + +- **OIDC logout** - Requests to the URI of either `/logout` or `/v2/logout` are counted here. Success is recorded as a 3xx response. + +- **OIDC error** - Counters are incremented here when errors in the code exchange process are actively detected. Typically there will be a corresponding error_log entry. + +To obtain the current set of metrics: + +```shell +$ curl localhost:8010/api/6/http/location_zones +``` + +In addition, the [NGINX Plus Dashboard](https://docs.nginx.com/nginx/admin-guide/monitoring/live-activity-monitoring/#dashboard) can be configured to visualize the monitoring metrics in a GUI. + +## Troubleshooting + +Any errors generated by the OpenID Connect flow are logged to the error log, `/var/log/nginx/error.log`. Check the contents of this file as it may include error responses received by the IdP. The level of detail recorded can be modified by adjusting the severity level of the `error_log` directive. + +- **400 error from IdP** + + - This is typically caused by incorrect configuration related to the client ID and client secret. + - Check the values of the `map…$oidc_client` and `map…$oidc_client_secret` variables against the IdP configuration. + +- **500 error from nginx after successful authentication** + - Check for `could not be resolved` and `empty JWK set while sending to client` messages in the error log. This is common when NGINX Plus cannot reach the IdP's `jwks_uri` endpoint. + - Check the `map…$oidc_jwt_keyfile` variable is correct. + - Check the `resolver` directive in **openid_connect.server_conf** is reachable from the NGINX Plus host. + - Check for `OIDC authorization code sent but token response is not JSON.` messages in the error log. This is common when NGINX Plus cannot decompress the IdP's response. Add the following configuration snippet to the `/_jwks_uri` and `/_token` locations to **openid_connect.server_conf**: + +```nginx + proxy_set_header Accept-Encoding "gzip"; +``` + +- **Authentication is successful but browser shows too many redirects** + + - This is typically because the JWT sent to the browser cannot be validated, resulting in 'authorization required' `401` response and starting the authentication process again. But the user is already authenticated so is redirected back to NGINX, hence the redirect loop. + - Avoid using `auth_jwt_require` directives in your configuration because this can also return a `401` which is indistinguishable from missing/expired JWT. + - Check the error log `/var/log/nginx/error.log` for JWT/JWK errors. + - Ensure that the JWK file (`map…$oidc_jwt_keyfile` variable) is correct and that the nginx user has permission to read it. + +- **Logged out but next request does not require authentication** + + - This is typically caused by the IdP issuing its own session cookie(s) to the client. NGINX Plus sends the request to the IdP for authentication and the IdP immediately sends back a new authorization code because the session is still valid. + - Check your IdP configuration if this behavior is not desired. + +- **Failed SSL/TLS handshake to IdP** + - Indicated by error log messages including `peer closed connection in SSL handshake (104: Connection reset by peer) while SSL handshaking to upstream`. + - This can occur when the IdP requires Server Name Indication (SNI) information as part of the TLS handshake. Additional configuration is required to satisfy this requirement. + - Edit **openid_connect.server_conf** and for each of the `/_jwks_uri`, `/_token`, and `/_refresh` locations, add the following configuration snippet: + +```nginx +proxy_set_header Host ; +proxy_ssl_name ; +```