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 = FalseThis 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 = FixedDockerSpawnerSolution 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_envSolution 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:
- Restart JupyterHub
- Stop any existing user containers
- Start a new named server
- 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.