0
# Services and OAuth Integration
1
2
JupyterHub provides comprehensive support for integrating external services through managed and unmanaged services, along with OAuth 2.0 provider capabilities for third-party application integration. This system enables secure authentication and authorization for external applications and services.
3
4
## Capabilities
5
6
### Service Management
7
8
Core functionality for registering and managing external services with JupyterHub.
9
10
```python { .api }
11
class Service:
12
"""
13
JupyterHub service for external application integration.
14
15
Services can be managed (started/stopped by JupyterHub) or
16
unmanaged (external processes that authenticate with JupyterHub).
17
"""
18
19
# Service configuration
20
name: str # Unique service name
21
url: str # Service URL (for unmanaged services)
22
prefix: str # URL prefix for routing requests
23
command: List[str] # Command to start managed service
24
environment: Dict[str, str] # Environment variables
25
26
# Service properties
27
admin: bool # Whether service has admin privileges
28
managed: bool # Whether JupyterHub manages the service process
29
api_token: str # API token for service authentication
30
oauth_client_id: str # OAuth client ID (if using OAuth)
31
32
def __init__(self, **kwargs):
33
"""
34
Initialize service configuration.
35
36
Args:
37
**kwargs: Service configuration parameters
38
"""
39
40
def start(self):
41
"""Start a managed service process"""
42
43
def stop(self):
44
"""Stop a managed service process"""
45
46
@property
47
def pid(self) -> int:
48
"""Process ID of managed service"""
49
50
@property
51
def proc(self) -> subprocess.Popen:
52
"""Process object for managed service"""
53
54
# Service configuration in jupyterhub_config.py
55
c.JupyterHub.services = [
56
{
57
'name': 'my-service',
58
'url': 'http://localhost:8001',
59
'api_token': 'secret-token',
60
'admin': True
61
}
62
]
63
```
64
65
### Hub Authentication for Services
66
67
Authentication utilities for services to authenticate with JupyterHub.
68
69
```python { .api }
70
class HubAuth:
71
"""
72
Authentication helper for JupyterHub services.
73
74
Provides methods for services to authenticate requests
75
and interact with the JupyterHub API.
76
"""
77
78
def __init__(self,
79
api_token: str = None,
80
api_url: str = None,
81
cache_max_age: int = 300):
82
"""
83
Initialize Hub authentication.
84
85
Args:
86
api_token: Service API token
87
api_url: JupyterHub API URL
88
cache_max_age: Cache duration for user info
89
"""
90
91
async def user_for_token(self, token: str, sync: bool = True) -> Dict[str, Any]:
92
"""
93
Get user information for an API token.
94
95
Args:
96
token: API token to validate
97
sync: Whether to sync with database
98
99
Returns:
100
User information dictionary or None if invalid
101
"""
102
103
async def user_for_cookie(self, cookie_name: str, cookie_value: str, use_cache: bool = True) -> Dict[str, Any]:
104
"""
105
Get user information for a login cookie.
106
107
Args:
108
cookie_name: Name of the cookie
109
cookie_value: Cookie value
110
use_cache: Whether to use cached results
111
112
Returns:
113
User information dictionary or None if invalid
114
"""
115
116
async def api_request(self, method: str, url: str, **kwargs) -> requests.Response:
117
"""
118
Make authenticated request to JupyterHub API.
119
120
Args:
121
method: HTTP method (GET, POST, etc.)
122
url: API endpoint URL (relative to api_url)
123
**kwargs: Additional request parameters
124
125
Returns:
126
HTTP response object
127
"""
128
129
# Example service using HubAuth
130
from jupyterhub.services.auth import HubAuth
131
132
auth = HubAuth(
133
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
134
api_url=os.environ['JUPYTERHUB_API_URL']
135
)
136
137
@app.route('/dashboard')
138
async def dashboard():
139
"""Protected service endpoint"""
140
cookie = request.cookies.get('jupyterhub-hub-login')
141
user = await auth.user_for_cookie('jupyterhub-hub-login', cookie)
142
143
if not user:
144
return redirect('/hub/login')
145
146
return render_template('dashboard.html', user=user)
147
```
148
149
### OAuth Provider System
150
151
JupyterHub's OAuth 2.0 provider implementation for third-party application integration.
152
153
```python { .api }
154
class OAuthProvider:
155
"""
156
OAuth 2.0 authorization server implementation.
157
158
Enables third-party applications to obtain access tokens
159
for JupyterHub API access.
160
"""
161
162
# OAuth endpoints (handled by JupyterHub)
163
# GET /hub/api/oauth2/authorize - Authorization endpoint
164
# POST /hub/api/oauth2/token - Token endpoint
165
# GET /hub/api/oauth2/userinfo - User info endpoint
166
167
def generate_authorization_code(self, client_id: str, user: User, scopes: List[str]) -> str:
168
"""
169
Generate OAuth authorization code.
170
171
Args:
172
client_id: OAuth client ID
173
user: Authorizing user
174
scopes: Requested scopes
175
176
Returns:
177
Authorization code string
178
"""
179
180
def exchange_code_for_token(self, code: str, client_id: str, client_secret: str) -> Dict[str, Any]:
181
"""
182
Exchange authorization code for access token.
183
184
Args:
185
code: Authorization code
186
client_id: OAuth client ID
187
client_secret: OAuth client secret
188
189
Returns:
190
Token response with access_token, token_type, scope
191
"""
192
193
def get_user_info(self, access_token: str) -> Dict[str, Any]:
194
"""
195
Get user information for access token.
196
197
Args:
198
access_token: OAuth access token
199
200
Returns:
201
User information (subject, name, groups, etc.)
202
"""
203
204
# OAuth client registration
205
class OAuthClient(Base):
206
"""OAuth client application registration"""
207
208
id: str # Client ID (primary key)
209
identifier: str # Human-readable identifier
210
description: str # Client description
211
secret: str # Client secret (hashed)
212
redirect_uri: str # Authorized redirect URI
213
allowed_scopes: List[str] # Scopes client can request
214
215
def check_secret(self, secret: str) -> bool:
216
"""Verify client secret"""
217
218
def check_redirect_uri(self, uri: str) -> bool:
219
"""Verify redirect URI is authorized"""
220
```
221
222
## Usage Examples
223
224
### Basic Service Configuration
225
226
```python
227
# jupyterhub_config.py
228
229
# Unmanaged service (external process)
230
c.JupyterHub.services = [
231
{
232
'name': 'announcement-service',
233
'url': 'http://localhost:8001',
234
'api_token': 'your-secret-token-here',
235
'admin': False,
236
'oauth_redirect_uri': 'http://localhost:8001/oauth-callback'
237
}
238
]
239
240
# Managed service (started by JupyterHub)
241
c.JupyterHub.services = [
242
{
243
'name': 'monitoring-service',
244
'managed': True,
245
'command': ['python', '/path/to/monitoring_service.py'],
246
'environment': {
247
'JUPYTERHUB_SERVICE_NAME': 'monitoring-service',
248
'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:8002'
249
},
250
'url': 'http://127.0.0.1:8002',
251
'api_token': 'monitoring-token'
252
}
253
]
254
```
255
256
### Service Implementation Example
257
258
```python
259
# announcement_service.py
260
import os
261
from flask import Flask, request, redirect, render_template
262
from jupyterhub.services.auth import HubAuth
263
264
app = Flask(__name__)
265
266
# Initialize Hub authentication
267
auth = HubAuth(
268
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
269
api_url=os.environ['JUPYTERHUB_API_URL']
270
)
271
272
@app.route('/')
273
async def index():
274
"""Main service page with user authentication"""
275
# Get user from cookie
276
cookie = request.cookies.get('jupyterhub-hub-login')
277
if not cookie:
278
return redirect('/hub/login?next=' + request.url)
279
280
user = await auth.user_for_cookie('jupyterhub-hub-login', cookie)
281
if not user:
282
return redirect('/hub/login?next=' + request.url)
283
284
# Get announcements for user
285
announcements = get_announcements_for_user(user)
286
return render_template('announcements.html',
287
user=user,
288
announcements=announcements)
289
290
@app.route('/api/announcements')
291
async def api_announcements():
292
"""API endpoint for announcements"""
293
# Authenticate via API token
294
token = request.headers.get('Authorization', '').replace('Bearer ', '')
295
user = await auth.user_for_token(token)
296
297
if not user:
298
return {'error': 'Unauthorized'}, 401
299
300
return {'announcements': get_announcements_for_user(user)}
301
302
def get_announcements_for_user(user):
303
"""Get announcements relevant to user"""
304
# Implementation depends on your announcement system
305
return []
306
307
if __name__ == '__main__':
308
app.run(port=8001)
309
```
310
311
### OAuth Client Registration
312
313
```python
314
# Register OAuth client application
315
from jupyterhub.orm import OAuthClient
316
317
client = OAuthClient(
318
id='my-external-app',
319
identifier='My External Application',
320
description='Third-party app that integrates with JupyterHub',
321
redirect_uri='https://myapp.example.com/oauth/callback',
322
allowed_scopes=['read:users', 'read:servers', 'identify']
323
)
324
325
# Set client secret (will be hashed)
326
client.secret = 'your-client-secret-here'
327
328
# Add to database
329
db.add(client)
330
db.commit()
331
```
332
333
### OAuth Flow Implementation
334
335
```python
336
# External application OAuth integration
337
import requests
338
from urllib.parse import urlencode
339
340
class JupyterHubOAuth:
341
"""OAuth client for JupyterHub integration"""
342
343
def __init__(self, client_id, client_secret, hub_url):
344
self.client_id = client_id
345
self.client_secret = client_secret
346
self.hub_url = hub_url
347
348
def get_authorization_url(self, redirect_uri, scopes, state=None):
349
"""
350
Generate OAuth authorization URL.
351
352
Args:
353
redirect_uri: Where to redirect after authorization
354
scopes: List of requested scopes
355
state: CSRF protection state parameter
356
357
Returns:
358
Authorization URL string
359
"""
360
params = {
361
'client_id': self.client_id,
362
'redirect_uri': redirect_uri,
363
'scope': ' '.join(scopes),
364
'response_type': 'code'
365
}
366
if state:
367
params['state'] = state
368
369
return f"{self.hub_url}/hub/api/oauth2/authorize?{urlencode(params)}"
370
371
async def exchange_code_for_token(self, code, redirect_uri):
372
"""
373
Exchange authorization code for access token.
374
375
Args:
376
code: Authorization code from callback
377
redirect_uri: Original redirect URI
378
379
Returns:
380
Token response dictionary
381
"""
382
token_url = f"{self.hub_url}/hub/api/oauth2/token"
383
384
data = {
385
'grant_type': 'authorization_code',
386
'code': code,
387
'redirect_uri': redirect_uri,
388
'client_id': self.client_id,
389
'client_secret': self.client_secret
390
}
391
392
response = requests.post(token_url, data=data)
393
return response.json()
394
395
async def get_user_info(self, access_token):
396
"""
397
Get user information with access token.
398
399
Args:
400
access_token: OAuth access token
401
402
Returns:
403
User information dictionary
404
"""
405
headers = {'Authorization': f'Bearer {access_token}'}
406
response = requests.get(
407
f"{self.hub_url}/hub/api/oauth2/userinfo",
408
headers=headers
409
)
410
return response.json()
411
412
# Usage in web application
413
oauth = JupyterHubOAuth(
414
client_id='my-external-app',
415
client_secret='your-client-secret',
416
hub_url='https://hub.example.com'
417
)
418
419
# Redirect user to authorization
420
auth_url = oauth.get_authorization_url(
421
redirect_uri='https://myapp.example.com/oauth/callback',
422
scopes=['read:users', 'identify'],
423
state='csrf-protection-token'
424
)
425
426
# Handle callback
427
@app.route('/oauth/callback')
428
async def oauth_callback():
429
code = request.args.get('code')
430
state = request.args.get('state')
431
432
# Verify state for CSRF protection
433
if state != session.get('oauth_state'):
434
return 'Invalid state', 400
435
436
# Exchange code for token
437
token_response = await oauth.exchange_code_for_token(
438
code=code,
439
redirect_uri='https://myapp.example.com/oauth/callback'
440
)
441
442
# Get user information
443
user_info = await oauth.get_user_info(token_response['access_token'])
444
445
# Store token and user info in session
446
session['access_token'] = token_response['access_token']
447
session['user'] = user_info
448
449
return redirect('/dashboard')
450
```
451
452
### Service with Role-Based Access
453
454
```python
455
# Service with RBAC integration
456
from jupyterhub.services.auth import HubAuth
457
from jupyterhub.scopes import check_scopes
458
459
class RBACService:
460
"""Service with role-based access control"""
461
462
def __init__(self):
463
self.auth = HubAuth()
464
465
async def check_permission(self, token, required_scopes):
466
"""
467
Check if token has required permissions.
468
469
Args:
470
token: API token or cookie
471
required_scopes: List of required scopes
472
473
Returns:
474
User info if authorized, None otherwise
475
"""
476
user = await self.auth.user_for_token(token)
477
if not user:
478
return None
479
480
user_scopes = user.get('scopes', [])
481
if check_scopes(required_scopes, user_scopes):
482
return user
483
484
return None
485
486
# Usage in service endpoints
487
service = RBACService()
488
489
@app.route('/admin/users')
490
async def admin_users():
491
"""Admin-only endpoint"""
492
token = request.headers.get('Authorization', '').replace('Bearer ', '')
493
user = await service.check_permission(token, ['admin:users'])
494
495
if not user:
496
return {'error': 'Insufficient permissions'}, 403
497
498
# Admin functionality here
499
return {'users': []}
500
```
501
502
## Advanced Integration Patterns
503
504
### Multi-Service Coordination
505
506
```python
507
# Service registry for coordinating multiple services
508
class ServiceRegistry:
509
"""Registry for coordinating multiple JupyterHub services"""
510
511
def __init__(self, hub_auth):
512
self.auth = hub_auth
513
self.services = {}
514
515
async def register_service(self, name, url, capabilities):
516
"""Register a service with capabilities"""
517
# Verify service authentication
518
service_info = await self.auth.api_request('GET', f'/services/{name}')
519
if service_info.status_code == 200:
520
self.services[name] = {
521
'url': url,
522
'capabilities': capabilities,
523
'info': service_info.json()
524
}
525
526
async def discover_service(self, capability):
527
"""Find services with specific capability"""
528
return [
529
service for service in self.services.values()
530
if capability in service['capabilities']
531
]
532
533
# Service mesh configuration
534
c.JupyterHub.services = [
535
{
536
'name': 'service-registry',
537
'managed': True,
538
'command': ['python', '/path/to/service_registry.py'],
539
'url': 'http://127.0.0.1:8003',
540
'admin': True
541
},
542
{
543
'name': 'data-service',
544
'url': 'http://localhost:8004',
545
'capabilities': ['data-processing', 'file-storage']
546
},
547
{
548
'name': 'compute-service',
549
'url': 'http://localhost:8005',
550
'capabilities': ['job-execution', 'resource-management']
551
}
552
]
553
```
554
555
### Event-Driven Service Integration
556
557
```python
558
# Service with event handling
559
import asyncio
560
from jupyterhub.services.auth import HubAuth
561
562
class EventDrivenService:
563
"""Service that responds to JupyterHub events"""
564
565
def __init__(self):
566
self.auth = HubAuth()
567
self.event_queue = asyncio.Queue()
568
569
async def poll_events(self):
570
"""Poll JupyterHub for events"""
571
while True:
572
try:
573
# Check for user activity updates
574
response = await self.auth.api_request('GET', '/users')
575
users = response.json()
576
577
for user in users:
578
if self.should_process_user(user):
579
await self.event_queue.put({
580
'type': 'user_activity',
581
'user': user
582
})
583
584
await asyncio.sleep(30) # Poll every 30 seconds
585
except Exception as e:
586
print(f"Error polling events: {e}")
587
await asyncio.sleep(60)
588
589
async def process_events(self):
590
"""Process events from queue"""
591
while True:
592
event = await self.event_queue.get()
593
await self.handle_event(event)
594
595
async def handle_event(self, event):
596
"""Handle specific event types"""
597
if event['type'] == 'user_activity':
598
await self.update_user_metrics(event['user'])
599
600
def should_process_user(self, user):
601
"""Determine if user event should be processed"""
602
# Your event filtering logic
603
return True
604
605
async def update_user_metrics(self, user):
606
"""Update metrics for user activity"""
607
# Your metrics update logic
608
pass
609
```