Coverage for /pythoncovmergedfiles/medio/medio/src/jupyter_server/jupyter_server/auth/identity.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

337 statements  

1"""Identity Provider interface 

2 

3This defines the _authentication_ layer of Jupyter Server, 

4to be used in combination with Authorizer for _authorization_. 

5 

6.. versionadded:: 2.0 

7""" 

8 

9from __future__ import annotations 

10 

11import binascii 

12import datetime 

13import json 

14import os 

15import re 

16import sys 

17import typing as t 

18import uuid 

19from dataclasses import asdict, dataclass 

20from http.cookies import Morsel 

21 

22from tornado import escape, httputil, web 

23from traitlets import Bool, Dict, Enum, List, TraitError, Type, Unicode, default, validate 

24from traitlets.config import LoggingConfigurable 

25 

26from jupyter_server.transutils import _i18n 

27 

28from .security import passwd_check, set_password 

29from .utils import get_anonymous_username 

30 

31_non_alphanum = re.compile(r"[^A-Za-z0-9]") 

32 

33 

34# Define the User properties that can be updated 

35UpdatableField = t.Literal["name", "display_name", "initials", "avatar_url", "color"] 

36 

37 

38@dataclass 

39class User: 

40 """Object representing a User 

41 

42 This or a subclass should be returned from IdentityProvider.get_user 

43 """ 

44 

45 username: str # the only truly required field 

46 

47 # these fields are filled from username if not specified 

48 # name is the 'real' name of the user 

49 name: str = "" 

50 # display_name is a shorter name for us in UI, 

51 # if different from name. e.g. a nickname 

52 display_name: str = "" 

53 

54 # these fields are left as None if undefined 

55 initials: str | None = None 

56 avatar_url: str | None = None 

57 color: str | None = None 

58 

59 # TODO: extension fields? 

60 # ext: Dict[str, Dict[str, Any]] = field(default_factory=dict) 

61 

62 def __post_init__(self): 

63 self.fill_defaults() 

64 

65 def fill_defaults(self): 

66 """Fill out default fields in the identity model 

67 

68 - Ensures all values are defined 

69 - Fills out derivative values for name fields fields 

70 - Fills out null values for optional fields 

71 """ 

72 

73 # username is the only truly required field 

74 if not self.username: 

75 msg = f"user.username must not be empty: {self}" 

76 raise ValueError(msg) 

77 

78 # derive name fields from username -> name -> display name 

79 if not self.name: 

80 self.name = self.username 

81 if not self.display_name: 

82 self.display_name = self.name 

83 

84 

85def _backward_compat_user(got_user: t.Any) -> User: 

86 """Backward-compatibility for LoginHandler.get_user 

87 

88 Prior to 2.0, LoginHandler.get_user could return anything truthy. 

89 

90 Typically, this was either a simple string username, 

91 or a simple dict. 

92 

93 Make some effort to allow common patterns to keep working. 

94 """ 

95 if isinstance(got_user, str): 

96 return User(username=got_user) 

97 elif isinstance(got_user, dict): 

98 kwargs = {} 

99 if "username" not in got_user and "name" in got_user: 

100 kwargs["username"] = got_user["name"] 

101 for field in User.__dataclass_fields__: 

102 if field in got_user: 

103 kwargs[field] = got_user[field] 

104 try: 

105 return User(**kwargs) 

106 except TypeError: 

107 msg = f"Unrecognized user: {got_user}" 

108 raise ValueError(msg) from None 

109 else: 

110 msg = f"Unrecognized user: {got_user}" 

111 raise ValueError(msg) 

112 

113 

114class IdentityProvider(LoggingConfigurable): 

115 """ 

116 Interface for providing identity management and authentication. 

117 

118 Two principle methods: 

119 

120 - :meth:`~jupyter_server.auth.IdentityProvider.get_user` returns a :class:`~.User` object 

121 for successful authentication, or None for no-identity-found. 

122 - :meth:`~jupyter_server.auth.IdentityProvider.identity_model` turns a :class:`~jupyter_server.auth.User` into a JSONable dict. 

123 The default is to use :py:meth:`dataclasses.asdict`, 

124 and usually shouldn't need override. 

125 

126 Additional methods can customize authentication. 

127 

128 .. versionadded:: 2.0 

129 """ 

