diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496212e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.crt +*.key +*.jwt +refresh_tokens.json +oidc*.json +oidc_credentials.conf +.DS_Store +docker/build-context/data diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..57bc69b --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: start ps watch down stop clean + +start: + docker-compose up -d + +ps: + docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Names}}" + +watch: + watch 'docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Names}}"' + +down: + docker-compose down + +stop: + docker-compose down + +clean: + docker kill $$(docker ps -q) 2> /dev/null || true + docker system prune -a + docker volume rm $(docker volume ls -qf dangling=true) diff --git a/README.md b/README.md index 2b3b006..52d2849 100644 --- a/README.md +++ b/README.md @@ -2,237 +2,113 @@ Reference implementation of NGINX Plus as relying party for OpenID Connect authentication -## 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). +- [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) +- [Documentation](#documentation) +- [Support](#support) -OpenID Connect components + -`Figure 1. High level components of an OpenID Connect environment` +## Overview -This implementation assumes the following environment: +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). - * 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 +> **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). -With this environment, both the client and NGINX Plus communicate directly with the IdP at different stages during the initial authentication event. + 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. -![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` +![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") -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. +### Requirements -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. +This solution requires the following: -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. +- 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. -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/). +## Documentation -### Refresh Tokens +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: -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. +- [Quick Start Guide](/docs/01-oidc-local-test.md) - 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 -### Logout +## Authorization Code Grant Flow -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. +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. -### Multiple IdPs +![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") -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:** The [openid_connect.server_conf](openid_connect.server_conf) configuration file sets up this authorization flow. -> **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. +When a client requests access to a protected resource for the first time, 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). -## Installation +NGINX Plus then communicates with the IdP to exchange the authorization code for a set of authentication tokens. -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: +> **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. + +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. -`$ sudo apt install nginx-plus-module-njs` for Debian/Ubuntu +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. -`$ sudo yum install nginx-plus-module-njs` for CentOS/RHEL +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. -The njs module needs to be loaded by adding the following configuration directive near the top of **nginx.conf**. +## OpenID Connect UserInfo Endpoint -```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** - -## 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 - * 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 - - * **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 [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. -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**. +- 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). -```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; -``` - -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. +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. -## 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/refresh_tokens +# +# 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; +} ``` -To delete all sessions: +## Logout Behavior -```shell -$ curl -iX DELETE localhost:8010/api/6/http/keyvals/oidc_id_tokens -$ curl -iX DELETE localhost:8010/api/6/http/keyvals/refresh_tokens -``` +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. -## Real time monitoring +> **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. -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: +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. - * **OIDC start** - New sessions are counted here. See step 2 in Figure 2, above. Success is recorded as a 3xx response. +The examples in this repository use the `/v1/_logout` for IdP configurations. You can change this to use `/v2/logout` according to your needs. - * **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. +> Note: Support for the `/v2/logout` endpoint was introduced in NGINX Plus R29. - * **OIDC logout** - Requests to the /logout URI are counted here. Success is recorded as a 3xx response. +## Using NGINX Plus with Multiple IdPs - * **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. +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). - 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 ; -``` +> **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/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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a47c579 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3.4' + +networks: + mynetwork: + name: mynetwork + attachable: true + +services: + + postgres: + container_name: idp-keycloak-db + image: postgres:12.0 + # volumes: + # - type: bind + # source: ./docker/build-context/data + # target: /var/lib/postgresql/data + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + networks: + - mynetwork + + keycloak: + container_name: idp-keycloak + image: jboss/keycloak:15.1.0 + environment: + DB_VENDOR: POSTGRES + DB_ADDR: postgres + DB_DATABASE: keycloak + DB_USER: keycloak + DB_SCHEMA: public + DB_PASSWORD: password + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: password + # Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it. + #JDBC_PARAMS: "ssl=true" + ports: + - 8080:8080 + depends_on: + - postgres + networks: + - mynetwork + + nginxplus_oidc_keycloak_ubuntu18.04: + container_name: nginxplus-oidc-keycloak + build: + context: ./ + dockerfile: ./docker/docker-files/nginxplus-ubuntu18.04/Dockerfile + image: nginxplus_oidc_keycloak_ubuntu18.04 + ports: + - 8010:8010 # Frontend/backend example v1: landing page w/ OIDC flow w/o login button + - 8020:8020 # Frontend/backend example v2: landing page w/ login button, login button w/ OIDC flow, logout button, /userinfo, access token based API authorization + volumes: + - type: bind + source: ./ + target: /etc/nginx/conf.d/ + - type: bind + source: ./docker/build-context/nginx/sample/ + target: /etc/nginx/sample/ + - type: bind + source: ./docker/build-context/content + target: /usr/share/nginx/html/ + depends_on: + - keycloak + networks: + - mynetwork diff --git a/docker/build-context/content/50x.html b/docker/build-context/content/50x.html new file mode 100644 index 0000000..9071e0a --- /dev/null +++ b/docker/build-context/content/50x.html @@ -0,0 +1,21 @@ + + + +Error + + + +

An error occurred.

+

Sorry, the page you are looking for is currently unavailable.
+Please try again later.

+

If you are the system administrator of this resource then you should check +the error log for details.

+

Faithfully yours, nginx.

