Skip to content

Commit ff1fa6e

Browse files
committed
[WIP] migrate LoginHandler config to IdentityProvider
now that we have a configurable class to represent identity, most of the LoginHandler's custom methods belong there Migrates: - ServerApp.password, token config - all LoginHandler class methods Adds: - LegacyIdentityProvider, LegacyLoginHandler to preserve old behavior with new API, triggered with ServerApp.login_handler_class is overridden, but not identity_provider_class.
1 parent d7307d0 commit ff1fa6e

File tree

8 files changed

+631
-171
lines changed

8 files changed

+631
-171
lines changed

jupyter_server/auth/identity.py

Lines changed: 455 additions & 23 deletions
Large diffs are not rendered by default.

jupyter_server/auth/login.py

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
from .security import passwd_check, set_password
1313

1414

15-
class LoginHandler(JupyterHandler):
15+
class LoginFormHandler(JupyterHandler):
1616
"""The basic tornado login handler
1717
18-
authenticates with a hashed password from the configuration.
18+
accepts login form, passed to IdentityProvider.process_login_form.
1919
"""
2020

2121
def _render(self, message=None):
@@ -66,6 +66,26 @@ def get(self):
6666
else:
6767
self._render()
6868

69+
def post(self):
70+
user = self.current_user = self.identity_provider.process_login_form(self)
71+
if user is None:
72+
self.set_status(401)
73+
self._render(message={"error": "Invalid credentials"})
74+
return
75+
76+
self.log.info(f"User {user.username} logged in.")
77+
self.identity_provider.set_login_cookie(self, user)
78+
next_url = self.get_argument("next", default=self.base_url)
79+
self._redirect_safe(next_url)
80+
81+
82+
class LegacyLoginHandler(LoginFormHandler):
83+
"""Legacy LoginHandler, implementing most custom auth configuration.
84+
85+
Deprecated in jupyter-server 2.0.
86+
Login configuration has moved to IdentityProvider.
87+
"""
88+
6989
@property
7090
def hashed_password(self):
7191
return self.password_from_settings(self.settings)
@@ -74,6 +94,7 @@ def passwd_check(self, a, b):
7494
return passwd_check(a, b)
7595

7696
def post(self):
97+
7798
typed_password = self.get_argument("password", default="")
7899
new_password = self.get_argument("new_password", default="")
79100

@@ -130,37 +151,20 @@ def get_token(cls, handler):
130151

131152
@classmethod
132153
def should_check_origin(cls, handler):
133-
"""Should the Handler check for CORS origin validation?
134-
135-
Origin check should be skipped for token-authenticated requests.
136-
137-
Returns:
138-
- True, if Handler must check for valid CORS origin.
139-
- False, if Handler should skip origin check since requests are token-authenticated.
140-
"""
154+
"""DEPRECATED in 2.0, use IdentityProvider API"""
141155
return not cls.is_token_authenticated(handler)
142156

143157
@classmethod
144158
def is_token_authenticated(cls, handler):
145-
"""Returns True if handler has been token authenticated. Otherwise, False.
146-
147-
Login with a token is used to signal certain things, such as:
148-
149-
- permit access to REST API
150-
- xsrf protection
151-
- skip origin-checks for scripts
152-
"""
159+
"""DEPRECATED in 2.0, use IdentityProvider API"""
153160
if getattr(handler, "_user_id", None) is None:
154161
# ensure get_user has been called, so we know if we're token-authenticated
155162
handler.current_user
156163
return getattr(handler, "_token_authenticated", False)
157164

158165
@classmethod
159166
def get_user(cls, handler):
160-
"""Called by handlers.get_current_user for identifying the current user.
161-
162-
See tornado.web.RequestHandler.get_current_user for details.
163-
"""
167+
"""DEPRECATED in 2.0, use IdentityProvider API"""
164168
# Can't call this get_current_user because it will collide when
165169
# called on LoginHandler itself.
166170
if getattr(handler, "_user_id", None):
@@ -197,7 +201,7 @@ def get_user(cls, handler):
197201

198202
@classmethod
199203
def get_user_cookie(cls, handler):
200-
"""Get user-id from a cookie"""
204+
"""DEPRECATED in 2.0, use IdentityProvider API"""
201205
get_secure_cookie_kwargs = handler.settings.get("get_secure_cookie_kwargs", {})
202206
user_id = handler.get_secure_cookie(handler.cookie_name, **get_secure_cookie_kwargs)
203207
if user_id:
@@ -206,12 +210,7 @@ def get_user_cookie(cls, handler):
206210

207211
@classmethod
208212
def get_user_token(cls, handler):
209-
"""Identify the user based on a token in the URL or Authorization header
210-
211-
Returns:
212-
- uuid if authenticated
213-
- None if not
214-
"""
213+
"""DEPRECATED in 2.0, use IdentityProvider API"""
215214
token = handler.token
216215
if not token:
217216
return
@@ -243,10 +242,7 @@ def get_user_token(cls, handler):
243242