130 

131 cookie_name: str | Unicode[str, str | bytes] = Unicode( 

132 "", 

133 config=True, 

134 help=_i18n("Name of the cookie to set for persisting login. Default: username-${Host}."), 

135 ) 

136 

137 cookie_options = Dict( 

138 config=True, 

139 help=_i18n( 

140 "Extra keyword arguments to pass to `set_secure_cookie`." 

141 " See tornado's set_secure_cookie docs for details." 

142 ), 

143 ) 

144 

145 secure_cookie: bool | Bool[bool | None, bool | int | None] = Bool( 

146 None, 

147 allow_none=True, 

148 config=True, 

149 help=_i18n( 

150 "Specify whether login cookie should have the `secure` property (HTTPS-only)." 

151 "Only needed when protocol-detection gives the wrong answer due to proxies." 

152 ), 

153 ) 

154 

155 get_secure_cookie_kwargs = Dict( 

156 config=True, 

157 help=_i18n( 

158 "Extra keyword arguments to pass to `get_secure_cookie`." 

159 " See tornado's get_secure_cookie docs for details." 

160 ), 

161 ) 

162 

163 token: str | Unicode[str, str | bytes] = Unicode( 

164 "<generated>", 

165 help=_i18n( 

166 """Token used for authenticating first-time connections to the server. 

167 

168 The token can be read from the file referenced by JUPYTER_TOKEN_FILE or set directly 

169 with the JUPYTER_TOKEN environment variable. 

170 

171 When no password is enabled, 

172 the default is to generate a new, random token. 

173 

174 Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED. 

175 

176 Prior to 2.0: configured as ServerApp.token 

177 """ 

178 ), 

179 ).tag(config=True) 

180 

181 login_handler_class = Type( 

182 default_value="jupyter_server.auth.login.LoginFormHandler", 

183 klass=web.RequestHandler, 

184 config=True, 

185 help=_i18n("The login handler class to use, if any."), 

186 ) 

187 

188 logout_handler_class = Type( 

189 default_value="jupyter_server.auth.logout.LogoutHandler", 

190 klass=web.RequestHandler, 

191 config=True, 

192 help=_i18n("The logout handler class to use."), 

193 ) 

194 

195 # Define the fields that can be updated 

196 updatable_fields = List( 

197 trait=Enum(list(t.get_args(UpdatableField))), 

198 default_value=["color"], # Default updatable field 

199 config=True, 

200 help=_i18n("List of fields in the User model that can be updated."), 

201 ) 

202 

203 token_generated = False 

204 

205 @default("token") 

206 def _token_default(self): 

207 if os.getenv("JUPYTER_TOKEN"): 

208 self.token_generated = False 

209 return os.environ["JUPYTER_TOKEN"] 

210 if os.getenv("JUPYTER_TOKEN_FILE"): 

211 self.token_generated = False 

212 with open(os.environ["JUPYTER_TOKEN_FILE"]) as token_file: 

213 return token_file.read() 

214 if not self.need_token: 

215 # no token if password is enabled 

216 self.token_generated = False 

217 return "" 

218 else: 

219 self.token_generated = True 

220 return binascii.hexlify(os.urandom(24)).decode("ascii") 

221 

222 @validate("updatable_fields") 

223 def _validate_updatable_fields(self, proposal): 

224 """Validate that all fields in updatable_fields are valid.""" 

225 valid_updatable_fields = list(t.get_args(UpdatableField)) 

226 invalid_fields = [ 

227 field for field in proposal["value"] if field not in valid_updatable_fields 

228 ] 

229 if invalid_fields: 

230 msg = f"Invalid fields in updatable_fields: {invalid_fields}" 

231 raise TraitError(msg) 

232 return proposal["value"] 

233 

234 need_token: bool | Bool[bool, t.Union[bool, int]] = Bool(True) 

235 

236 def get_user(self, handler: web.RequestHandler) -> User | None | t.Awaitable[User | None]: 

237 """Get the authenticated user for a request 

238 

239 Must return a :class:`jupyter_server.auth.User`, 

240 though it may be a subclass. 

241 

242 Return None if the request is not authenticated. 

243 

244 _may_ be a coroutine 

245 """ 

