Reference implementation of NGINX Plus as relying party for OpenID Connect authentication
This repository describes how to enable OpenID Connect integration for NGINX Plus. The solution depends on NGINX Plus components (auth_jwt module and key-value store) and as such is not suitable for open source NGINX.
flowchart BT
subgraph " "
direction LR
id1(User)==>|Request for app|id2
id2-. Unauthenticated .->id1
id2(NGINX+)-->|Authenticated|id3(Backend app)
end
subgraph IDP
id4(Authorization Server)
end
id1<-. User authenticates directly with IdP .->IDP
IDP<-. NGINX exchanges authorization code for ID token .->id2
style id1 fill:#fff,stroke:#444,stroke-width:3px,color:#222
style id3 fill:#fff,stroke:#444,stroke-width:3px,color:#222
style id2 fill:#009639,stroke:#215732,stroke-width:2px,color:#fff
style id4 fill:#666,stroke:#222,stroke-width:1px,color:#fff
Figure 1. High level components of an OpenID Connect environment
This implementation assumes the following environment:
With this environment, both the client and NGINX Plus communicate directly with the IdP at different stages during the initial authentication event.
sequenceDiagram
autonumber
actor User
participant Browser
participant IdP
participant NGINX Plus
participant Web App
User->>NGINX Plus: Requests protected resource
NGINX Plus->>Browser: Sends redirect to IdP for authentication
Browser->>IdP: Requests login page
User->>IdP: Provides authentication and consent
IdP->>Browser: Sends redirect w/ authZ code
Browser->>NGINX Plus: Redirected for code exchange
NGINX Plus->>IdP: Sends authZ code
IdP->>NGINX Plus: Sends ID(+refresh) token
NGINX Plus-->>NGINX Plus: Validates ID token, stores in keyval, creates session cookie
Note right of NGINX Plus: keyvals zone for ID token (JWT)
Note right of NGINX Plus: keyval zone for refresh token
NGINX Plus->>Browser: Sends redirect to original URI with session cookie
Browser->>NGINX Plus: Requests original URI, supplies session cookie
NGINX Plus-->>NGINX Plus: Obtains ID token from keyval, validates JWT
NGINX Plus->>Web App: Proxies request
Web App->>Browser: Sends resource
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. 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.
When configuring NGINX Plus as an OpenID Connect client, it supports multiple client authentication methods:
client_id
and client_secret
are sent in the Authorization header as a Base64-encoded string.client_id
and client_secret
are sent in the body of the POST request.code_verifier
is used instead of a client_secret
.Access tokens are used in token-based authentication to allow OIDC client to access a protected resource on behalf of the user. NGINX Plus receives an access token after a user successfully authenticates and authorizes access, and then stores it in the key-value store. NGINX Plus can pass that token on the HTTP Authorization header as a Bearer token for every request that is sent to the downstream application.
Note: NGINX Plus does not verify the validity of the access token on each request, as we do with the ID token, so we cannot know if the access token has already expired or not. So, if access token lifetime is less than the ID token lifetime, you have to use the
proxy_intercept_errors on
directive, which will intercept and redirect401 Unauthorized
responses to NGINX in order to refresh the access token.
If a refresh token 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.
Requests made to the /logout
location invalidate both the ID token, access 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.
RP-initiated logout is supported according to OpenID Connect RP-Initiated Logout 1.0. This behavior is controlled by the $oidc_end_session_endpoint
variable.
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.
Start by installing NGINX Plus. In addition, the NGINX JavaScript module (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.
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
The GitHub repository contains 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.
$ nginx -p /path/to/conf -c /path/to/conf/nginx.conf
This implementation is suitable for running in a container provided that the base image 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.
$ 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;'
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.
Create an OpenID Connect client to represent your NGINX Plus instance
/_codexch
as the path, e.g. https://my-nginx.example.com:443/_codexch
client ID
and client secret
if set/_logout
as the path, e.g. https://my-nginx.example.com:443/_logout
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:
jwks_uri
or download the JWK file to your NGINX Plus instanceConfiguration 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
map…$oidc_
blocks to match your IdP configurationmap…$oidc_logout_redirect
to specify an unprotected resource to be displayed after requesting the /logout
location$oidc_hmac_key
to ensure nonce values are unpredictablemap…$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
error_log
directive to suit the deployment environmentauth_jwt_key_file
or auth_jwt_key_request
directives based on whether $oidc_jwt_keyfile
is a file or URI, respectivelyproxy_set_header Authorization "Bearer $access_token"
directive if you want to pass access/bearer token in HTTP header to the protected backend/upstreamproxy_intercept_errors on
directive if the access token lifetime is less than the ID token lifetimeopenid_connect.server_conf - this is the NGINX configuration for handling the various stages of OpenID Connect authorization code flow
resolver
directive to match a DNS server that is capable of resolving the IdP defined in $oidc_token_endpoint
and $oidc_end_session_endpoint
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 IdPopenid_connect.js - this is the JavaScript code for performing the authorization code exchange and nonce hashing
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.
keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
keyval_zone zone=refresh_tokens:1M state=/var/lib/nginx/state/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 and by adding the sync
parameter to the keyval_zone
directives above. If deployed in NGINXaaS for Azure, sync is a must configuration. Check NGINXaaS OIDC for more details.
The NGINX Plus API 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:
$ curl localhost:8010/api/6/http/keyvals/oidc_id_tokens
To delete a single session:
$ curl -iX PATCH -d '{"<session ID>":null}' localhost:8010/api/6/http/keyvals/oidc_id_tokens
$ curl -iX PATCH -d '{"<session ID>":null}' localhost:8010/api/6/http/keyvals/oidc_access_tokens
$ curl -iX PATCH -d '{"<session ID>":null}' localhost:8010/api/6/http/keyvals/refresh_tokens
To delete all sessions:
$ 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
The openid_connect.server_conf file defines several 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 /logout URI 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:
$ curl localhost:8010/api/6/http/location_zones
In addition, the NGINX Plus Dashboard can be configured to visualize the monitoring metrics in a GUI.
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
map…$oidc_client
and map…$oidc_client_secret
variables against the IdP configuration.500 error from nginx after successful authentication
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.map…$oidc_jwt_keyfile
variable is correct.resolver
directive in openid_connect.server_conf is reachable from the NGINX Plus host.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:
proxy_set_header Accept-Encoding "gzip";
Authentication is successful but browser shows too many redirects
401
response and starting the authentication process again. But the user is already authenticated so is redirected back to NGINX, hence the redirect loop.auth_jwt_require
directives in your configuration because this can also return a 401
which is indistinguishable from missing/expired JWT./var/log/nginx/error.log
for JWT/JWK errors.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
Failed SSL/TLS handshake to IdP
peer closed connection in SSL handshake (104: Connection reset by peer) while SSL handshaking to upstream
./_jwks_uri
, /_token
, and /_refresh
locations, add the following configuration snippet:
proxy_set_header Host <IdP hostname>;
proxy_ssl_name <IdP hostname>;
Invalid access token
Users may receive a 401
response with an optional "Invalid token" message despite successful authentication. There are several reasons why an OIDC access token might not be accepted by the upstream server, even if it has not expired:
Note: The scope of an OIDC access token is independent of its validity. Even if an OIDC access token is not expired and has not been revoked, it may still be considered invalid if it does not have the necessary scope for the requested action. Please check the
$oidc_scopes
variable in theopenid_connect_configuration.conf
file.
This reference implementation for OpenID Connect is supported for NGINX Plus subscribers.
/logout
location.js_import
. Container-friendly logging. Additional metrics for OIDC activity.client_secret_basic
client authentication method.