or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication.mdconfiguration-utilities.mdcore-application.mddatabase-models.mdindex.mdmonitoring-metrics.mdrbac-permissions.mdrest-api.mdservices-oauth.mdsingleuser-integration.mdspawners.md

services-oauth.mddocs/

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

```