246 return self._get_user(handler) 

247 

248 # not sure how to have optional-async type signature 

249 # on base class with `async def` without splitting it into two methods 

250 

251 async def _get_user(self, handler: web.RequestHandler) -> User | None: 

252 """Get the user.""" 

253 if getattr(handler, "_jupyter_current_user", None): 

254 # already authenticated 

255 return t.cast(User, handler._jupyter_current_user) # type:ignore[attr-defined] 

256 _token_user: User | None | t.Awaitable[User | None] = self.get_user_token(handler) 

257 if isinstance(_token_user, t.Awaitable): 

258 _token_user = await _token_user 

259 token_user: User | None = _token_user # need second variable name to collapse type 

260 _cookie_user = self.get_user_cookie(handler) 

261 if isinstance(_cookie_user, t.Awaitable): 

262 _cookie_user = await _cookie_user 

263 cookie_user: User | None = _cookie_user 

264 # prefer token to cookie if both given, 

265 # because token is always explicit 

266 user = token_user or cookie_user 

267 

268 if user is not None and token_user is not None: 

269 # if token-authenticated, persist user_id in cookie 

270 # if it hasn't already been stored there 

271 if user != cookie_user: 

272 self.set_login_cookie(handler, user) 

273 # Record that the current request has been authenticated with a token. 

274 # Used in is_token_authenticated above. 

275 handler._token_authenticated = True # type:ignore[attr-defined] 

276 

277 if user is None: 

278 # If an invalid cookie was sent, clear it to prevent unnecessary 

279 # extra warnings. But don't do this on a request with *no* cookie, 

280 # because that can erroneously log you out (see gh-3365) 

281 cookie_name = self.get_cookie_name(handler) 

282 cookie = handler.get_cookie(cookie_name) 

283 if cookie is not None: 

284 self.log.warning(f"Clearing invalid/expired login cookie {cookie_name}") 

285 self.clear_login_cookie(handler) 

286 if not self.auth_enabled: 

287 # Completely insecure! No authentication at all. 

288 # No need to warn here, though; validate_security will have already done that. 

289 user = self.generate_anonymous_user(handler) 

290 # persist user on first request 

291 # so the user data is stable for a given browser session 

292 self.set_login_cookie(handler, user) 

293 

294 return user 

295 

296 def update_user( 

297 self, handler: web.RequestHandler, user_data: dict[UpdatableField, str] 

298 ) -> User: 

299 """Update user information and persist the user model.""" 

300 self.check_update(user_data) 

301 current_user = t.cast(User, handler.current_user) 

302 updated_user = self.update_user_model(current_user, user_data) 

303 self.persist_user_model(handler) 

304 return updated_user 

305 

306 def check_update(self, user_data: dict[UpdatableField, str]) -> None: 

307 """Raises if some fields to update are not updatable.""" 

308 for field in user_data: 

309 if field not in self.updatable_fields: 

310 msg = f"Field {field} is not updatable" 

311 raise ValueError(msg) 

312 

313 def update_user_model(self, current_user: User, user_data: dict[UpdatableField, str]) -> User: 

314 """Update user information.""" 

315 raise NotImplementedError 

316 

317 def persist_user_model(self, handler: web.RequestHandler) -> None: 

318 """Persist the user model (i.e. a cookie).""" 

319 raise NotImplementedError 

320 

321 def identity_model(self, user: User) -> dict[str, t.Any]: 

322 """Return a User as an Identity model""" 

323 # TODO: validate? 

324 return asdict(user) 

325 

326 def get_handlers(self) -> list[tuple[str, object]]: 

327 """Return list of additional handlers for this identity provider 

328 

329 For example, an OAuth callback handler. 

330 """ 

331 handlers = [] 

332 if self.login_available: 

333 handlers.append((r"/login", self.login_handler_class)) 

334 if self.logout_available: 

335 handlers.append((r"/logout", self.logout_handler_class)) 

336 return handlers 

337 

338 def user_to_cookie(self, user: User) -> str: 

339 """Serialize a user to a string for storage in a cookie 

340 

341 If overriding in a subclass, make sure to define user_from_cookie as well. 

342 

343 Default is just the user's username. 

344 """ 

345 # default: username is enough 

