Cloud Platform as a Service

Cloud Platform as a Service

Join us to learn more from a community of collaborative experts and IBM Cloud product users to share advice and best practices with peers and stay up to date regarding product enhancements, regional user group meetings, webinars, how-to blogs, and other helpful materials.

 View Only

Part3: Advanced OIDC integrations with Code Engine

By Enrico Regge posted 4 days ago

  

Written by
Simon Daniel Moser (smoser@de.ibm.com) - Distinguished Engineer for Containers @ IBM Cloud
Enrico Regge (reggeenr@de.ibm.com) - Software Architect @ IBM Cloud

----------------------------------

In the second part of this article series, we demonstrated how to secure access to a Code Engine application using an OAuth2 Proxy with GitHub as the external OIDC provider. In that setup, the OAuth2 Proxy served two main purposes: authenticating users and routing traffic to the origin application. In more advanced scenarios, however, it can be beneficial to separate these responsibilities — using one component for routing traffic and another for authentication and enforcing access policies. For example, if you have two applications (App A and App B) that both need protection, the previous approach would require deploying a separate OAuth2 Proxy in front of each app.

Wouldn’t it be simpler to have a single authentication component that validates users once and then routes them to App A or App B as appropriate? The good news is that this setup is possible without creating a custom routing solution. You can use a standard reverse proxy such as Nginx to handle routing, while an OAuth2 Proxy manages authentication to ensure that only authorized users reach the applications. Conceptually, this "OAuth2 Proxy" is similar to the one from Part 2, but it works alongside the reverse proxy instead of performing both roles. In Figure 1, the "Proxy app" represents the reverse proxy (e.g., Nginx), and the "OIDC app" represents the authentication service.

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:

  1. Install and configure the IBM Cloud CLI
    • Install the IBM Cloud CLI including the Code Engine plugin as described in the Code Engine documentation
    • Perform all necessary login steps to IBM Cloud

  2. 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}"

  3. 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
  4. 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

    1. On github.com, navigate to "Settings > Developer Settings" https://github.com/settings/applications/new

    2. Provide an application name; e.g. "my-jupyter-sample"

    3. Put in the jupyter-proxy app (that we will create in a moment) URL as "Homepage URL"; e.g. "https://jupyter-proxy${ROOT_DOMAIN}/"

    4. 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"

    5. Complete this step by clicking "Register application"

    6. 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

      1. Copy the Client ID and paste into the "oauth2-proxy.properties" as value for the property OAUTH2_PROXY_CLIENT_ID

      2. 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.

  1. 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.

    • ibmcloud ce secret create \
          --name oidc-proxy-credentials \
          --from-env-file oidc-proxy.properties

  2. 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"
  3. 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.

And yes, you are now able to run a remote JupyterLab allowing you to create und execute notebooks on IBM Cloud Code Engine 🙂

Next, let’s do a deep dive into one key aspects of the implementation and take a closer look at the nginx proxy that makes sure that you JupyterLab is protected properly. The following code snippet depicts the nginx configuration. In the root route "/", each request will be handled by first sending a subrequest to the "/auth" route. 

  • If that subrequest is causing a 401 response, the user will be directed to the login endpoint.
  • If that subrequest succeeds, the request will be forwarded to the origin app, the jupyter-lab. 

Pay attention to the proxy_set_header statements that set the Upgrade and Connection headers, which make sure that Websocket support is being enabled, a feature being heavily used by the JupyterLab.

Also, notice that requests to both backend apps are being realised through project internal URLs (.svc.cluster.local).

server {

    listen 8080;
    server_name ${ORIGIN_APP_FQDN};
    root         /opt/app-root/src;

    location / {        
        auth_request /auth;
        error_page 401 = /auth/login;
        
        proxy_pass http://${ORIGIN_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local;
        proxy_set_header Host ${ORIGIN_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
        proxy_pass_request_headers      on;
        
        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 86400;
    }

    location /auth {
        proxy_pass http://${AUTH_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local;
        proxy_set_header Host ${AUTH_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local;
        proxy_pass_request_body off;
        proxy_set_header        Content-Length "";
        proxy_set_header        X-Original-URI $request_uri;
        proxy_pass_request_headers      on;
        
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 86400;
    }
}

In this example, we are just focusing on the Nginx configuration. The mechanics of the OIDC app setup is similar to the setup described in Part1 of this blog post series. The full source code of the auth app is available here: https://github.com/IBM/CodeEngine/tree/main/auth-oidc-proxy/auth

Summary and Conclusion

For the blog series OIDC with IBM Cloud Code Engine, here’s a quick recap of what you’ve learned:

  • Part 1: We demonstrated a do‑it‑yourself approach to securing access to a Code Engine application using OIDC with GitHub identities. You learned how to redirect users to an identity provider, obtain an ID token, and establish a session.

  • Part 2: We explored a simpler, no‑code method for protecting your application — using an OAuth2 Proxy with GitHub as the external OIDC provider.

  • Part 3: We applied these concepts to a real‑world example that uses WebSockets and Jupyter Notebooks, showing why separating routing from authentication is beneficial in advanced scenarios. We also discussed how to define more fine‑grained permissions, such as user‑ or group‑specific authorization.

With that, we conclude our series. However, given Jupyter notebooks are very very popular in the Data Science Community, but a JupyterLab is only worth half without persistent storage, as you will notice that your notebooks are going to be deleted as soon the app instance scales down. We'll be releasing another blog post soon that talks about how a new Code Engine feature makes it easy to use persistent storage and so preserves your Jupyter notebooks.

0 comments
3 views

Permalink