JupyterHub OAuth Scope Fix for Named Servers

JupyterHub OAuth Scope Fix for Named Servers

Calliope Integration: This component is integrated into the Calliope AI platform. Some features and configurations may differ from the upstream project.

The Problem

When JupyterHub spawns named servers (like waiide), it’s setting incorrect OAuth scopes in the environment variables. This causes a “403: Forbidden - user lmata is not allowed” error.

Current (Incorrect):

JUPYTERHUB_OAUTH_ACCESS_SCOPES=["access:servers!server=lmata/", "access:servers!user=lmata"]

Should Be:

JUPYTERHUB_OAUTH_ACCESS_SCOPES=["access:servers!server=lmata/waiide", "access:servers!user=lmata"]

The server name (waiide) is missing from the scope!

Solution 1: Disable Named Servers (Quick Fix)

In your JupyterHub configuration:

# Disable named servers to avoid OAuth issues
c.JupyterHub.allow_named_servers = False

This forces all users to have a single default server, avoiding the scope issue entirely.

Solution 2: Fix OAuth Scopes for Named Servers

Add this to your JupyterHub configuration file:

# Fix OAuth scopes for named servers
from jupyterhub.spawner import Spawner
import json

class FixedScopeSpawner(Spawner):
    """Spawner that fixes OAuth scopes for named servers"""
    
    def get_env(self):
        env = super().get_env()
        
        # Fix the OAuth scopes if this is a named server
        if self.name:  # Named server
            # Get current scopes
            oauth_scopes = json.loads(env.get('JUPYTERHUB_OAUTH_ACCESS_SCOPES', '[]'))
            
            # Add the correct scope with server name
            correct_scope = f"access:servers!server={self.user.name}/{self.name}"
            if correct_scope not in oauth_scopes:
                # Replace the incorrect scope
                oauth_scopes = [
                    scope if not scope.startswith(f"access:servers!server={self.user.name}/") 
                    else correct_scope 
                    for scope in oauth_scopes
                ]
                # Ensure the scope is present
                if correct_scope not in oauth_scopes:
                    oauth_scopes.append(correct_scope)
                
                env['JUPYTERHUB_OAUTH_ACCESS_SCOPES'] = json.dumps(oauth_scopes)
        
        return env

# Use the fixed spawner with DockerSpawner
from dockerspawner import DockerSpawner

class FixedDockerSpawner(FixedScopeSpawner, DockerSpawner):
    pass

c.JupyterHub.spawner_class = FixedDockerSpawner

Solution 3: Patch the Spawner Environment (Simpler)

Add this after your spawner configuration:

# Patch environment variables for named servers
from jupyterhub.spawner import Spawner
import json

original_get_env = Spawner.get_env

def patched_get_env(self):
    env = original_get_env(self)
    
    # Fix OAuth scopes for named servers
    if self.name and 'JUPYTERHUB_OAUTH_ACCESS_SCOPES' in env:
        oauth_scopes = json.loads(env['JUPYTERHUB_OAUTH_ACCESS_SCOPES'])
        server_scope = f"access:servers!server={self.user.name}/{self.name}"
        
        # Fix any incorrect server scopes
        fixed_scopes = []
        for scope in oauth_scopes:
            if scope.startswith(f"access:servers!server={self.user.name}/") and scope != server_scope:
                fixed_scopes.append(server_scope)
            else:
                fixed_scopes.append(scope)
        
        # Ensure the correct scope is present
        if server_scope not in fixed_scopes:
            fixed_scopes.append(server_scope)
            
        env['JUPYTERHUB_OAUTH_ACCESS_SCOPES'] = json.dumps(fixed_scopes)
    
    return env

Spawner.get_env = patched_get_env

Solution 4: Configure OAuth Scopes Properly

Ensure your JupyterHub OAuth configuration includes proper scopes:

# Configure OAuth scopes for services
c.JupyterHub.load_roles = [
    {
        "name": "user",
        "scopes": [
            "self",
            "access:servers",  # Allow access to all of user's servers
        ],
    },
    {
        "name": "server",
        "scopes": [
            "access:servers!server",  # The server's own access
            "users:activity!user",    # Activity tracking
        ],
    }
]

Testing the Fix

After applying the fix:

  1. Restart JupyterHub
  2. Stop any existing user containers
  3. Start a new named server
  4. Check that the URL works: http://localhost:8008/user/lmata/waiide/

Debugging

To verify the fix is working, check the container’s environment:

# Find the container
docker ps | grep waiide

# Check the OAuth scopes
docker exec CONTAINER_ID bash -c 'echo $JUPYTERHUB_OAUTH_ACCESS_SCOPES'

The output should include: "access:servers!server=lmata/waiide"

Alternative: Use Default Servers Only

If named servers continue to cause issues, use the default server with a custom URL:

# Use default server, but redirect to WAIIDE
c.JupyterHub.allow_named_servers = False
c.Spawner.default_url = '/proxy/8081/'

# Container name without server name
c.DockerSpawner.name_template = '{username}-waiide'

Summary

The issue is that JupyterHub’s OAuth scope generation for named servers is incomplete. The hub needs to include the full server path (username/servername) in the access:servers!server= scope. Any of the solutions above will fix this on the hub side.