346 cookie = json.dumps( 

347 { 

348 "username": user.username, 

349 "name": user.name, 

350 "display_name": user.display_name, 

351 "initials": user.initials, 

352 "color": user.color, 

353 } 

354 ) 

355 return cookie 

356 

357 def user_from_cookie(self, cookie_value: str) -> User | None: 

358 """Inverse of user_to_cookie""" 

359 user = json.loads(cookie_value) 

360 return User( 

361 user["username"], 

362 user["name"], 

363 user["display_name"], 

364 user["initials"], 

365 None, 

366 user["color"], 

367 ) 

368 

369 def get_cookie_name(self, handler: web.RequestHandler) -> str: 

370 """Return the login cookie name 

371 

372 Uses IdentityProvider.cookie_name, if defined. 

373 Default is to generate a string taking host into account to avoid 

374 collisions for multiple servers on one hostname with different ports. 

375 """ 

376 if self.cookie_name: 

377 return self.cookie_name 

378 else: 

379 return _non_alphanum.sub("-", f"username-{handler.request.host}") 

380 

381 def set_login_cookie(self, handler: web.RequestHandler, user: User) -> None: 

382 """Call this on handlers to set the login cookie for success""" 

383 cookie_options = {} 

384 cookie_options.update(self.cookie_options) 

385 cookie_options.setdefault("httponly", True) 

386 # tornado <4.2 has a bug that considers secure==True as soon as 

387 # 'secure' kwarg is passed to set_secure_cookie 

388 secure_cookie = self.secure_cookie 

389 if secure_cookie is None: 

390 secure_cookie = handler.request.protocol == "https" 

391 if secure_cookie: 

392 cookie_options.setdefault("secure", True) 

393 cookie_options.setdefault("path", handler.base_url) # type:ignore[attr-defined] 

394 cookie_name = self.get_cookie_name(handler) 

395 handler.set_secure_cookie(cookie_name, self.user_to_cookie(user), **cookie_options) 

396 

397 def _force_clear_cookie( 

398 self, handler: web.RequestHandler, name: str, path: str = "/", domain: str | None = None 

399 ) -> None: 

400 """Deletes the cookie with the given name. 

401 

402 Tornado's cookie handling currently (Jan 2018) stores cookies in a dict 

403 keyed by name, so it can only modify one cookie with a given name per 

404 response. The browser can store multiple cookies with the same name 

405 but different domains and/or paths. This method lets us clear multiple 

406 cookies with the same name. 

407 

408 Due to limitations of the cookie protocol, you must pass the same 

409 path and domain to clear a cookie as were used when that cookie 

410 was set (but there is no way to find out on the server side 

411 which values were used for a given cookie). 

412 """ 

413 name = escape.native_str(name) 

414 expires = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=365) 

415 

416 morsel: Morsel[t.Any] = Morsel() 

417 morsel.set(name, "", '""') 

418 morsel["expires"] = httputil.format_timestamp(expires) 

419 morsel["path"] = path 

420 if domain: 

421 morsel["domain"] = domain 

422 handler.add_header("Set-Cookie", morsel.OutputString()) 

423 

424 def clear_login_cookie(self, handler: web.RequestHandler) -> None: 

425 """Clear the login cookie, effectively logging out the session.""" 

426 cookie_options = {} 

427 cookie_options.update(self.cookie_options) 

428 path = cookie_options.setdefault("path", handler.base_url) # type:ignore[attr-defined] 

429 cookie_name = self.get_cookie_name(handler) 

430 handler.clear_cookie(cookie_name, path=path) 

431 if path and path != "/": 

432 # also clear cookie on / to ensure old cookies are cleared 

433 # after the change in path behavior. 

434 # N.B. This bypasses the normal cookie handling, which can't update 

435 # two cookies with the same name. See the method above. 

436 self._force_clear_cookie(handler, cookie_name) 

437 

438 def get_user_cookie( 

439 self, handler: web.RequestHandler 

440 ) -> User | None | t.Awaitable[User | None]: 

441 """Get user from a cookie 

442 

443 Calls user_from_cookie to deserialize cookie value 

444 """ 

445 _user_cookie = handler.get_secure_cookie( 

446 self.get_cookie_name(handler), 

447 **self.get_secure_cookie_kwargs, 

448 ) 

