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
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
1"""Identity Provider interface
3This defines the _authentication_ layer of Jupyter Server,
4to be used in combination with Authorizer for _authorization_.
6.. versionadded:: 2.0
7"""
9from __future__ import annotations
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
22from tornado import escape, httputil, web
23from traitlets import Bool, Dict, Enum, List, TraitError, Type, Unicode, default, validate
24from traitlets.config import LoggingConfigurable
26from jupyter_server.transutils import _i18n
28from .security import passwd_check, set_password
29from .utils import get_anonymous_username
31_non_alphanum = re.compile(r"[^A-Za-z0-9]")
34# Define the User properties that can be updated
35UpdatableField = t.Literal["name", "display_name", "initials", "avatar_url", "color"]
38@dataclass
39class User:
40 """Object representing a User
42 This or a subclass should be returned from IdentityProvider.get_user
43 """
45 username: str # the only truly required field
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 = ""
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
59 # TODO: extension fields?
60 # ext: Dict[str, Dict[str, Any]] = field(default_factory=dict)
62 def __post_init__(self):
63 self.fill_defaults()
65 def fill_defaults(self):
66 """Fill out default fields in the identity model
68 - Ensures all values are defined
69 - Fills out derivative values for name fields fields
70 - Fills out null values for optional fields
71 """
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)
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
85def _backward_compat_user(got_user: t.Any) -> User:
86 """Backward-compatibility for LoginHandler.get_user
88 Prior to 2.0, LoginHandler.get_user could return anything truthy.
90 Typically, this was either a simple string username,
91 or a simple dict.
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)
114class IdentityProvider(LoggingConfigurable):
115 """
116 Interface for providing identity management and authentication.
118 Two principle methods:
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.
126 Additional methods can customize authentication.
128 .. versionadded:: 2.0
129 """
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 )
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 )
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 )
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 )
163 token: str | Unicode[str, str | bytes] = Unicode(
164 "<generated>",
165 help=_i18n(
166 """Token used for authenticating first-time connections to the server.
168 The token can be read from the file referenced by JUPYTER_TOKEN_FILE or set directly
169 with the JUPYTER_TOKEN environment variable.
171 When no password is enabled,
172 the default is to generate a new, random token.
174 Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.
176 Prior to 2.0: configured as ServerApp.token
177 """
178 ),
179 ).tag(config=True)
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 )
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 )
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 )
203 token_generated = False
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")
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"]
234 need_token: bool | Bool[bool, t.Union[bool, int]] = Bool(True)
236 def get_user(self, handler: web.RequestHandler) -> User | None | t.Awaitable[User | None]:
237 """Get the authenticated user for a request
239 Must return a :class:`jupyter_server.auth.User`,
240 though it may be a subclass.
242 Return None if the request is not authenticated.
244 _may_ be a coroutine
245 """
246 return self._get_user(handler)
248 # not sure how to have optional-async type signature
249 # on base class with `async def` without splitting it into two methods
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
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]
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)
294 return user
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
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)
313 def update_user_model(self, current_user: User, user_data: dict[UpdatableField, str]) -> User:
314 """Update user information."""
315 raise NotImplementedError
317 def persist_user_model(self, handler: web.RequestHandler) -> None:
318 """Persist the user model (i.e. a cookie)."""
319 raise NotImplementedError
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)
326 def get_handlers(self) -> list[tuple[str, object]]:
327 """Return list of additional handlers for this identity provider
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
338 def user_to_cookie(self, user: User) -> str:
339 """Serialize a user to a string for storage in a cookie
341 If overriding in a subclass, make sure to define user_from_cookie as well.
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
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 )
369 def get_cookie_name(self, handler: web.RequestHandler) -> str:
370 """Return the login cookie name
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}")
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)
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.
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.
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)
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())
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)
438 def get_user_cookie(
439 self, handler: web.RequestHandler
440 ) -> User | None | t.Awaitable[User | None]:
441 """Get user from a cookie
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
461 auth_header_pat = re.compile(r"(token|bearer)\s+(.+)", re.IGNORECASE)
463 def get_token(self, handler: web.RequestHandler) -> str | None:
464 """Get the user token from a request
466 Default:
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
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
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
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
514 def generate_anonymous_user(self, handler: web.RequestHandler) -> User:
515 """Generate a random anonymous user.
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)
528 def should_check_origin(self, handler: web.RequestHandler) -> bool:
529 """Should the Handler check for CORS origin validation?
531 Origin check should be skipped for token-authenticated requests.
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)
539 def is_token_authenticated(self, handler: web.RequestHandler) -> bool:
540 """Returns True if handler has been token authenticated. Otherwise, False.
542 Login with a token is used to signal certain things, such as:
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)
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.
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 )
576 def process_login_form(self, handler: web.RequestHandler) -> User | None:
577 """Process login form data
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)
587 if self.token and self.token == typed_password:
588 return t.cast(User, self.user_for_token(typed_password)) # type:ignore[attr-defined]
590 return user
592 @property
593 def auth_enabled(self):
594 """Is authentication enabled?
596 Should always be True, but may be False in rare, insecure cases
597 where requests with no auth are allowed.
599 Previously: LoginHandler.get_login_available
600 """
601 return True
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
608 @property
609 def logout_available(self):
610 """Whether a LogoutHandler is needed."""
611 return True
614class PasswordIdentityProvider(IdentityProvider):
615 """A password identity provider."""
617 hashed_password = Unicode(
618 "",
619 config=True,
620 help=_i18n(
621 """
622 Hashed password to use for web authentication.
624 To generate, type in a python/IPython shell:
626 from jupyter_server.auth import passwd; passwd()
628 The string should be of the form type:salt:hashed-password.
629 """
630 ),
631 )
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.
642 In such a case, serving on localhost is not secure since
643 any user can connect to the Jupyter server via ssh.
645 """
646 ),
647 )
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.
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.
660 This can be set to False to prevent changing password from the UI/API.
661 """
662 ),
663 )
665 @default("need_token")
666 def _need_token_default(self):
667 return not bool(self.hashed_password)
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 ]
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
684 @property
685 def auth_enabled(self) -> bool:
686 """Return whether any auth is enabled"""
687 return bool(self.hashed_password or self.token)
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
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)
700 def passwd_check(self, password):
701 """Check password against our stored hashed password"""
702 return passwd_check(self.hashed_password, password)
704 def process_login_form(self, handler: web.RequestHandler) -> User | None:
705 """Process login form data
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)
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))
726 return user
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)
744class LegacyIdentityProvider(PasswordIdentityProvider):
745 """Legacy IdentityProvider for use with custom LoginHandlers
747 Login configuration has moved from LoginHandler to IdentityProvider
748 in Jupyter Server 2.0.
749 """
751 # settings must be passed for
752 settings = Dict()
754 @default("settings")
755 def _default_settings(self):
756 return {
757 "token": self.token,
758 "password": self.hashed_password,
759 }
761 @default("login_handler_class")
762 def _default_login_handler_class(self):
763 from .login import LegacyLoginHandler
765 return LegacyLoginHandler
767 @property
768 def auth_enabled(self):
769 return self.login_available
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)
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 )
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]
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]
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 )