+ + diff --git a/docker/build-context/content/css/global.css b/docker/build-context/content/css/global.css new file mode 100644 index 0000000..68b474f --- /dev/null +++ b/docker/build-context/content/css/global.css @@ -0,0 +1,572 @@ +/* Variables */ +:root { + --gray-offset: rgba(0, 0, 0, 0.03); + --gray-border: rgba(0, 0, 0, 0.15); + --gray-light: rgba(0, 0, 0, 0.4); + --gray-mid: rgba(0, 0, 0, 0.7); + --gray-dark: rgba(0, 0, 0, 0.9); + --body-color: var(--gray-mid); + --headline-color: var(--gray-dark); + --accent-color: #0066f0; + --body-font-family: -apple-system, BlinkMacSystemFont, sans-serif; + --radius: 6px; + --form-width: 600px; +} + +/* Base */ +* { + box-sizing: border-box; +} +body { + font-family: var(--body-font-family); + font-size: 16px; + color: var(--body-color); + -webkit-font-smoothing: antialiased; +} +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--body-color); + margin-top: 2px; + margin-bottom: 4px; +} +h1 { + font-size: 27px; + color: var(--headline-color); +} +h4 { + font-weight: 500; + font-size: 14px; + color: var(--gray-light); +} + +/* Layout */ +.sr-root { + display: flex; + flex-direction: row; + width: 100%; + max-width: 980px; + padding: 24px; + align-content: center; + justify-content: center; + height: auto; + min-height: 100vh; + margin: 0 auto; +} +.sr-header { + margin-bottom: 32px; +} +.sr-summary { + margin-bottom: 20px; +} +.sr-main, +.sr-content { + display: flex; + justify-content: center; + height: 100%; + align-self: center; +} +.sr-main { + width: var(--form-width); +} +.sr-content { + padding-left: 48px; +} +.sr-header__logo { + height: 24px; + background-size: contain; + background-repeat: no-repeat; + width: 100%; +} +.sr-legal-text { + color: var(--gray-light); + text-align: center; + font-size: 13px; + line-height: 17px; + margin-top: 12px; +} +.sr-field-error { + color: var(--accent-color); + text-align: left; + font-size: 13px; + line-height: 17px; + margin-top: 12px; +} + +/* Form */ +.sr-form-row { + margin: 16px 0; +} +label { + font-size: 13px; + font-weight: 500; + margin-bottom: 8px; + display: inline-block; +} + +/* Inputs */ +.sr-input, +.sr-select, +input[type="text"], +input[type="number"] { + border: 1px solid var(--gray-border); + border-radius: var(--radius); + padding: 5px 12px; + height: 44px; + width: 100%; + transition: box-shadow 0.2s ease; + background: white; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + color: #32325d; +} +.sr-input:focus, +input[type="text"]:focus, +button:focus, +.focused { + box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), + 0 0 0 4px rgba(50, 151, 211, 0.3); + outline: none; + z-index: 9; +} +.sr-input::placeholder, +input[type="text"]::placeholder, +input[type="number"]::placeholder { + color: var(--gray-light); +} + +/* Checkbox */ +.sr-checkbox-label { + position: relative; + cursor: pointer; +} + +.sr-checkbox-label input { + opacity: 0; + margin-right: 6px; +} + +.sr-checkbox-label .sr-checkbox-check { + position: absolute; + left: 0; + height: 16px; + width: 16px; + background-color: white; + border: 1px solid var(--gray-border); + border-radius: 4px; + transition: all 0.2s ease; +} + +.sr-checkbox-label input:focus ~ .sr-checkbox-check { + box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), + 0 0 0 4px rgba(50, 151, 211, 0.3); + outline: none; +} + +.sr-checkbox-label input:checked ~ .sr-checkbox-check { + background-color: var(--accent-color); + background-image: url("https://storage.googleapis.com/stripe-sample-images/icon-checkmark.svg"); + background-repeat: no-repeat; + background-size: 16px; + background-position: -1px -1px; +} + +/* Select */ +.sr-select { + display: block; + height: 44px; + margin: 0; + background-image: url("https://storage.googleapis.com/stripe-sample-images/icon-chevron-down.svg"); + background-repeat: no-repeat, repeat; + background-position: right 12px top 50%, 0 0; + background-size: 0.65em auto, 100%; +} +.sr-select:after { +} +.sr-select::-ms-expand { + display: none; +} +.sr-select:hover { + cursor: pointer; +} +.sr-select:focus { + box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), + 0 0 0 4px rgba(50, 151, 211, 0.3); + outline: none; +} +.sr-select option { + font-weight: 400; +} +.sr-select:invalid { + color: var(--gray-light); + background-opacity: 0.4; +} + +/* Combo inputs */ +.sr-combo-inputs { + display: flex; + flex-direction: column; +} +.sr-combo-inputs input, +.sr-combo-inputs .sr-select { + border-radius: 0; + border-bottom: 0; +} +.sr-combo-inputs > input:first-child, +.sr-combo-inputs > .sr-select:first-child { + border-radius: var(--radius) var(--radius) 0 0; +} +.sr-combo-inputs > input:last-child, +.sr-combo-inputs > .sr-select:last-child { + border-radius: 0 0 var(--radius) var(--radius); + border-bottom: 1px solid var(--gray-border); +} +.sr-combo-inputs > .sr-combo-inputs-row:last-child input:first-child { + border-radius: 0 0 0 var(--radius); + border-bottom: 1px solid var(--gray-border); +} +.sr-combo-inputs > .sr-combo-inputs-row:last-child input:last-child { + border-radius: 0 0 var(--radius) 0; + border-bottom: 1px solid var(--gray-border); +} +.sr-combo-inputs > .sr-combo-inputs-row:first-child input:first-child { + border-radius: var(--radius) 0 0 0; +} +.sr-combo-inputs > .sr-combo-inputs-row:first-child input:last-child { + border-radius: 0 var(--radius) 0 0; +} +.sr-combo-inputs > .sr-combo-inputs-row:first-child input:only-child { + border-radius: var(--radius) var(--radius) 0 0; +} +.sr-combo-inputs-row { + width: 100%; + display: flex; +} + +.sr-combo-inputs-row > input { + width: 100%; + border-radius: 0; +} + +.sr-combo-inputs-row > input:first-child:not(:only-child) { + border-right: 0; +} + +.sr-combo-inputs-row:not(:first-of-type) .sr-input { + border-radius: 0 0 var(--radius) var(--radius); +} + +/* Buttons and links */ +button { + background: var(--accent-color); + border-radius: var(--radius); + color: white; + border: 0; + padding: 12px 16px; + margin-top: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: block; +} +button:hover { + filter: contrast(115%); +} +button:active { + transform: translateY(0px) scale(0.98); + filter: brightness(0.9); +} +button:disabled { + opacity: 0.5; + cursor: none; +} + +.sr-payment-form button, +.fullwidth { + width: 100%; +} + +a { + color: var(--accent-color); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + filter: brightness(0.8); +} + +a:active { + filter: brightness(0.5); +} + +/* Code block */ +.sr-callout { + background: var(--gray-offset); + padding: 12px; + border-radius: var(--radius); + max-height: 400px; + overflow: auto; +} +code, +pre { + font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace; + font-size: 12px; +} + +/* Stripe Element placeholder */ +.sr-card-element { + padding-top: 12px; +} + +/* Responsiveness */ +@media (max-width: 720px) { + .sr-root { + flex-direction: column; + justify-content: flex-start; + padding: 48px 20px; + min-width: 320px; + } + + .sr-header__logo { + background-position: center; + } + + .sr-summary { + text-align: center; + } + + .sr-content { + display: none; + } + + .sr-main { + width: 100%; + } +} + +/* Pasha styles – Brand-overrides, can split these out */ +:root { + --accent-color: #008000; + --headline-color: var(--accent-color); +} + +.pasha-image-stack { + display: grid; + grid-gap: 12px; + grid-template-columns: auto auto; +} + +.pasha-image-stack img, +.pasha-image img { + border-radius: var(--radius); + background-color: var(--gray-border); + box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), + 0 3px 6px 0 rgba(0, 0, 0, 0.07); + transition: all 0.8s ease; + opacity: 0; +} + +.pasha-image { + text-align: center; + margin-top: 20px; +} + +.pasha-image img { + opacity: 1; +} + +.pasha-image-stack img:nth-child(1) { + transform: translate(30px, 15px); + opacity: 1; +} +.pasha-image-stack img:nth-child(2) { + transform: translate(-28px, 0px); + opacity: 1; +} +.pasha-image-stack img:nth-child(3) { + transform: translate(64px, -50px); + opacity: 1; +} + +/* todo: spinner/processing state, errors, animations */ + +.spinner, +.spinner:before, +.spinner:after { + border-radius: 50%; +} +.spinner { + color: #ffffff; + font-size: 22px; + text-indent: -99999px; + margin: 0px auto; + position: relative; + width: 20px; + height: 20px; + box-shadow: inset 0 0 0 2px; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); +} +.spinner:before, +.spinner:after { + position: absolute; + content: ""; +} +.spinner:before { + width: 10.4px; + height: 20.4px; + background: var(--accent-color); + border-radius: 20.4px 0 0 20.4px; + top: -0.2px; + left: -0.2px; + -webkit-transform-origin: 10.4px 10.2px; + transform-origin: 10.4px 10.2px; + -webkit-animation: loading 2s infinite ease 1.5s; + animation: loading 2s infinite ease 1.5s; +} +.spinner:after { + width: 10.4px; + height: 10.2px; + background: var(--accent-color); + border-radius: 0 10.2px 10.2px 0; + top: -0.1px; + left: 10.2px; + -webkit-transform-origin: 0px 10.2px; + transform-origin: 0px 10.2px; + -webkit-animation: loading 2s infinite ease; + animation: loading 2s infinite ease; +} +@-webkit-keyframes loading { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes loading { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +/* Custom */ +.container { + border: 1px solid #e8e8e8; + border-radius: 6px; + padding: 32px 28px; + display: flex; + flex-direction: column; + justify-content: space-between; + max-width: 400px; +} + +.container .pasha-image-stack { + margin-top: 20px; +} + +.container:first-of-type { + margin-right: 20px; +} + +.quantity-setter { + display: flex; + justify-content: center; + margin-top: 20px; +} + +.quantity-setter input { + border-radius: 0; + width: 50px; + padding: 0; + text-align: center; +} + +.quantity-setter .increment-btn { + margin-top: 0; + border-radius: 0; + border: 1px solid var(--accent-color); +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Animated form */ + +.sr-root { + animation: 0.4s form-in; + animation-fill-mode: both; + animation-timing-function: ease; +} + +.sr-payment-form .sr-form-row { + animation: 0.4s field-in; + animation-fill-mode: both; + animation-timing-function: ease; + transform-origin: 50% 0%; +} + +/* need saas for loop :D */ +.sr-payment-form .sr-form-row:nth-child(1) { + animation-delay: 0; +} +.sr-payment-form .sr-form-row:nth-child(2) { + animation-delay: 60ms; +} +.sr-payment-form .sr-form-row:nth-child(3) { + animation-delay: 120ms; +} +.sr-payment-form .sr-form-row:nth-child(4) { + animation-delay: 180ms; +} +.sr-payment-form .sr-form-row:nth-child(5) { + animation-delay: 240ms; +} +.sr-payment-form .sr-form-row:nth-child(6) { + animation-delay: 300ms; +} +.hidden { + display: none; +} + +@keyframes field-in { + 0% { + opacity: 0; + transform: translateY(8px) scale(0.95); + } + 100% { + opacity: 1; + transform: translateY(0px) scale(1); + } +} + +@keyframes form-in { + 0% { + opacity: 0; + transform: scale(0.98); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +#submit { + width: 100%; +} diff --git a/docker/build-context/content/css/json-viewer.css b/docker/build-context/content/css/json-viewer.css new file mode 100644 index 0000000..f5f2074 --- /dev/null +++ b/docker/build-context/content/css/json-viewer.css @@ -0,0 +1,69 @@ +.json-viewer { + color: #000; + padding-left: 20px; +} + +.json-viewer ul { + list-style-type: none; + margin: 0; + margin: 0 0 0 1px; + border-left: 1px dotted #ccc; + padding-left: 2em; +} + +.json-viewer .hide { + display: none; +} + +.json-viewer .type-string { + color: #0B7500; +} + +.json-viewer .type-date { + color: #CB7500; +} + +.json-viewer .type-boolean { + color: #1A01CC; + font-weight: bold; +} + +.json-viewer .type-number { + color: #1A01CC; +} + +.json-viewer .type-null, .json-viewer .type-undefined { + color: #90a; +} + +.json-viewer a.list-link { + color: #000; + text-decoration: none; + position: relative; +} + +.json-viewer a.list-link:before { + color: #aaa; + content: "\25BC"; + position: absolute; + display: inline-block; + width: 1em; + left: -1em; +} + +.json-viewer a.list-link.collapsed:before { + content: "\25B6"; +} + +.json-viewer a.list-link.empty:before { + content: ""; +} + +.json-viewer .items-ph { + color: #aaa; + padding: 0 1em; +} + +.json-viewer .items-ph:hover { + text-decoration: underline; +} diff --git a/docker/build-context/content/css/normalize.css b/docker/build-context/content/css/normalize.css new file mode 100644 index 0000000..53e2941 --- /dev/null +++ b/docker/build-context/content/css/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document +========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections +========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content +========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics +========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content +========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms +========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive +========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc +========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/docker/build-context/content/favicon-48x48.ico b/docker/build-context/content/favicon-48x48.ico new file mode 100644 index 0000000..70f79db Binary files /dev/null and b/docker/build-context/content/favicon-48x48.ico differ diff --git a/docker/build-context/content/favicon.ico b/docker/build-context/content/favicon.ico new file mode 100644 index 0000000..70f79db Binary files /dev/null and b/docker/build-context/content/favicon.ico differ diff --git a/docker/build-context/content/index.html b/docker/build-context/content/index.html new file mode 100644 index 0000000..bcaf293 --- /dev/null +++ b/docker/build-context/content/index.html @@ -0,0 +1,52 @@ + + + + + + + NGINX Plus OIDC + + + + + + + + + + +
+
+
+
+

NGINX Plus OIDC

+

Sample page for testing Open ID Connect.

+ +
+ +
+
+ +
+ + +
+

Enter an URI for testing an API w/ access-token:

+ + +
+
+
+

+            
+
+
+
+
+
+ + \ No newline at end of file diff --git a/docker/build-context/content/index.js b/docker/build-context/content/index.js new file mode 100644 index 0000000..e6838d1 --- /dev/null +++ b/docker/build-context/content/index.js @@ -0,0 +1,234 @@ +/** + * JavaScript functions for testing NGINX Plus OpenID Connect + * + * Copyright (C) 2022 Nginx, Inc. + */ + +// Constants for common error message. +var isSignedIn = false; +var TITLE_SIGNIN = 'Sign in'; +var TITLE_SIGNOUT = 'Sign out'; +var MSG_SIGNINIG_IN = 'Signinig in'; +var MSG_SIGNED_IN = 'Signed in'; +var MSG_SIGNED_OUT = 'Signed out'; +var MSG_EMPTY_JSON = '{"message": "N/A"}'; +var btnSignin = document.getElementById('signin'); +var btnProxiedAPI = document.getElementById('proxied-api'); +var jsonViewer = new JSONViewer(); +var viewerJSON = document.querySelector("#json").appendChild(jsonViewer.getContainer()); +var accessToken = ''; +var userName = ''; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 0. Initialize Main Page * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +var initButtons = function () { + if (isSignedIn) { + initButtonsAfterSignIn() + } else { + initButtonsBeforeSignIn() + } +} + + +var initButtonsBeforeSignIn = function () { + btnProxiedAPI.disabled = true + isSignedIn = false; + showLoginBtnTitle(TITLE_SIGNIN); +} + +var initButtonsAfterSignIn = function () { + btnProxiedAPI.disabled = false + isSignedIn = true; + showLoginBtnTitle(TITLE_SIGNOUT); +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 1. Event Handler for testing NGINX Plus OIDC * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +// Event Handler: for when clicking a either 'Sign in' or `Sign out` button. +var eventHandlerSignIn = function (evt) { + if (!isSignedIn) { + doSignIn(evt) + } else { + doSignOut(evt) + } +}; + +// Event Handler: for when clicking a 'Backend API w/ Cookie + Bearer' button. +// - /v1/api/2: cookie is used. The bearer access token is also passed to the +// backend API via `proxy_set_header Authorization` directive. +var eventHandlerProxiedAPI = function (evt) { + var headers = {}; + doAPIRequest( + evt, + getSampleURI(), + 'calling a sample proxied API...', + 'called a sample proxied API', + headers + ) +}; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 2. Common Functions for testing OIDC Workflows via Sample UI * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +// Request NGINX endpoint which is a location block. +var doNginxEndpointRequest = function(evt, uri) { + if (evt && evt.type === 'keypress' && evt.keyCode !== 13) { + return; + } + location.href = window.location.origin + uri; +}; + +var eraseCookie = function(name) { + document.cookie = name+'=; Path=/; SameSite=lax;'; +} + +var setCookie = function(name, value) { + document.cookie = name+'='+value+'; Path=/; SameSite=lax;'; +} + +// Sign in by clicking 'Sign In' button of the UI. +var doSignIn = function(evt) { + eraseCookie('session_id') + eraseCookie('auth_redir') + eraseCookie('auth_nonce') + + doNginxEndpointRequest(evt, '/login'); +}; + +// Sign in by clicking 'Sign In' button of the UI. +var doSignOut = function(evt) { + doNginxEndpointRequest(evt, '/v2/logout'); +}; + +// Request an API with application/json type response. +var doAPIRequest = function(evt, uri, msgBefore, msgAfter, headers) { + if (evt && evt.type === 'keypress' && evt.keyCode !== 13) { + return false; + } + showMessage(msgBefore) + const url = window.location.origin + uri; + fetch(url, { + method : 'GET', + mode : 'cors', + headers: headers + }) + .then((response) => { + showResponseStatus(response.status, response.statusText, url) + showMessageDetail(MSG_EMPTY_JSON) + if (response.ok || response.status == 400) { + return response.json(); + } + throw new Error(response.error) + }) + .then((data) => { + showMessage(msgAfter) + showMessageDetail(JSON.stringify(data)) + if (uri == '/userinfo') { + initButtonsAfterSignIn() + } + if (data.username) { + userName = data.username; + showMessage(userName) + } else if (data.name) { + userName = data.name; + showMessage(userName) + } else if (data.email) { + userName = data.email; + showMessage(userName) + } + }) + .catch(function(error) { + if (uri == '/userinfo') { + initButtonsBeforeSignIn() + showMessage('Sign in to retrieve user information!'); + showMessageDetail(MSG_EMPTY_JSON) + } else { + showMessage(error); + showMessageDetail(MSG_EMPTY_JSON) + } + }); + return true; +} + +// Show user information in the UI via the endpoint of /userinfo +var showUserInfo = function(evt) { + var headers = {}; + doAPIRequest( + evt, + '/userinfo', + 'getting user info from IdP...', + 'user info: received from IdP', + headers + ); +} + +// Display summarized message for each testing. +var showMessage = function (msg) { + document.getElementById('message').value = msg; +}; + +// Display response status & message for each testing. +var showResponseStatus = function (status, msg, uri) { + document.querySelector('pre').textContent = uri + ', ' + status + ', ' + msg; +}; + +// Clear message window +var clearMessage = function() { + document.querySelector('pre').textContent = ''; + showMessageDetail(MSG_EMPTY_JSON); +}; + +// Display detail message for each testing. +var showMessageDetail = function (msg) { + var setJSON = function() { + try { + jsonObj = JSON.parse(msg); + } + catch (err) { + alert(err); + } + }; + setJSON(); + jsonViewer.showJSON(jsonObj); + var res = jsonObj; + return res +} + +// Display a button title for toggling between 'Sign in' and 'Sign out'. +var showLoginBtnTitle = function (msg) { + btnSignin.innerText = msg +}; + +// Display 'Sign In' button when signed-out or occurs error during signing-in. +var showSignInBtn = function () { + isSignedIn = false; + showLoginBtnTitle(TITLE_SIGNIN); + showMessage(MSG_SIGNED_OUT); +}; + +// Display 'Sign Out' button when signed-in. +var showSignOutBtn = function () { + isSignedIn = true; + showLoginBtnTitle(TITLE_SIGNOUT); + showMessage(MSG_SIGNED_IN); +}; + +var getSampleURI = function () { + return document.getElementById('sample-uri').value; +}; + +// Add event lister of each button for testing NGINX Plus OIDC integration. +btnSignin .addEventListener('click', eventHandlerSignIn); +btnProxiedAPI.addEventListener('click', eventHandlerProxiedAPI); + +showUserInfo(null) diff --git a/docker/build-context/content/json-viewer.js b/docker/build-context/content/json-viewer.js new file mode 100644 index 0000000..d8718cb --- /dev/null +++ b/docker/build-context/content/json-viewer.js @@ -0,0 +1,269 @@ +/** + * JSONViewer - by Roman Makudera 2016 (c) MIT licence. + */ +var JSONViewer = (function(document) { + var Object_prototype_toString = ({}).toString; + var DatePrototypeAsString = Object_prototype_toString.call(new Date); + + /** @constructor */ + function JSONViewer() { + this._dom_container = document.createElement("pre"); + this._dom_container.classList.add("json-viewer"); + }; + + /** + * Visualise JSON object. + * + * @param {Object|Array} json Input value + * @param {Number} [inputMaxLvl] Process only to max level, where 0..n, -1 unlimited + * @param {Number} [inputColAt] Collapse at level, where 0..n, -1 unlimited + */ + JSONViewer.prototype.showJSON = function(jsonValue, inputMaxLvl, inputColAt) { + // Process only to maxLvl, where 0..n, -1 unlimited + var maxLvl = typeof inputMaxLvl === "number" ? inputMaxLvl : -1; // max level + // Collapse at level colAt, where 0..n, -1 unlimited + var colAt = typeof inputColAt === "number" ? inputColAt : -1; // collapse at + + this._dom_container.innerHTML = ""; + walkJSONTree(this._dom_container, jsonValue, maxLvl, colAt, 0); + }; + + /** + * Get container with pre object - this container is used for visualise JSON data. + * + * @return {Element} + */ + JSONViewer.prototype.getContainer = function() { + return this._dom_container; + }; + + /** + * Recursive walk for input value. + * + * @param {Element} outputParent is the Element that will contain the new DOM + * @param {Object|Array} value Input value + * @param {Number} maxLvl Process only to max level, where 0..n, -1 unlimited + * @param {Number} colAt Collapse at level, where 0..n, -1 unlimited + * @param {Number} lvl Current level + */ + function walkJSONTree(outputParent, value, maxLvl, colAt, lvl) { + var isDate = Object_prototype_toString.call(value) === DatePrototypeAsString; + var realValue = !isDate && typeof value === "object" && value !== null && "toJSON" in value ? value.toJSON() : value; + if (typeof realValue === "object" && realValue !== null && !isDate) { + var isMaxLvl = maxLvl >= 0 && lvl >= maxLvl; + var isCollapse = colAt >= 0 && lvl >= colAt; + + var isArray = Array.isArray(realValue); + var items = isArray ? realValue : Object.keys(realValue); + + if (lvl === 0) { + // root level + var rootCount = _createItemsCount(items.length); + // hide/show + var rootLink = _createLink(isArray ? "[" : "{"); + + if (items.length) { + rootLink.addEventListener("click", function() { + if (isMaxLvl) return; + + rootLink.classList.toggle("collapsed"); + rootCount.classList.toggle("hide"); + + // main list + outputParent.querySelector("ul").classList.toggle("hide"); + }); + + if (isCollapse) { + rootLink.classList.add("collapsed"); + rootCount.classList.remove("hide"); + } + } + else { + rootLink.classList.add("empty"); + } + + rootLink.appendChild(rootCount); + outputParent.appendChild(rootLink); // output the rootLink + } + + if (items.length && !isMaxLvl) { + var len = items.length - 1; + var ulList = document.createElement("ul"); + ulList.setAttribute("data-level", lvl); + ulList.classList.add("type-" + (isArray ? "array" : "object")); + + items.forEach(function(key, ind) { + var item = isArray ? key : value[key]; + var li = document.createElement("li"); + + if (typeof item === "object") { + // null && date + if (!item || item instanceof Date) { + li.appendChild(document.createTextNode(isArray ? "" : key + ": ")); + li.appendChild(createSimpleViewOf(item ? item : null, true)); + } + // array & object + else { + var itemIsArray = Array.isArray(item); + var itemLen = itemIsArray ? item.length : Object.keys(item).length; + + // empty + if (!itemLen) { + li.appendChild(document.createTextNode(key + ": " + (itemIsArray ? "[]" : "{}"))); + } + else { + // 1+ items + var itemTitle = (typeof key === "string" ? key + ": " : "") + (itemIsArray ? "[" : "{"); + var itemLink = _createLink(itemTitle); + var itemsCount = _createItemsCount(itemLen); + + // maxLvl - only text, no link + if (maxLvl >= 0 && lvl + 1 >= maxLvl) { + li.appendChild(document.createTextNode(itemTitle)); + } + else { + itemLink.appendChild(itemsCount); + li.appendChild(itemLink); + } + + walkJSONTree(li, item, maxLvl, colAt, lvl + 1); + li.appendChild(document.createTextNode(itemIsArray ? "]" : "}")); + + var list = li.querySelector("ul"); + var itemLinkCb = function() { + itemLink.classList.toggle("collapsed"); + itemsCount.classList.toggle("hide"); + list.classList.toggle("hide"); + }; + + // hide/show + itemLink.addEventListener("click", itemLinkCb); + + // collapse lower level + if (colAt >= 0 && lvl + 1 >= colAt) { + itemLinkCb(); + } + } + } + } + // simple values + else { + // object keys with key: + if (!isArray) { + li.appendChild(document.createTextNode(key + ": ")); + } + + // recursive + walkJSONTree(li, item, maxLvl, colAt, lvl + 1); + } + + // add comma to the end + if (ind < len) { + li.appendChild(document.createTextNode(",")); + } + + ulList.appendChild(li); + }, this); + + outputParent.appendChild(ulList); // output ulList + } + else if (items.length && isMaxLvl) { + var itemsCount = _createItemsCount(items.length); + itemsCount.classList.remove("hide"); + + outputParent.appendChild(itemsCount); // output itemsCount + } + + if (lvl === 0) { + // empty root + if (!items.length) { + var itemsCount = _createItemsCount(0); + itemsCount.classList.remove("hide"); + + outputParent.appendChild(itemsCount); // output itemsCount + } + + // root cover + outputParent.appendChild(document.createTextNode(isArray ? "]" : "}")); + + // collapse + if (isCollapse) { + outputParent.querySelector("ul").classList.add("hide"); + } + } + } else { + // simple values + outputParent.appendChild( createSimpleViewOf(value, isDate) ); + } + }; + + /** + * Create simple value (no object|array). + * + * @param {Number|String|null|undefined|Date} value Input value + * @return {Element} + */ + function createSimpleViewOf(value, isDate) { + var spanEl = document.createElement("span"); + var type = typeof value; + var asText = "" + value; + + if (type === "string") { + asText = '"' + value + '"'; + } else if (value === null) { + type = "null"; + //asText = "null"; + } else if (isDate) { + type = "date"; + asText = value.toLocaleString(); + } + + spanEl.className = "type-" + type; + spanEl.textContent = asText; + + return spanEl; + }; + + /** + * Create items count element. + * + * @param {Number} count Items count + * @return {Element} + */ + function _createItemsCount(count) { + var itemsCount = document.createElement("span"); + itemsCount.className = "items-ph hide"; + itemsCount.innerHTML = _getItemsTitle(count); + + return itemsCount; + }; + + /** + * Create clickable link. + * + * @param {String} title Link title + * @return {Element} + */ + function _createLink(title) { + var linkEl = document.createElement("a"); + linkEl.classList.add("list-link"); + linkEl.href = "javascript:void(0)"; + linkEl.innerHTML = title || ""; + + return linkEl; + }; + + /** + * Get correct item|s title for count. + * + * @param {Number} count Items count + * @return {String} + */ + function _getItemsTitle(count) { + var itemsTxt = count > 1 || count === 0 ? "items" : "item"; + + return (count + " " + itemsTxt); + }; + + return JSONViewer; +})(document); diff --git a/docker/build-context/content/signout.html b/docker/build-context/content/signout.html new file mode 100644 index 0000000..29b016c --- /dev/null +++ b/docker/build-context/content/signout.html @@ -0,0 +1,58 @@ + + + + + + + NGINX Plus OIDC + + + + + + + +
+
+
+ +
+ +
+

Successfully signed-out

+
+
+
+

+          
+ +
+
+ +
+
+ + + + +
+
+
+ + diff --git a/docker/build-context/nginx/nginx.conf b/docker/build-context/nginx/nginx.conf new file mode 100644 index 0000000..3aab0ce --- /dev/null +++ b/docker/build-context/nginx/nginx.conf @@ -0,0 +1,31 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; +load_module modules/ngx_http_js_module.so; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + include conf.d/openid_connect_configuration.conf; + include conf.d/frontend.conf; + include sample/proxy_server_frontend.conf; + include sample/proxy_server_backend.conf; +} + +# vim: syntax=nginx \ No newline at end of file diff --git a/docker/build-context/nginx/sample/proxy_server_backend.conf b/docker/build-context/nginx/sample/proxy_server_backend.conf new file mode 100644 index 0000000..95e8e58 --- /dev/null +++ b/docker/build-context/nginx/sample/proxy_server_backend.conf @@ -0,0 +1,30 @@ +# -----------------------------------------------------------------------------# +# # +# Sample Configuration for Proxied Backend API Server # +# # +# -----------------------------------------------------------------------------# + +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; + # + + listen 9092; + server_name localhost; + + # + # Sample API endpoint for testing API authorization. + # + # - This internal API endpoint is proxied by 'frontend_backend_sample.conf'. + # - This is not called if an access token is invalid or expirerd based on + # the token validation with an IdP in the 'frontend_backend_sample.conf'. + # + location /v1/api/example { + default_type application/json; + return 200 '{"message": "sample API" }'; + } +} + +# vim: syntax=nginx \ No newline at end of file diff --git a/docker/build-context/nginx/sample/proxy_server_frontend.conf b/docker/build-context/nginx/sample/proxy_server_frontend.conf new file mode 100644 index 0000000..acf893e --- /dev/null +++ b/docker/build-context/nginx/sample/proxy_server_frontend.conf @@ -0,0 +1,41 @@ +# -----------------------------------------------------------------------------# +# # +# Sample Configuration for Proxied Frotend Site # +# # +# -----------------------------------------------------------------------------# + +server { + # + # Logging: 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; + # + + listen 9091; + server_name localhost; + + # + # Sample page for testing a landing page of proxied frontend site. + # + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + # + # Sample endpoint for redirecting to a custom logout page instead of root + # langding page after successful signing-out from the IdP when one of the + # following cases: + # + # 1) a variable of `postLogoutReturnURI` is configured in NMS - ACM. + # 2) a variable of `$post_logout_return_uri` is manually configured in the + # file of openid_connect_configuration.conf. + # + location /signout { + root /usr/share/nginx/html; + try_files $uri $uri/ /signout.html; + } +} + +# vim: syntax=nginx \ No newline at end of file diff --git a/docker/build-context/ssl/.gitkeep b/docker/build-context/ssl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker/docker-files/nginxplus-ubuntu18.04/Dockerfile b/docker/docker-files/nginxplus-ubuntu18.04/Dockerfile new file mode 100644 index 0000000..52945e0 --- /dev/null +++ b/docker/docker-files/nginxplus-ubuntu18.04/Dockerfile @@ -0,0 +1,206 @@ +FROM ubuntu:18.04 + +LABEL maintainer="NGINX Docker Maintainers " + +# Define NGINX versions for NGINX Plus and NGINX Plus modules +# Uncomment this block and the versioned nginxPackages in the main RUN +# instruction to install a specific release +# https://docs.nginx.com/nginx/releases/ +ENV NGINX_VERSION 25 +# https://nginx.org/en/docs/njs/changes.html +ENV NJS_VERSION 0.6.2 +# https://plus-pkgs.nginx.com 1~bionic +ENV PKG_RELEASE 1~bionic + +## Install Nginx Plus + +# Download certificate and key from the customer portal https://account.f5.com/myf5 +# and copy to the build context and set correct permissions +RUN mkdir -p /etc/ssl/nginx +COPY docker/build-context/ssl/*.crt /etc/ssl/nginx/ +COPY docker/build-context/ssl/*.key /etc/ssl/nginx/ + +RUN set -x \ + && chmod 644 /etc/ssl/nginx/* \ + # Create nginx user/group first, to be consistent throughout Docker variants + && addgroup --system --gid 1001 nginx \ + && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 1001 nginx \ + # Install prerequisite packages, vim for editing, then Install NGINX Plus + && apt-get update && apt-get upgrade -y \ + && DEBIAN_FRONTEND=noninteractive apt-get -qq -y install --no-install-recommends apt-transport-https lsb-release ca-certificates wget dnsutils gnupg vim-tiny apt-utils jq \ + # Install NGINX Plus from repo (https://cs.nginx.com/repo_setup) + && wget http://nginx.org/keys/nginx_signing.key && apt-key add nginx_signing.key \ + && printf "deb https://plus-pkgs.nginx.com/ubuntu `lsb_release -cs` nginx-plus\n" | tee /etc/apt/sources.list.d/nginx-plus.list \ + && wget -P /etc/apt/apt.conf.d https://cs.nginx.com/static/files/90nginx \ + && apt-get update \ + # + ## Install the latest release of NGINX Plus and/or NGINX Plus modules + ## Optionally use versioned packages over defaults to specify a release + # List available versions: + && apt-cache policy nginx-plus \ + ## Uncomment one: + # && DEBIAN_FRONTEND=noninteractive apt-get -qq -y install --no-install-recommends nginx-plus \ + && DEBIAN_FRONTEND=noninteractive apt-get -qq -y install --no-install-recommends nginx-plus=${NGINX_VERSION}-${PKG_RELEASE} \ + # + ## Optional: Install NGINX Plus Dynamic Modules (3rd-party) from repo + ## See https://www.nginx.com/products/nginx/modules + ## Some modules include debug binaries, install module ending with "-dbg" + ## Uncomment one: + ## njs dynamic modules + #nginx-plus-module-njs \ + #nginx-plus-module-dbg \ + nginx-plus-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \ + nginx-plus-module-njs-dbg=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \ + ## NGINX high Availablity keepalived + #nginx-ha-keepalived \ + ## NGINX agent for New Relic \ + #nginx-nr-agent \ + ## SPNEGO for Kerberos authentication + #nginx-plus-module-auth-spnego + #nginx-plus-module-auth-spnego-dbg + #nginx-plus-module-auth-spnego=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} + #nginx-plus-module-auth-spnego-dbg=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} + ## brotli compression dynamic modules + #nginx-plus-module-brotli \ + #nginx-plus-module-brotli-dbg \ + #nginx-plus-module-brotli=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-brotli-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## cookie flag dynamic module + #nginx-plus-module-cookie-flag \ + #nginx-plus-module-cookie-flag-dbg + #nginx-plus-module-cookie-flag=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-cookie-flag-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## Encrypted-Session dynamic module + #nginx-plus-module-encrypted-session \ + #nginx-plus-module-encrypted-session=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-encrypted-session-dbg \ + #nginx-plus-module-encrypted-session-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## FIPS Check + #nginx-plus-module-fips-check \ + #nginx-plus-module-fips-check-dbg \ + #nginx-plus-module-fips-check=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-fips-check-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## GeoIP dynamic modules + #nginx-plus-module-geoip \ + #nginx-plus-module-geoip-dbg \ + #nginx-plus-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-geoip-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## GeoIP2 dynamic modules + #nginx-plus-module-geoip2 \ + #nginx-plus-module-geoip2-dbg \ + #nginx-plus-module-geoip2=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-geoip2-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## headers-more dynamic module + #nginx-plus-module-headers-more \ + #nginx-plus-module-headers-more-dbg \ + #nginx-plus-module-headers-more=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-headers-more-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## image filter dynamic module + #nginx-plus-module-image-filter \ + #nginx-plus-module-image-filter-dbg \ + #nginx-plus-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-image-filter-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## Lua dynamic module + #nginx-plus-module-lua \ + #nginx-plus-module-lua-dbg \ + #nginx-plus-module-lua=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-lua-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## ModSecurity dynamic module + #nginx-plus-module-modsecurity \ + #nginx-plus-module-modsecurity-dbg \ + #nginx-plus-module-modsecurity=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-modsecurity-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## Nginx Development Kit dynamic module + #nginx-plus-module-ndk \ + #nginx-plus-module-ndk-dbg \ + #nginx-plus-module-ndk=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-ndk-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## OpenTracing dynamic module + #nginx-plus-module-opentracing \ + #nginx-plus-module-opentracing-dbg \ + #nginx-plus-module-opentracing=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-opentracing-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## Phusion Passenger Open Source dynamic module + #nginx-plus-module-passenger \ + #nginx-plus-module-passenger-dbg \ + #nginx-plus-module-passenger=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-passenger-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## Perl dynamic module + #nginx-plus-module-perl \ + #nginx-plus-module-perl-dbg \ + #nginx-plus-module-perl=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-perl-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## Prometheus exporter NJS module + #nginx-plus-module-prometheus \ + #nginx-plus-module-prometheus=${NGINX_VERSION}-${PKG_RELEASE} \ + ## RTMP dynamic module + #nginx-plus-module-rtmp \ + #nginx-plus-module-rtmp-dbg \ + #nginx-plus-module-rtmp=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-rtmp-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## set-misc dynamic module + #nginx-plus-module-set-misc \ + #nginx-plus-module-set-misc-dbg \ + #nginx-plus-module-set-misc=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-set-misc-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## HTTP Substitutions Filter dynamic module + #nginx-plus-module-subs-filter \ + #nginx-plus-module-subs-filter-dbg \ + #nginx-plus-module-subs-filter=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-subs-filter-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## xslt dynamic module + #nginx-plus-module-xslt \ + #nginx-plus-module-xslt-dbg \ + #nginx-plus-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \ + #nginx-plus-module-xslt-dbg=${NGINX_VERSION}-${PKG_RELEASE} \ + ## NGINX Sync Script nginx-sync.sh + #nginx-sync \ + # Remove default nginx config + && rm /etc/nginx/conf.d/default.conf \ + # Optional: Create cache folder and set permissions for proxy caching + && mkdir -p /var/cache/nginx \ + && chown -R nginx /var/cache/nginx \ + # Optional: Create State file folder and set permissions + && mkdir -p /var/lib/nginx/state \ + && chown -R nginx /var/lib/nginx/state \ + # Set permissions + && chown -R nginx:nginx /etc/nginx \ + # Forward request and error logs to docker log collector + && ln -sf /dev/stdout /var/log/nginx/access.log \ + && ln -sf /dev/stderr /var/log/nginx/error.log \ + # Raise the limits to successfully run benchmarks + && ulimit -c -m -s -t unlimited \ + # Cleanup + && apt-get remove --purge --auto-remove -y gnupg lsb-release apt-utils \ + && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx-plus.list \ + && rm -rf /etc/apt/apt.conf.d/90nginx \ + && rm -rf nginx_signing.key +# Remove the cert/keys from the image +# && rm /etc/ssl/nginx/nginx-repo.crt /etc/ssl/nginx/nginx-repo.key + +# Optional: COPY over any of your SSL certs for HTTPS servers +# For example, +#COPY etc/ssl/my-nginx.example.com.crt /etc/ssl/my-nginx.example.com.crt +#COPY etc/ssl/my-nginx.example.com.key /etc/ssl/my-nginx.example.com.key + +RUN mkdir -p /etc/nginx/conf.d \ + && mkdir -p /usr/share/nginx/html/css \ + && mkdir -p /var/lib/nginx/state \ + && mkdir -p /etc/controller-agent/configurator/auxfiles + +VOLUME /usr/share/nginx/html +VOLUME /etc/nginx + +RUN chmod -R 644 /etc/nginx +RUN chmod -R 644 /etc/nginx/conf.d +RUN chmod -R 644 /usr/share/nginx/html/css + +# COPY (Nginx configuration) directory +COPY docker/build-context/content/ /usr/share/nginx/html/ +COPY docker/build-context/nginx/ /etc/nginx/ +COPY oidc*.conf /etc/nginx/conf.d/ +COPY openid*.* /etc/nginx/conf.d/ + +# EXPOSE ports, HTTP 80, HTTPS 443 and, Nginx status page 8010 +STOPSIGNAL SIGTERM +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docs/01-oidc-local-test.md b/docs/01-oidc-local-test.md new file mode 100644 index 0000000..d2343ea --- /dev/null +++ b/docs/01-oidc-local-test.md @@ -0,0 +1,239 @@ +# How To Set Up and Locally Test NGINX Plus OIDC + +This is to provide how to set up IdP, NGINX Plus and locally test OIDC flow in your machine before implementing your frontend and backend application. + +> Note: The main [`README.md`](../README.md) of this repo explains several options in detail. But it focuses on `Getting Started` to quickly set up and test the OIDC flow locally using bundled frontend app and backend environment. + +![](./img/nginx-oidc-workflow.png) + +- [1. Prerequisites](#1-prerequisites) +- [2. Running a Docker Container](#2-running-a-docker-container) +- [3. Running a Browser and Checking If Bundle Page Works](#3-running-a-browser-and-checking-if-bundle-page-works) +- [4. Setting up an Identity Provider (IdP)](#4-setting-up-an-identity-provider-idp) +- [5. Configuring NGINX Plus](#5-configuring-nginx-plus) +- [6. Testing NGINX Plus OIDC](#6-testing-nginx-plus-oidc) +- [7. Stop and Remove Docker Containers](#7-stop-and-remove-docker-containers) + +## 1. Prerequisites + +- [Install and Run Docker](https://docs.docker.com/engine/install/) in your local machine. + +- Edit `hosts` file in your laptop via if you want to locally test NGINX Plus OIDC: + + ```bash + $ sudo vi /etc/hosts or notepad.exe c:\Windows\System32\Drivers\etc\hosts + + 127.0.0.1 nginx.keycloak.test + 127.0.0.1 host.docker.internal + ``` + + > Note: In this example, **Keycloak** is used as it can be installed as one of Docker containers in your local machine. + +- [Download NGINX Plus license files](https://www.nginx.com/free-trial-request/), and copy them to ./docker/build-context/ssl/ + + ```bash + nginx-repo.crt + nginx-repo.key + ``` + +- Clone the [nginxinc/nginx-openid-connect](https://github.com/nginxinc/nginx-openid-connect) GitHub repository, or download the repo files. + + ```bash + git clone https://github.com/nginxinc/nginx-openid-connect.git + ``` + +## 2. Running a Docker Container + +- Start a Docker container: + + ```bash + $ make start + ``` + + > Note: + > + > - In this example, a Keycloak container image (`jboss/keycloak:15.1.0`) is used. + > - Please try another version of Keycloak container images in [`docker-compose.yml`](../docker-compose.yml) if you have any issue when starting it in your local machine. + +
+ +- Check Docker container's status: + + ```bash + $ make watch + ``` + + ![](./img/make-watch.png) + +## 3. Running a Browser and Checking If Bundle Page Works + +- Run a Web Browser with `http://nginx.oidc.test:8020/`, and check if the bundle frontend landing page is shown: + + ![](./img/bundle-frontend-landing-page.png) + +## 4. Setting up an Identity Provider (IdP) + +Choose one of your prefered IdPs, and set up your IdP by referencing the following guides: + +- [Create and configure an app in Amazon Cognito](https://github.com/nginx-openid-connect/nginx-oidc-amazon-cognito/blob/main/docs/01-IdP-Setup.md) +- [Create and configure an app in Auto0](https://github.com/nginx-openid-connect/nginx-oidc-auth0/blob/main/docs/01-Auth0-Setup.md) +- [Create and configure an app in Azure AD](https://github.com/nginx-openid-connect/nginx-oidc-azure-ad/blob/main/docs/01-IdP-Setup.md) +- [Create and configure an app in Keycloak](https://github.com/nginx-openid-connect/nginx-oidc-keycloak/blob/main/docs/01-IdP-Setup.md) +- [Create and configure an app in Okta](https://github.com/nginx-openid-connect/nginx-oidc-okta/blob/main/docs/01-IdP-Setup.md) +- [Create and configure an app in OneLogin](https://github.com/nginx-openid-connect/nginx-oidc-onelogin/blob/main/docs/01-IdP-Setup.md) +- [Create and configure an app in Ping Identity](https://github.com/nginx-openid-connect/nginx-oidc-ping-identity/blob/main/docs/01-IdP-Setup.md) + +> Notes: +> +> - In this example, **Keycloak** is used as it can be installed as one of Docker containers in your local machine. +> - **Client ID**: `my-client-id` +> - **Access Type**: `public` for PKCE +> - **Valid Redirected URIs**: +> - `http://nginx.oidc.test:8020/_codexch` +> - `http://nginx.oidc.test:8020/v2/_logout` +> - The above references will be eventually consolidated into the [NGINX Docs](https://docs.nginx.com/nginx/deployment-guides/single-sign-on/). So feel free to contribute the repo to make better examples for each IdP as references. + +## 5. Configuring NGINX Plus + +Update the NGINX Plus configuration file if you want. Otherwise, skip the following steps for your quick test as the minimal config information is already provided in this repo. + +- In the `openid_connect_configuration.conf`, find and update `$oidc_authz_endpoint`, `$oidc_token_endpoint`, `$oidc_jwt_keyfile`, `$oidc_end_session_endpoint`, `$oidc_userinfo_endpoint`, `$oidc_client`, `$oidc_pkce_enable`, `$oidc_client_secret`, and `$oidc_scopes` upon your setup in IdP. + + ```nginx + map $host $oidc_authz_endpoint { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/auth"; + } + + map $host $oidc_token_endpoint { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/token"; + } + + map $host $oidc_jwt_keyfile { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/certs"; + } + + map $host $oidc_end_session_endpoint { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/logout"; + } + + map $host $oidc_userinfo_endpoint { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/userinfo"; + } + + map $host $oidc_client { + default "my-client-id"; + } + + map $host $oidc_pkce_enable { + default 1; + } + + map $host $oidc_client_secret { + default "my-client-secret"; + } + + map $host $oidc_scopes { + default "openid+profile+email+offline_access"; + } + + 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; + } + ``` + +- In the `openid_connect.server_conf`, find the directive of `$resolver` and update it, + + ```bash + resolver 127.0.0.11; # For local Docker DNS lookup + # 8.8.8.8; # For global DNS lookup of IDP endpoint + ``` + +- Restart the instance of NGINX Plus + + ```bash + docker exec -it nginxplus-oidc-keycloak bash + nginx -t + nginx -s reload + ``` + +## 6. Testing NGINX Plus OIDC + +- Click `Sign In` button: + + ![](./img/oidc-keycloak-login.png) + + ![](./img/oidc-signed-in-page.png) + +- Click `Call a Sample Proxied API` button: + + ![](./img/oidc-sample-proxied-api.png) + + > Note: + > + > - In the [`frontend.conf`](../frontend.conf), you can add additional API endpoints like: + > + > ```nginx + > 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 + > + > proxy_set_header Authorization "Bearer $access_token"; + > proxy_pass http://my_backend_app; + > } + > ``` + > + > - So you can enter a different URI for testing your additional API endpoints via this bundled frontend tool. + +- Click `Sign Out` button: + + - Redirect to the original landing page if you configure `$post_logout_return_uri` with `$redirect_base` as the following example: + + ```nginx + map $host $post_logout_return_uri { + default $redirect_base; + } + ``` + + ![](./img/oidc-original-landing-page.png) + + - Redirect to the custom logout page if you configure `$post_logout_return_uri` with `$redirect_base/signout` such as a custom URI as the following example: + + ```nginx + map $host $post_logout_return_uri { + default $redirect_base/signout; + } + ``` + + ![](./img/oidc-custom-logout-page.png) + +## 7. Stop and Remove Docker Containers + +- Stop Docker containers + + ```bash + $ make down + ``` + +- Remove Docker container images + + ```bash + $ make clean + ``` diff --git a/docs/02-getting-started.md b/docs/02-getting-started.md new file mode 100644 index 0000000..9283879 --- /dev/null +++ b/docs/02-getting-started.md @@ -0,0 +1,228 @@ +# 🏠 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 + + > Note: Two configuration examples are provided as follows. + > + > - 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 /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: + +```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 ; +``` diff --git a/docs/img/bundle-frontend-landing-page.png b/docs/img/bundle-frontend-landing-page.png new file mode 100644 index 0000000..054e34e Binary files /dev/null and b/docs/img/bundle-frontend-landing-page.png differ diff --git a/docs/img/make-watch.png b/docs/img/make-watch.png new file mode 100644 index 0000000..24ef23a Binary files /dev/null and b/docs/img/make-watch.png differ diff --git a/docs/img/nginx-oidc-workflow.png b/docs/img/nginx-oidc-workflow.png new file mode 100644 index 0000000..1cb1da5 Binary files /dev/null and b/docs/img/nginx-oidc-workflow.png differ diff --git a/docs/img/oidc-custom-logout-page.png b/docs/img/oidc-custom-logout-page.png new file mode 100644 index 0000000..3d9c0f6 Binary files /dev/null and b/docs/img/oidc-custom-logout-page.png differ diff --git a/docs/img/oidc-keycloak-login.png b/docs/img/oidc-keycloak-login.png new file mode 100644 index 0000000..3d2070d Binary files /dev/null and b/docs/img/oidc-keycloak-login.png differ diff --git a/docs/img/oidc-original-landing-page.png b/docs/img/oidc-original-landing-page.png new file mode 100644 index 0000000..4b76c70 Binary files /dev/null and b/docs/img/oidc-original-landing-page.png differ diff --git a/docs/img/oidc-sample-proxied-api.png b/docs/img/oidc-sample-proxied-api.png new file mode 100644 index 0000000..0c0f4e8 Binary files /dev/null and b/docs/img/oidc-sample-proxied-api.png differ diff --git a/docs/img/oidc-signed-in-page.png b/docs/img/oidc-signed-in-page.png new file mode 100644 index 0000000..d5a302b Binary files /dev/null and b/docs/img/oidc-signed-in-page.png differ diff --git a/frontend.conf b/frontend.conf index d79f10d..1ab3628 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,104 @@ 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. +# - This is a bundle frontend app to locally test NGINX Plus OIDC workflow. +# + Sample: ./docker/build-context/nginx/sample/proxy_server_frontend.conf +# - 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. +# - This is a bundle backend app to locally test an API using access token. +# + Sample: ./docker/build-context/nginx/sample/proxy_server_backend.conf +# - 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..a73b624 100644 --- a/openid_connect.server_conf +++ b/openid_connect.server_conf @@ -1,7 +1,9 @@ # 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 127.0.0.11; # For local Docker DNS lookup + # 8.8.8.8; # For global DNS lookup of IDP endpoint + 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 +44,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 +68,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..ef987dd 100644 --- a/openid_connect_configuration.conf +++ b/openid_connect_configuration.conf @@ -4,16 +4,24 @@ # the $host variable is used as the default input parameter but can be changed. # map $host $oidc_authz_endpoint { - default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/auth"; + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/auth"; #www.example.com "https://my-idp/oauth2/v1/authorize"; } map $host $oidc_token_endpoint { - default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/token"; + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/token"; } map $host $oidc_jwt_keyfile { - default "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs"; + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/certs"; +} + +map $host $oidc_end_session_endpoint { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/logout"; +} + +map $host $oidc_userinfo_endpoint { + default "http://host.docker.internal:8080/auth/realms/master/protocol/openid-connect/userinfo"; } map $host $oidc_client { @@ -21,7 +29,7 @@ map $host $oidc_client { } map $host $oidc_pkce_enable { - default 0; + default 1; } map $host $oidc_client_secret { @@ -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;