449 if not _user_cookie: 

450 return None 

451 user_cookie = _user_cookie.decode() 

452 # TODO: try/catch in case of change in config? 

453 try: 

454 return self.user_from_cookie(user_cookie) 

455 except Exception as e: 

456 # log bad cookie itself, only at debug-level 

457 self.log.debug(f"Error unpacking user from cookie: cookie={user_cookie}", exc_info=True) 

458 self.log.error(f"Error unpacking user from cookie: {e}") 

459 return None 

460 

461 auth_header_pat = re.compile(r"(token|bearer)\s+(.+)", re.IGNORECASE) 

462 

463 def get_token(self, handler: web.RequestHandler) -> str | None: 

464 """Get the user token from a request 

465 

466 Default: 

467 

468 - in URL parameters: ?token=<token> 

469 - in header: Authorization: token <token> 

470 """ 

471 user_token = handler.get_argument("token", "") 

472 if not user_token: 

473 # get it from Authorization header 

474 m = self.auth_header_pat.match(handler.request.headers.get("Authorization", "")) 

475 if m: 

476 user_token = m.group(2) 

477 return user_token 

478 

479 async def get_user_token(self, handler: web.RequestHandler) -> User | None: 

480 """Identify the user based on a token in the URL or Authorization header 

481 

482 Returns: 

483 - uuid if authenticated 

484 - None if not 

485 """ 

486 token = t.cast("str | None", handler.token) # type:ignore[attr-defined] 

487 if not token: 

488 return None 

489 # check login token from URL argument or Authorization header 

490 user_token = self.get_token(handler) 

491 authenticated = False 

492 if user_token == token: 

493 # token-authenticated, set the login cookie 

494 self.log.debug( 

495 "Accepting token-authenticated request from %s", 

496 handler.request.remote_ip, 

497 ) 

498 authenticated = True 

499 

500 if authenticated: 

501 # token does not correspond to user-id, 

502 # which is stored in a cookie. 

503 # still check the cookie for the user id 

504 _user = self.get_user_cookie(handler) 

505 if isinstance(_user, t.Awaitable): 

506 _user = await _user 

507 user: User | None = _user 

508 if user is None: 

509 user = self.generate_anonymous_user(handler) 

510 return user 

511 else: 

512 return None 

513 

514 def generate_anonymous_user(self, handler: web.RequestHandler) -> User: 

515 """Generate a random anonymous user. 

516 

517 For use when a single shared token is used, 

518 but does not identify a user. 

519 """ 

520 user_id = uuid.uuid4().hex 

521 moon = get_anonymous_username() 

522 name = display_name = f"Anonymous {moon}" 

523 initials = f"A{moon[0]}" 

524 color = None 

525 handler.log.debug(f"Generating new user for token-authenticated request: {user_id}") # type:ignore[attr-defined] 

526 return User(user_id, name, display_name, initials, None, color) 

527 

528 def should_check_origin(self, handler: web.RequestHandler) -> bool: 

529 """Should the Handler check for CORS origin validation? 

530 

531 Origin check should be skipped for token-authenticated requests. 

532 

533 Returns: 

534 - True, if Handler must check for valid CORS origin. 

535 - False, if Handler should skip origin check since requests are token-authenticated. 

536 """ 

537 return not self.is_token_authenticated(handler) 

538 

539 def is_token_authenticated(self, handler: web.RequestHandler) -> bool: 

540 """Returns True if handler has been token authenticated. Otherwise, False. 

541 

542 Login with a token is used to signal certain things, such as: 

543 

544 - permit access to REST API 

545 - xsrf protection 

546 - skip origin-checks for scripts 

547 """ 

548 # ensure get_user has been called, so we know if we're token-authenticated 

549 handler.current_user # noqa: B018 

550 return getattr(handler, "_token_authenticated", False) 

551 

552 def validate_security( 

553 self, 

554 app: t.Any, 

555 ssl_options: dict[str, t.Any] | None = None, 

556 ) -> None: 

557 """Check the application's security. 

558 

559 Show messages, or abort if necessary, based on the security configuration. 

560 """ 

561 if not app.ip: 

562 warning = "WARNING: The Jupyter server is listening on all IP addresses" 

