identity.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. """Identity Provider interface
  2. This defines the _authentication_ layer of Jupyter Server,
  3. to be used in combination with Authorizer for _authorization_.
  4. .. versionadded:: 2.0
  5. """
  6. from __future__ import annotations
  7. import binascii
  8. import datetime
  9. import json
  10. import os
  11. import re
  12. import sys
  13. import typing as t
  14. import uuid
  15. from dataclasses import asdict, dataclass
  16. from http.cookies import Morsel
  17. from tornado import escape, httputil, web
  18. from traitlets import Bool, Dict, Enum, List, TraitError, Type, Unicode, default, validate
  19. from traitlets.config import LoggingConfigurable
  20. from jupyter_server.transutils import _i18n
  21. from .security import passwd_check, set_password
  22. from .utils import get_anonymous_username
  23. _non_alphanum = re.compile(r"[^A-Za-z0-9]")
  24. # Define the User properties that can be updated
  25. UpdatableField = t.Literal["name", "display_name", "initials", "avatar_url", "color"]
  26. @dataclass
  27. class User:
  28. """Object representing a User
  29. This or a subclass should be returned from IdentityProvider.get_user
  30. """
  31. username: str # the only truly required field
  32. # these fields are filled from username if not specified
  33. # name is the 'real' name of the user
  34. name: str = ""
  35. # display_name is a shorter name for us in UI,
  36. # if different from name. e.g. a nickname
  37. display_name: str = ""
  38. # these fields are left as None if undefined
  39. initials: str | None = None
  40. avatar_url: str | None = None
  41. color: str | None = None
  42. # TODO: extension fields?
  43. # ext: Dict[str, Dict[str, Any]] = field(default_factory=dict)
  44. def __post_init__(self):
  45. self.fill_defaults()
  46. def fill_defaults(self):
  47. """Fill out default fields in the identity model
  48. - Ensures all values are defined
  49. - Fills out derivative values for name fields fields
  50. - Fills out null values for optional fields
  51. """
  52. # username is the only truly required field
  53. if not self.username:
  54. msg = f"user.username must not be empty: {self}"
  55. raise ValueError(msg)
  56. # derive name fields from username -> name -> display name
  57. if not self.name:
  58. self.name = self.username
  59. if not self.display_name:
  60. self.display_name = self.name
  61. def _backward_compat_user(got_user: t.Any) -> User:
  62. """Backward-compatibility for LoginHandler.get_user
  63. Prior to 2.0, LoginHandler.get_user could return anything truthy.
  64. Typically, this was either a simple string username,
  65. or a simple dict.
  66. Make some effort to allow common patterns to keep working.
  67. """
  68. if isinstance(got_user, str):
  69. return User(username=got_user)
  70. elif isinstance(got_user, dict):
  71. kwargs = {}
  72. if "username" not in got_user and "name" in got_user:
  73. kwargs["username"] = got_user["name"]
  74. for field in User.__dataclass_fields__:
  75. if field in got_user:
  76. kwargs[field] = got_user[field]
  77. try:
  78. return User(**kwargs)
  79. except TypeError:
  80. msg = f"Unrecognized user: {got_user}"
  81. raise ValueError(msg) from None
  82. else:
  83. msg = f"Unrecognized user: {got_user}"
  84. raise ValueError(msg)
  85. class IdentityProvider(LoggingConfigurable):
  86. """
  87. Interface for providing identity management and authentication.
  88. Two principle methods:
  89. - :meth:`~jupyter_server.auth.IdentityProvider.get_user` returns a :class:`~.User` object
  90. for successful authentication, or None for no-identity-found.
  91. - :meth:`~jupyter_server.auth.IdentityProvider.identity_model` turns a :class:`~jupyter_server.auth.User` into a JSONable dict.
  92. The default is to use :py:meth:`dataclasses.asdict`,
  93. and usually shouldn't need override.
  94. Additional methods can customize authentication.
  95. .. versionadded:: 2.0
  96. """
  97. cookie_name: str | Unicode[str, str | bytes] = Unicode(
  98. "",
  99. config=True,
  100. help=_i18n("Name of the cookie to set for persisting login. Default: username-${Host}."),
  101. )
  102. cookie_options = Dict(
  103. config=True,
  104. help=_i18n(
  105. "Extra keyword arguments to pass to `set_secure_cookie`."
  106. " See tornado's set_secure_cookie docs for details."
  107. ),
  108. )
  109. secure_cookie: bool | Bool[bool | None, bool | int | None] = Bool(
  110. None,
  111. allow_none=True,
  112. config=True,
  113. help=_i18n(
  114. "Specify whether login cookie should have the `secure` property (HTTPS-only)."
  115. "Only needed when protocol-detection gives the wrong answer due to proxies."
  116. ),
  117. )
  118. get_secure_cookie_kwargs = Dict(
  119. config=True,
  120. help=_i18n(
  121. "Extra keyword arguments to pass to `get_secure_cookie`."
  122. " See tornado's get_secure_cookie docs for details."
  123. ),
  124. )
  125. token: str | Unicode[str, str | bytes] = Unicode(
  126. "<generated>",
  127. help=_i18n(
  128. """Token used for authenticating first-time connections to the server.
  129. The token can be read from the file referenced by JUPYTER_TOKEN_FILE or set directly
  130. with the JUPYTER_TOKEN environment variable.
  131. When no password is enabled,
  132. the default is to generate a new, random token.
  133. Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.
  134. Prior to 2.0: configured as ServerApp.token
  135. """
  136. ),
  137. ).tag(config=True)
  138. login_handler_class = Type(
  139. default_value="jupyter_server.auth.login.LoginFormHandler",
  140. klass=web.RequestHandler,
  141. config=True,
  142. help=_i18n("The login handler class to use, if any."),
  143. )
  144. logout_handler_class = Type(
  145. default_value="jupyter_server.auth.logout.LogoutHandler",
  146. klass=web.RequestHandler,
  147. config=True,
  148. help=_i18n("The logout handler class to use."),
  149. )
  150. # Define the fields that can be updated
  151. updatable_fields = List(
  152. trait=Enum(list(t.get_args(UpdatableField))),
  153. default_value=["color"], # Default updatable field
  154. config=True,
  155. help=_i18n("List of fields in the User model that can be updated."),
  156. )
  157. token_generated = False
  158. @default("token")
  159. def _token_default(self):
  160. if os.getenv("JUPYTER_TOKEN"):
  161. self.token_generated = False
  162. return os.environ["JUPYTER_TOKEN"]
  163. if os.getenv("JUPYTER_TOKEN_FILE"):
  164. self.token_generated = False
  165. with open(os.environ["JUPYTER_TOKEN_FILE"]) as token_file:
  166. return token_file.read()
  167. if not self.need_token:
  168. # no token if password is enabled
  169. self.token_generated = False
  170. return ""
  171. else:
  172. self.token_generated = True
  173. return binascii.hexlify(os.urandom(24)).decode("ascii")
  174. @validate("updatable_fields")
  175. def _validate_updatable_fields(self, proposal):
  176. """Validate that all fields in updatable_fields are valid."""
  177. valid_updatable_fields = list(t.get_args(UpdatableField))
  178. invalid_fields = [
  179. field for field in proposal["value"] if field not in valid_updatable_fields
  180. ]
  181. if invalid_fields:
  182. msg = f"Invalid fields in updatable_fields: {invalid_fields}"
  183. raise TraitError(msg)
  184. return proposal["value"]
  185. need_token: bool | Bool[bool, t.Union[bool, int]] = Bool(True)
  186. def get_user(self, handler: web.RequestHandler) -> User | None | t.Awaitable[User | None]:
  187. """Get the authenticated user for a request
  188. Must return a :class:`jupyter_server.auth.User`,
  189. though it may be a subclass.
  190. Return None if the request is not authenticated.
  191. _may_ be a coroutine
  192. """
  193. return self._get_user(handler)
  194. # not sure how to have optional-async type signature
  195. # on base class with `async def` without splitting it into two methods
  196. async def _get_user(self, handler: web.RequestHandler) -> User | None:
  197. """Get the user."""
  198. if getattr(handler, "_jupyter_current_user", None):
  199. # already authenticated
  200. return t.cast(User, handler._jupyter_current_user) # type:ignore[attr-defined]
  201. _token_user: User | None | t.Awaitable[User | None] = self.get_user_token(handler)
  202. if isinstance(_token_user, t.Awaitable):
  203. _token_user = await _token_user
  204. token_user: User | None = _token_user # need second variable name to collapse type
  205. _cookie_user = self.get_user_cookie(handler)
  206. if isinstance(_cookie_user, t.Awaitable):
  207. _cookie_user = await _cookie_user
  208. cookie_user: User | None = _cookie_user
  209. # prefer token to cookie if both given,
  210. # because token is always explicit
  211. user = token_user or cookie_user
  212. if user is not None and token_user is not None:
  213. # if token-authenticated, persist user_id in cookie
  214. # if it hasn't already been stored there
  215. if user != cookie_user:
  216. self.set_login_cookie(handler, user)
  217. # Record that the current request has been authenticated with a token.
  218. # Used in is_token_authenticated above.
  219. handler._token_authenticated = True # type:ignore[attr-defined]
  220. if user is None:
  221. # If an invalid cookie was sent, clear it to prevent unnecessary
  222. # extra warnings. But don't do this on a request with *no* cookie,
  223. # because that can erroneously log you out (see gh-3365)
  224. cookie_name = self.get_cookie_name(handler)
  225. cookie = handler.get_cookie(cookie_name)
  226. if cookie is not None:
  227. self.log.warning(f"Clearing invalid/expired login cookie {cookie_name}")
  228. self.clear_login_cookie(handler)
  229. if not self.auth_enabled:
  230. # Completely insecure! No authentication at all.
  231. # No need to warn here, though; validate_security will have already done that.
  232. user = self.generate_anonymous_user(handler)
  233. # persist user on first request
  234. # so the user data is stable for a given browser session
  235. self.set_login_cookie(handler, user)
  236. return user
  237. def update_user(
  238. self, handler: web.RequestHandler, user_data: dict[UpdatableField, str]
  239. ) -> User:
  240. """Update user information and persist the user model."""
  241. self.check_update(user_data)
  242. current_user = t.cast(User, handler.current_user)
  243. updated_user = self.update_user_model(current_user, user_data)
  244. self.persist_user_model(handler)
  245. return updated_user
  246. def check_update(self, user_data: dict[UpdatableField, str]) -> None:
  247. """Raises if some fields to update are not updatable."""
  248. for field in user_data:
  249. if field not in self.updatable_fields:
  250. msg = f"Field {field} is not updatable"
  251. raise ValueError(msg)
  252. def update_user_model(self, current_user: User, user_data: dict[UpdatableField, str]) -> User:
  253. """Update user information."""
  254. raise NotImplementedError
  255. def persist_user_model(self, handler: web.RequestHandler) -> None:
  256. """Persist the user model (i.e. a cookie)."""
  257. raise NotImplementedError
  258. def identity_model(self, user: User) -> dict[str, t.Any]:
  259. """Return a User as an Identity model"""
  260. # TODO: validate?
  261. return asdict(user)
  262. def get_handlers(self) -> list[tuple[str, object]]:
  263. """Return list of additional handlers for this identity provider
  264. For example, an OAuth callback handler.
  265. """
  266. handlers = []
  267. if self.login_available:
  268. handlers.append((r"/login", self.login_handler_class))
  269. if self.logout_available:
  270. handlers.append((r"/logout", self.logout_handler_class))
  271. return handlers
  272. def user_to_cookie(self, user: User) -> str:
  273. """Serialize a user to a string for storage in a cookie
  274. If overriding in a subclass, make sure to define user_from_cookie as well.
  275. Default is just the user's username.
  276. """
  277. # default: username is enough
  278. cookie = json.dumps(
  279. {
  280. "username": user.username,
  281. "name": user.name,
  282. "display_name": user.display_name,
  283. "initials": user.initials,
  284. "color": user.color,
  285. }
  286. )
  287. return cookie
  288. def user_from_cookie(self, cookie_value: str) -> User | None:
  289. """Inverse of user_to_cookie"""
  290. user = json.loads(cookie_value)
  291. return User(
  292. user["username"],
  293. user["name"],
  294. user["display_name"],
  295. user["initials"],
  296. None,
  297. user["color"],
  298. )
  299. def get_cookie_name(self, handler: web.RequestHandler) -> str:
  300. """Return the login cookie name
  301. Uses IdentityProvider.cookie_name, if defined.
  302. Default is to generate a string taking host into account to avoid
  303. collisions for multiple servers on one hostname with different ports.
  304. """
  305. if self.cookie_name:
  306. return self.cookie_name
  307. else:
  308. return _non_alphanum.sub("-", f"username-{handler.request.host}")
  309. def set_login_cookie(self, handler: web.RequestHandler, user: User) -> None:
  310. """Call this on handlers to set the login cookie for success"""
  311. cookie_options = {}
  312. cookie_options.update(self.cookie_options)
  313. cookie_options.setdefault("httponly", True)
  314. # tornado <4.2 has a bug that considers secure==True as soon as
  315. # 'secure' kwarg is passed to set_secure_cookie
  316. secure_cookie = self.secure_cookie
  317. if secure_cookie is None:
  318. secure_cookie = handler.request.protocol == "https"
  319. if secure_cookie:
  320. cookie_options.setdefault("secure", True)
  321. cookie_options.setdefault("path", handler.base_url) # type:ignore[attr-defined]
  322. cookie_name = self.get_cookie_name(handler)
  323. handler.set_secure_cookie(cookie_name, self.user_to_cookie(user), **cookie_options)
  324. def _force_clear_cookie(
  325. self, handler: web.RequestHandler, name: str, path: str = "/", domain: str | None = None
  326. ) -> None:
  327. """Deletes the cookie with the given name.
  328. Tornado's cookie handling currently (Jan 2018) stores cookies in a dict
  329. keyed by name, so it can only modify one cookie with a given name per
  330. response. The browser can store multiple cookies with the same name
  331. but different domains and/or paths. This method lets us clear multiple
  332. cookies with the same name.
  333. Due to limitations of the cookie protocol, you must pass the same
  334. path and domain to clear a cookie as were used when that cookie
  335. was set (but there is no way to find out on the server side
  336. which values were used for a given cookie).
  337. """
  338. name = escape.native_str(name)
  339. expires = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=365)
  340. morsel: Morsel[t.Any] = Morsel()
  341. morsel.set(name, "", '""')
  342. morsel["expires"] = httputil.format_timestamp(expires)
  343. morsel["path"] = path
  344. if domain:
  345. morsel["domain"] = domain
  346. handler.add_header("Set-Cookie", morsel.OutputString())
  347. def clear_login_cookie(self, handler: web.RequestHandler) -> None:
  348. """Clear the login cookie, effectively logging out the session."""
  349. cookie_options = {}
  350. cookie_options.update(self.cookie_options)
  351. path = cookie_options.setdefault("path", handler.base_url) # type:ignore[attr-defined]
  352. cookie_name = self.get_cookie_name(handler)
  353. handler.clear_cookie(cookie_name, path=path)
  354. if path and path != "/":
  355. # also clear cookie on / to ensure old cookies are cleared
  356. # after the change in path behavior.
  357. # N.B. This bypasses the normal cookie handling, which can't update
  358. # two cookies with the same name. See the method above.
  359. self._force_clear_cookie(handler, cookie_name)
  360. def get_user_cookie(
  361. self, handler: web.RequestHandler
  362. ) -> User | None | t.Awaitable[User | None]:
  363. """Get user from a cookie
  364. Calls user_from_cookie to deserialize cookie value
  365. """
  366. _user_cookie = handler.get_secure_cookie(
  367. self.get_cookie_name(handler),
  368. **self.get_secure_cookie_kwargs,
  369. )
  370. if not _user_cookie:
  371. return None
  372. user_cookie = _user_cookie.decode()
  373. # TODO: try/catch in case of change in config?
  374. try:
  375. return self.user_from_cookie(user_cookie)
  376. except Exception as e:
  377. # log bad cookie itself, only at debug-level
  378. self.log.debug(f"Error unpacking user from cookie: cookie={user_cookie}", exc_info=True)
  379. self.log.error(f"Error unpacking user from cookie: {e}")
  380. return None
  381. auth_header_pat = re.compile(r"(token|bearer)\s+(.+)", re.IGNORECASE)
  382. def get_token(self, handler: web.RequestHandler) -> str | None:
  383. """Get the user token from a request
  384. Default:
  385. - in URL parameters: ?token=<token>
  386. - in header: Authorization: token <token>
  387. """
  388. user_token = handler.get_argument("token", "")
  389. if not user_token:
  390. # get it from Authorization header
  391. m = self.auth_header_pat.match(handler.request.headers.get("Authorization", ""))
  392. if m:
  393. user_token = m.group(2)
  394. return user_token
  395. async def get_user_token(self, handler: web.RequestHandler) -> User | None:
  396. """Identify the user based on a token in the URL or Authorization header
  397. Returns:
  398. - uuid if authenticated
  399. - None if not
  400. """
  401. token = t.cast("str | None", handler.token) # type:ignore[attr-defined]
  402. if not token:
  403. return None
  404. # check login token from URL argument or Authorization header
  405. user_token = self.get_token(handler)
  406. authenticated = False
  407. if user_token == token:
  408. # token-authenticated, set the login cookie
  409. self.log.debug(
  410. "Accepting token-authenticated request from %s",
  411. handler.request.remote_ip,
  412. )
  413. authenticated = True
  414. if authenticated:
  415. # token does not correspond to user-id,
  416. # which is stored in a cookie.
  417. # still check the cookie for the user id
  418. _user = self.get_user_cookie(handler)
  419. if isinstance(_user, t.Awaitable):
  420. _user = await _user
  421. user: User | None = _user
  422. if user is None:
  423. user = self.generate_anonymous_user(handler)
  424. return user
  425. else:
  426. return None
  427. def generate_anonymous_user(self, handler: web.RequestHandler) -> User:
  428. """Generate a random anonymous user.
  429. For use when a single shared token is used,
  430. but does not identify a user.
  431. """
  432. user_id = uuid.uuid4().hex
  433. moon = get_anonymous_username()
  434. name = display_name = f"Anonymous {moon}"
  435. initials = f"A{moon[0]}"
  436. color = None
  437. handler.log.debug(f"Generating new user for token-authenticated request: {user_id}") # type:ignore[attr-defined]
  438. return User(user_id, name, display_name, initials, None, color)
  439. def should_check_origin(self, handler: web.RequestHandler) -> bool:
  440. """Should the Handler check for CORS origin validation?
  441. Origin check should be skipped for token-authenticated requests.
  442. Returns:
  443. - True, if Handler must check for valid CORS origin.
  444. - False, if Handler should skip origin check since requests are token-authenticated.
  445. """
  446. return not self.is_token_authenticated(handler)
  447. def is_token_authenticated(self, handler: web.RequestHandler) -> bool:
  448. """Returns True if handler has been token authenticated. Otherwise, False.
  449. Login with a token is used to signal certain things, such as:
  450. - permit access to REST API
  451. - xsrf protection
  452. - skip origin-checks for scripts
  453. """
  454. # ensure get_user has been called, so we know if we're token-authenticated
  455. handler.current_user # noqa: B018
  456. return getattr(handler, "_token_authenticated", False)
  457. def validate_security(
  458. self,
  459. app: t.Any,
  460. ssl_options: dict[str, t.Any] | None = None,
  461. ) -> None:
  462. """Check the application's security.
  463. Show messages, or abort if necessary, based on the security configuration.
  464. """
  465. if not app.ip:
  466. warning = "WARNING: The Jupyter server is listening on all IP addresses"
  467. if ssl_options is None:
  468. app.log.warning(f"{warning} and not using encryption. This is not recommended.")
  469. if not self.auth_enabled:
  470. app.log.warning(
  471. f"{warning} and not using authentication. "
  472. "This is highly insecure and not recommended."
  473. )
  474. elif not self.auth_enabled:
  475. app.log.warning(
  476. "All authentication is disabled."
  477. " Anyone who can connect to this server will be able to run code."
  478. )
  479. def process_login_form(self, handler: web.RequestHandler) -> User | None:
  480. """Process login form data
  481. Return authenticated User if successful, None if not.
  482. """
  483. typed_password = handler.get_argument("password", default="")
  484. user = None
  485. if not self.auth_enabled:
  486. self.log.warning("Accepting anonymous login because auth fully disabled!")
  487. return self.generate_anonymous_user(handler)
  488. if self.token and self.token == typed_password:
  489. return t.cast(User, self.user_for_token(typed_password)) # type:ignore[attr-defined]
  490. return user
  491. @property
  492. def auth_enabled(self):
  493. """Is authentication enabled?
  494. Should always be True, but may be False in rare, insecure cases
  495. where requests with no auth are allowed.
  496. Previously: LoginHandler.get_login_available
  497. """
  498. return True
  499. @property
  500. def login_available(self):
  501. """Whether a LoginHandler is needed - and therefore whether the login page should be displayed."""
  502. return self.auth_enabled
  503. @property
  504. def logout_available(self):
  505. """Whether a LogoutHandler is needed."""
  506. return True
  507. class PasswordIdentityProvider(IdentityProvider):
  508. """A password identity provider."""
  509. hashed_password = Unicode(
  510. "",
  511. config=True,
  512. help=_i18n(
  513. """
  514. Hashed password to use for web authentication.
  515. To generate, type in a python/IPython shell:
  516. from jupyter_server.auth import passwd; passwd()
  517. The string should be of the form type:salt:hashed-password.
  518. """
  519. ),
  520. )
  521. password_required = Bool(
  522. False,
  523. config=True,
  524. help=_i18n(
  525. """
  526. Forces users to use a password for the Jupyter server.
  527. This is useful in a multi user environment, for instance when
  528. everybody in the LAN can access each other's machine through ssh.
  529. In such a case, serving on localhost is not secure since
  530. any user can connect to the Jupyter server via ssh.
  531. """
  532. ),
  533. )
  534. allow_password_change = Bool(
  535. True,
  536. config=True,
  537. help=_i18n(
  538. """
  539. Allow password to be changed at login for the Jupyter server.
  540. While logging in with a token, the Jupyter server UI will give the opportunity to
  541. the user to enter a new password at the same time that will replace
  542. the token login mechanism.
  543. This can be set to False to prevent changing password from the UI/API.
  544. """
  545. ),
  546. )
  547. @default("need_token")
  548. def _need_token_default(self):
  549. return not bool(self.hashed_password)
  550. @default("updatable_fields")
  551. def _default_updatable_fields(self):
  552. return [
  553. "name",
  554. "display_name",
  555. "initials",
  556. "avatar_url",
  557. "color",
  558. ]
  559. @property
  560. def login_available(self) -> bool:
  561. """Whether a LoginHandler is needed - and therefore whether the login page should be displayed."""
  562. return self.auth_enabled
  563. @property
  564. def auth_enabled(self) -> bool:
  565. """Return whether any auth is enabled"""
  566. return bool(self.hashed_password or self.token)
  567. def update_user_model(self, current_user: User, user_data: dict[UpdatableField, str]) -> User:
  568. """Update user information."""
  569. for field in self.updatable_fields:
  570. if field in user_data:
  571. setattr(current_user, field, user_data[field])
  572. return current_user
  573. def persist_user_model(self, handler: web.RequestHandler) -> None:
  574. """Persist the user model to a cookie."""
  575. self.set_login_cookie(handler, handler.current_user)
  576. def passwd_check(self, password):
  577. """Check password against our stored hashed password"""
  578. return passwd_check(self.hashed_password, password)
  579. def process_login_form(self, handler: web.RequestHandler) -> User | None:
  580. """Process login form data
  581. Return authenticated User if successful, None if not.
  582. """
  583. typed_password = handler.get_argument("password", default="")
  584. new_password = handler.get_argument("new_password", default="")
  585. user = None
  586. if not self.auth_enabled:
  587. self.log.warning("Accepting anonymous login because auth fully disabled!")
  588. return self.generate_anonymous_user(handler)
  589. if self.passwd_check(typed_password) and not new_password:
  590. return self.generate_anonymous_user(handler)
  591. elif self.token and self.token == typed_password:
  592. user = self.generate_anonymous_user(handler)
  593. if new_password and self.allow_password_change:
  594. config_dir = handler.settings.get("config_dir", "")
  595. config_file = os.path.join(config_dir, "jupyter_server_config.json")
  596. self.hashed_password = set_password(new_password, config_file=config_file)
  597. self.log.info(_i18n("Wrote hashed password to {file}").format(file=config_file))
  598. return user
  599. def validate_security(
  600. self,
  601. app: t.Any,
  602. ssl_options: dict[str, t.Any] | None = None,
  603. ) -> None:
  604. """Handle security validation."""
  605. super().validate_security(app, ssl_options)
  606. if self.password_required and (not self.hashed_password):
  607. self.log.critical(
  608. _i18n("Jupyter servers are configured to only be run with a password.")
  609. )
  610. self.log.critical(_i18n("Hint: run the following command to set a password"))
  611. self.log.critical(_i18n("\t$ python -m jupyter_server.auth password"))
  612. sys.exit(1)
  613. class LegacyIdentityProvider(PasswordIdentityProvider):
  614. """Legacy IdentityProvider for use with custom LoginHandlers
  615. Login configuration has moved from LoginHandler to IdentityProvider
  616. in Jupyter Server 2.0.
  617. """
  618. # settings must be passed for
  619. settings = Dict()
  620. @default("settings")
  621. def _default_settings(self):
  622. return {
  623. "token": self.token,
  624. "password": self.hashed_password,
  625. }
  626. @default("login_handler_class")
  627. def _default_login_handler_class(self):
  628. from .login import LegacyLoginHandler
  629. return LegacyLoginHandler
  630. @property
  631. def auth_enabled(self):
  632. return self.login_available
  633. def get_user(self, handler: web.RequestHandler) -> User | None:
  634. """Get the user."""
  635. user = self.login_handler_class.get_user(handler) # type:ignore[attr-defined]
  636. if user is None:
  637. return None
  638. return _backward_compat_user(user)
  639. @property
  640. def login_available(self) -> bool:
  641. return bool(
  642. self.login_handler_class.get_login_available( # type:ignore[attr-defined]
  643. self.settings
  644. )
  645. )
  646. def should_check_origin(self, handler: web.RequestHandler) -> bool:
  647. """Whether we should check origin."""
  648. return bool(self.login_handler_class.should_check_origin(handler)) # type:ignore[attr-defined]
  649. def is_token_authenticated(self, handler: web.RequestHandler) -> bool:
  650. """Whether we are token authenticated."""
  651. return bool(self.login_handler_class.is_token_authenticated(handler)) # type:ignore[attr-defined]
  652. def validate_security(
  653. self,
  654. app: t.Any,
  655. ssl_options: dict[str, t.Any] | None = None,
  656. ) -> None:
  657. """Validate security."""
  658. if self.password_required and (not self.hashed_password):
  659. self.log.critical(
  660. _i18n("Jupyter servers are configured to only be run with a password.")
  661. )
  662. self.log.critical(_i18n("Hint: run the following command to set a password"))
  663. self.log.critical(_i18n("\t$ python -m jupyter_server.auth password"))
  664. sys.exit(1)
  665. self.login_handler_class.validate_security( # type:ignore[attr-defined]
  666. app, ssl_options
  667. )