Let’s look at how the request flow in this setup differs from the one in Part 2: Reverse proxies such as Nginx include a feature called authentication sub‑requests, provided by the auth_request module. This mechanism allows the proxy to delegate authentication checks to a separate service — in our case, the OIDC app.
Here’s what happens step by step:
1. A user sends a request to the reverse proxy app (for example, Nginx).
2+3. The proxy makes a sub‑request (auth_request) to the authentication backend — the OIDC app, and redirects the the user request to the OIDC app.
4+5. The OIDC app verifies the user’s credentials or session by contacting the OIDC SSO provider to perform a login. If the OIDC app responds with an HTTP 2xx status code (meaning "success"), the authenticated request is redirected back to the oidc app.
6. OIDC app verifies authorization code and obtains user information with the OIDC SSO provider and, if successful, redirects the authenticated request back to the oidc app.
7. Authenticated User request is redirected to the reverse proxy app, where the proxy makes a sub‑request (auth_request) to the authentication backend — the OIDC app, again.
8. If successful, Nginx forwards (proxy_pass) the original request to the protected application (Origin app).
Anywhere in the flow, if the response is anything else (such as 401 Unauthorized), Nginx blocks access or redirects the user to the login flow.
This setup clearly separates routing from authentication. The reverse proxy (for example, Nginx) focuses on directing traffic to the correct backend service, while the OIDC app handles user authentication and access control.
Using a dedicated reverse proxy also brings several practical benefits. For instance, the OAuth2 Proxy we used in Part 2 can struggle in scenarios where the backend application needs to establish WebSocket connections. A full-featured reverse proxy such as Nginx supports WebSockets natively, so this limitation no longer applies. By introducing Nginx as the routing layer, you gain greater flexibility and compatibility with a variety of backend communication patterns.
In the next section, we’ll put this capability to use by showing how to run an access-protected JupyterLab on IBM Cloud Code Engine. This setup would not have been possible with the approach from Part 2, because JupyterLab relies heavily on dynamic WebSocket connections — something the OAuth2 Proxy could not handle at the time of writing.
Setup and Configuration
In this section, we will prepare the environment and required resources for running a more advanced sample with IBM Cloud Code Engine. The source code of this example has been published to the following repository https://github.com/IBM/CodeEngine/tree/main/auth-oidc-proxy:
- Install and configure the IBM Cloud CLI
- Create or select a Code Engine project
-
Create or select the Code Engine project of your choice; e.g.
ibmcloud ce project create --name oidc-sample or
ibmcloud ce project select --name <yourProjectName>
-
Once the project had been created or selected, run the following commands to initialize environment variables in your current terminal session that are required later on:
CE_PROJECT=$(ibmcloud ce project current --output json)
CE_PROJECT_DOMAIN=$(echo "$CE_PROJECT" | jq -r '.domain')
CE_PROJECT_NAMESPACE=$(echo "$CE_PROJECT" | jq -r '.kube_config_context')
ROOT_DOMAIN=.${CE_PROJECT_NAMESPACE}.${CE_PROJECT_DOMAIN}
echo "CE root domain: ${ROOT_DOMAIN}"
-
Deploy a JupyterLab, which is a web-based interactive development environment for data science, scientific computing, and machine learning
- ibmcloud ce app create --name jupyter-lab \
--image "quay.io/jupyter/minimal-notebook:latest" \
--max-scale 1 \
--scale-down-delay 600 \
--command "jupyter" \
--command "lab" \
--command "--ip" \
--command "*" \
--command "--NotebookApp.allow_origin" \
--command "*" \
--command "--NotebookApp.token" \
--command "''" \
--command "--NotebookApp.password" \
--command "''" \
--service-account "none" \
--port 8888 \
--visibility project
- To provide necessary configuration information, we’ll eventually create a Code Engine secret that exposes relevant information to the application as environment variables. For now, let us use a temporary file located on your local workstation, called "oidc-proxy.properties" that contains the following content:
OIDC_CLIENT_ID & OIDC_CLIENT_SECRET
Before registering a new GitHub OAuth app, we’ll need to determine the homepage and callback URLs of our application. In principal both URL pattern starts with: "https://<app-name>.<proj-namespace>.<code-engine-region>.codeengine.appdomain.cloud".
The <app-name> should correspond to the public facing name that you want to surface. Assuming, you’ll want to expose your app with the name "jupyter-proxy", the run the following command in your local terminal to craft the base URL:
echo "https://jupyter-proxy${ROOT_DOMAIN}"
Register a new OAuth app in GitHub.com
-
-
On github.com, navigate to "Settings > Developer Settings" https://github.com/settings/applications/new
-
Provide an application name; e.g. "my-jupyter-sample"
-
Put in the jupyter-proxy app (that we will create in a moment) URL as "Homepage URL"; e.g. "https://jupyter-proxy${ROOT_DOMAIN}/"
-
As "Authorization callback URL", use the value of the URL of the jupyter-auth app (that we will create in a moment) and append the route "/auth/callback"; e.g. "https://jupyter-auth${ROOT_DOMAIN}/auth/callback"
-
Complete this step by clicking "Register application"
-
On the next page, GitHub.com provides necessary meta information and credentials necessary to establish the trust relation with the application that initiates the OIDC flow
-
Copy the Client ID and paste into the "oauth2-proxy.properties" as value for the property OAUTH2_PROXY_CLIENT_ID
-
Click "Generate a new client secret", to obtain a new client secret. Use the value as property OAUTH2_PROXY_CLIENT_SECRET
COOKIE_ENCRYPTION_KEY
As the value for the property COOKIE_ENCRYPTION_KEY, we’ll generate an base64 encoded sequence of random bytes.
In order to make sure that the authentication flow will only be performed once per user session, the app will use a cookie that stores the authentication context. To protect those sensitive information from malicious actors, it is encrypted using symmetric encryption method and by applying AES 256 CBC as encryption algorithm that utilizes the provided key. If you have the openssl tool installed on your machine, the following command can be used to generate and encryption key from your terminal window:
openssl rand -base64 32
AUTHZ_USER_PROPERTY & AUTHZ_ALLOWED_USERS
So far, we only explored authentication capabilities, which make sure that only authenticated users can access your workload. With this example, we’ll also tap into authorization capabilities, which allows to authorize authenticated user based on profile attributes (e.g. login name, email address, groups). A typical real world scenario would protect your app by only allowing users that are able to enterprise OIDC SSO challenge (=authentication) AND who are member of a certain group within the enterprise directory service (=authorization).
For our example we’ll apply a configuration that will only allow a single user to access the app, by specifying AUTHZ_USER_PROPERTY=login and AUTHZ_ALLOWED_USERS=<your-github-com-login-name>. For a full list of available user properties, see the GitHub.com REST API documentation.
-
Now, that we have all configuration values at hand, let us create the Code Engine secret, that will make sure to pass them along to the deployed app.
-
Next, we’ll deploy the authentication and authorization app that will be called on each incoming request to verify whether the caller is authenticated and authorized
- ibmcloud ce app create --name jupyter-auth \
--image "icr.io/codeengine/auth-oidc-proxy/auth" \
--max-scale 1 \
--cpu 0.125 \
--memory 0.25G \
--scale-down-delay 600 \
--port 8080 \
--env-from-secret oidc-proxy-credentials \
--env COOKIE_DOMAIN="$ROOT_DOMAIN" \
--env REDIRECT_URL="https://jupyter-proxy${ROOT_DOMAIN}" \
--env OIDC_REDIRECT_URL="https://jupyter-auth${ROOT_DOMAIN}/auth/callback"
-
Lastly, we’ll deploy the proxy application that is in charge to check authorization
- ibmcloud ce app create --name jupyter-proxy \
--image "icr.io/codeengine/auth-oidc-proxy/nginx" \
--max-scale 1 \
--cpu 0.5 \
--memory 1G \
--scale-down-delay 600 \
--env ORIGIN_APP_FQDN="jupyter-proxy${ROOT_DOMAIN}" \
--env ORIGIN_APP_NAME=jupyter-lab \
--env AUTH_APP_NAME=jupyter-auth \
--port 8080
Now, that we do have everything in place, let us call the jupyter-proxy app by opening a browser and calling the proxy app endpoint. Once the login procedure has been passed, you’ll be greeted with the JupyterLab dashboard.