563 if ssl_options is None: 

564 app.log.warning(f"{warning} and not using encryption. This is not recommended.") 

565 if not self.auth_enabled: 

566 app.log.warning( 

567 f"{warning} and not using authentication. " 

568 "This is highly insecure and not recommended." 

569 ) 

570 elif not self.auth_enabled: 

571 app.log.warning( 

572 "All authentication is disabled." 

573 " Anyone who can connect to this server will be able to run code." 

574 ) 

575 

576 def process_login_form(self, handler: web.RequestHandler) -> User | None: 

577 """Process login form data 

578 

579 Return authenticated User if successful, None if not. 

580 """ 

581 typed_password = handler.get_argument("password", default="") 

582 user = None 

583 if not self.auth_enabled: 

584 self.log.warning("Accepting anonymous login because auth fully disabled!") 

585 return self.generate_anonymous_user(handler) 

586 

587 if self.token and self.token == typed_password: 

588 return t.cast(User, self.user_for_token(typed_password)) # type:ignore[attr-defined] 

589 

590 return user 

591 

592 @property 

593 def auth_enabled(self): 

594 """Is authentication enabled? 

595 

596 Should always be True, but may be False in rare, insecure cases 

597 where requests with no auth are allowed. 

598 

599 Previously: LoginHandler.get_login_available 

600 """ 

601 return True 

602 

603 @property 

604 def login_available(self): 

605 """Whether a LoginHandler is needed - and therefore whether the login page should be displayed.""" 

606 return self.auth_enabled 

607 

608 @property 

609 def logout_available(self): 

610 """Whether a LogoutHandler is needed.""" 

611 return True 

612 

613 

614class PasswordIdentityProvider(IdentityProvider): 

615 """A password identity provider.""" 

616 

617 hashed_password = Unicode( 

618 "", 

619 config=True, 

620 help=_i18n( 

621 """ 

622 Hashed password to use for web authentication. 

623 

624 To generate, type in a python/IPython shell: 

625 

626 from jupyter_server.auth import passwd; passwd() 

627 

628 The string should be of the form type:salt:hashed-password. 

629 """ 

630 ), 

631 ) 

632 

633 password_required = Bool( 

634 False, 

635 config=True, 

636 help=_i18n( 

637 """ 

638 Forces users to use a password for the Jupyter server. 

639 This is useful in a multi user environment, for instance when 

640 everybody in the LAN can access each other's machine through ssh. 

641 

642 In such a case, serving on localhost is not secure since 

643 any user can connect to the Jupyter server via ssh. 

644 

645 """ 

646 ), 

647 ) 

648 

649 allow_password_change = Bool( 

650 True, 

651 config=True, 

652 help=_i18n( 

653 """ 

654 Allow password to be changed at login for the Jupyter server. 

655 

656 While logging in with a token, the Jupyter server UI will give the opportunity to 

657 the user to enter a new password at the same time that will replace 

658 the token login mechanism. 

659 

660 This can be set to False to prevent changing password from the UI/API. 

661 """ 

662 ), 

663 ) 

664 

665 @default("need_token") 

666 def _need_token_default(self): 

667 return not bool(self.hashed_password) 

668 

669 @default("updatable_fields") 

670 def _default_updatable_fields(self): 

671 return [ 

672 "name", 

673 "display_name", 

674 "initials", 

675 "avatar_url", 

676 "color", 

677 ] 

678 

679 @property 

680 def login_available(self) -> bool: 

681 """Whether a LoginHandler is needed - and therefore whether the login page should be displayed.""" 

682 return self.auth_enabled 

683 

684 @property 

685 def auth_enabled(self) -> bool: 

686 """Return whether any auth is enabled""" 

687 return bool(self.hashed_password or self.token) 

688 

689 def update_user_model(self, current_user: User, user_data: dict[UpdatableField, str]) -> User: 

690 """Update user information.""" 

691 for field in self.updatable_fields: 

692 if field in user_data: 

693 setattr(current_user, field, user_data[field]) 

694 return current_user 

695 

696 def persist_user_model(self, handler: web.RequestHandler) -> None: 

697 """Persist the user model to a cookie.""" 

698 self.set_login_cookie(handler, handler.current_user) 