244243
@classmethod
245244
def validate_security(cls, app, ssl_options=None):
246-
"""Check the application's security.
247-
248-
Show messages, or abort if necessary, based on the security configuration.
249-
"""
245+
"""DEPRECATED in 2.0, use IdentityProvider API"""
250246
if not app.ip:
251247
warning = "WARNING: The Jupyter server is listening on all IP addresses"
252248
if ssl_options is None:
@@ -265,13 +261,15 @@ def validate_security(cls, app, ssl_options=None):
265261

266262
@classmethod
267263
def password_from_settings(cls, settings):
268-
"""Return the hashed password from the tornado settings.
269-
270-
If there is no configured password, an empty string will be returned.
271-
"""
264+
"""DEPRECATED in 2.0, use IdentityProvider API"""
272265
return settings.get("password", "")
273266

274267
@classmethod
275268
def get_login_available(cls, settings):
276-
"""Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
269+
"""DEPRECATED in 2.0, use IdentityProvider API"""
270+
277271
return bool(cls.password_from_settings(settings) or settings.get("token"))
272+
273+
274+
# deprecated import, so deprecated implementations get the Legacy class instead
275+
LoginHandler = LegacyLoginHandler

jupyter_server/base/handlers.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,12 @@ def skip_check_origin(self):
160160
if self.request.method == "OPTIONS":
161161
# no origin-check on options requests, which are used to check origins!
162162
return True
163-
if self.login_handler is None or not hasattr(self.login_handler, "should_check_origin"):
164-
return False
165-
return not self.login_handler.should_check_origin(self)
163+
return not self.identity_provider.should_check_origin(self)
166164

167165
@property
168166
def token_authenticated(self):
169167
"""Have I been authenticated with a token?"""
170-
if self.login_handler is None or not hasattr(self.login_handler, "is_token_authenticated"):
171-
return False
172-
return self.login_handler.is_token_authenticated(self)
168+
return self.identity_provider.is_token_authenticated(self)
173169

174170
@property
175171
def cookie_name(self):
@@ -185,12 +181,19 @@ def logged_in(self):
185181
@property
186182
def login_handler(self):
187183
"""Return the login handler for this application, if any."""
188-
return self.settings.get("login_handler_class", None)
184+
warnings.warn(
185+
"""JupyterHandler.login_handler is deprecated in 2.0,
186+
use JupyterHandler.identity_provider.
187+
""",
188+
DeprecationWarning,
189+
stacklevel=2,
190+
)
191+
return self.identity_provider.login_handler_class
189192

190193
@property
191194
def token(self):
192195
"""Return the login token for this application, if any."""
193-
return self.settings.get("token", None)
196+
return self.identity_provider.token
194197

195198
@property
196199
def login_available(self):
@@ -200,9 +203,7 @@ def login_available(self):
200203
whether the user is already logged in or not.
201204
202205
"""
203-
if self.login_handler is None:
204-
return False
205-
return bool(self.login_handler.get_login_available(self.settings))
206+
return self.identity_provider.login_available
206207

207208
@property
208209
def authorizer(self):
@@ -628,8 +629,9 @@ def template_namespace(self):
628629
default_url=self.default_url,
629630
ws_url=self.ws_url,
630631
logged_in=self.logged_in,
631-
allow_password_change=self.settings.get("allow_password_change"),
632-
login_available=self.login_available,
632+
allow_password_change=getattr(self.identity_provider, "allow_password_change", False),
633+
auth_enabled=self.identity_provider.auth_enabled,
634+
login_available=self.identity_provider.login_available,
633635
token_available=bool(self.token),
634636
static_url=self.static_url,
635637
sys_info=json_sys_info(),

jupyter_server/pytest_plugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ def _configurable_serverapp(
251251
c = Config(config)
252252
c.NotebookNotary.db_file = ":memory:"
253253
token = hexlify(os.urandom(4)).decode("ascii")
254+
c.IdentityProvider.token = token
254255

255256
# Allow tests to configure root_dir via a file, argv, or its
256257
# default (cwd) by specifying a value of None.
@@ -266,7 +267,6 @@ def _configurable_serverapp(
266267
base_url=base_url,
267268
config=c,
268269
allow_root=True,
269-
token=token,
270270
**kwargs,
271271
)
272272

@@ -329,7 +329,7 @@ def jp_web_app(jp_serverapp):
329329
@pytest.fixture
330330
def jp_auth_header(jp_serverapp):
331331
"""Configures an authorization header using the token from the serverapp fixture."""
332-
return {"Authorization": f"token {jp_serverapp.token}"}
332+
return {"Authorization": f"token {jp_serverapp.identity_provider.token}"}
333333

334334

335335
@pytest.fixture

0 commit comments

Comments
 (0)