699 

700 def passwd_check(self, password): 

701 """Check password against our stored hashed password""" 

702 return passwd_check(self.hashed_password, password) 

703 

704 def process_login_form(self, handler: web.RequestHandler) -> User | None: 

705 """Process login form data 

706 

707 Return authenticated User if successful, None if not. 

708 """ 

709 typed_password = handler.get_argument("password", default="") 

710 new_password = handler.get_argument("new_password", default="") 

711 user = None 

712 if not self.auth_enabled: 

713 self.log.warning("Accepting anonymous login because auth fully disabled!") 

714 return self.generate_anonymous_user(handler) 

715 

716 if self.passwd_check(typed_password) and not new_password: 

717 return self.generate_anonymous_user(handler) 

718 elif self.token and self.token == typed_password: 

719 user = self.generate_anonymous_user(handler) 

720 if new_password and self.allow_password_change: 

721 config_dir = handler.settings.get("config_dir", "") 

722 config_file = os.path.join(config_dir, "jupyter_server_config.json") 

723 self.hashed_password = set_password(new_password, config_file=config_file) 

724 self.log.info(_i18n("Wrote hashed password to {file}").format(file=config_file)) 

725 

726 return user 

727 

728 def validate_security( 

729 self, 

730 app: t.Any, 

731 ssl_options: dict[str, t.Any] | None = None, 

732 ) -> None: 

733 """Handle security validation.""" 

734 super().validate_security(app, ssl_options) 

735 if self.password_required and (not self.hashed_password): 

736 self.log.critical( 

737 _i18n("Jupyter servers are configured to only be run with a password.") 

738 ) 

739 self.log.critical(_i18n("Hint: run the following command to set a password")) 

740 self.log.critical(_i18n("\t$ python -m jupyter_server.auth password")) 

741 sys.exit(1) 

742 

743 

744class LegacyIdentityProvider(PasswordIdentityProvider): 

745 """Legacy IdentityProvider for use with custom LoginHandlers 

746 

747 Login configuration has moved from LoginHandler to IdentityProvider 

748 in Jupyter Server 2.0. 

749 """ 

750 

751 # settings must be passed for 

752 settings = Dict() 

753 

754 @default("settings") 

755 def _default_settings(self): 

756 return { 

757 "token": self.token, 

758 "password": self.hashed_password, 

759 } 

760 

761 @default("login_handler_class") 

762 def _default_login_handler_class(self): 

763 from .login import LegacyLoginHandler 

764 

765 return LegacyLoginHandler 

766 

767 @property 

768 def auth_enabled(self): 

769 return self.login_available 

770 

771 def get_user(self, handler: web.RequestHandler) -> User | None: 

772 """Get the user.""" 

773 user = self.login_handler_class.get_user(handler) # type:ignore[attr-defined] 

774 if user is None: 

775 return None 

776 return _backward_compat_user(user) 

777 

778 @property 

779 def login_available(self) -> bool: 

780 return bool( 

781 self.login_handler_class.get_login_available( # type:ignore[attr-defined] 

782 self.settings 

783 ) 

784 ) 

785 

786 def should_check_origin(self, handler: web.RequestHandler) -> bool: 

787 """Whether we should check origin.""" 

788 return bool(self.login_handler_class.should_check_origin(handler)) # type:ignore[attr-defined] 

789 

790 def is_token_authenticated(self, handler: web.RequestHandler) -> bool: 

791 """Whether we are token authenticated.""" 

792 return bool(self.login_handler_class.is_token_authenticated(handler)) # type:ignore[attr-defined] 

793 

794 def validate_security( 

795 self, 

796 app: t.Any, 

797 ssl_options: dict[str, t.Any] | None = None, 

798 ) -> None: 

799 """Validate security.""" 

800 if self.password_required and (not self.hashed_password): 

801 self.log.critical( 

802 _i18n("Jupyter servers are configured to only be run with a password.") 

803 ) 

804 self.log.critical(_i18n("Hint: run the following command to set a password")) 

805 self.log.critical(_i18n("\t$ python -m jupyter_server.auth password")) 

806 sys.exit(1) 

807 self.login_handler_class.validate_security( # type:ignore[attr-defined] 

808 app, ssl_options 

809 )