Skip to content

Commit

Permalink
[WIP] migrate LoginHandler config to IdentityProvider
Browse files Browse the repository at this point in the history
now that we have a configurable class to represent identity, most of the LoginHandler's custom methods belong there
  • Loading branch information
minrk committed May 2, 2022
1 parent 6d84507 commit 8da140e
Showing 1 changed file with 180 additions and 0 deletions.
180 changes: 180 additions & 0 deletions jupyter_server/auth/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
.. versionadded:: 2.0
"""
import re
from dataclasses import asdict, dataclass
from typing import Any, Optional

from tornado.web import RequestHandler
from traitlets import Unicode, default
from traitlets.config import LoggingConfigurable

# from dataclasses import field
Expand Down Expand Up @@ -108,6 +110,8 @@ class IdentityProvider(LoggingConfigurable):
.. versionadded:: 2.0
"""

cookie_name = Unicode(config=True)

def get_user(self, handler: RequestHandler) -> User:
"""Get the authenticated user for a request
Expand Down Expand Up @@ -139,3 +143,179 @@ def get_handlers(self) -> list:
For example, an OAuth callback handler.
"""
return []

def set_login_cookie(self, handler, user_id=None):
"""Call this on handlers to set the login cookie for success"""
cookie_options = handler.settings.get("cookie_options", {})
cookie_options.setdefault("httponly", True)
# tornado <4.2 has a bug that considers secure==True as soon as
# 'secure' kwarg is passed to set_secure_cookie
if handler.settings.get("secure_cookie", handler.request.protocol == "https"):
cookie_options.setdefault("secure", True)
cookie_options.setdefault("path", handler.base_url)
handler.set_secure_cookie(handler.cookie_name, user_id, **cookie_options)
return user_id

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

def get_token(self, handler):
"""Get the user token from a request
Default:
- in URL parameters: ?token=<token>
- in header: Authorization: token <token>
"""

user_token = handler.get_argument("token", "")
if not user_token:
# get it from Authorization header
m = self.auth_header_pat.match(handler.request.headers.get("Authorization", ""))
if m:
user_token = m.group(1)
return user_token

def should_check_origin(self, handler):
"""Should the Handler check for CORS origin validation?
Origin check should be skipped for token-authenticated requests.
Returns:
- True, if Handler must check for valid CORS origin.
- False, if Handler should skip origin check since requests are token-authenticated.
"""
return not self.is_token_authenticated(handler)

def is_token_authenticated(self, handler):
"""Returns True if handler has been token authenticated. Otherwise, False.
Login with a token is used to signal certain things, such as:
- permit access to REST API
- xsrf protection
- skip origin-checks for scripts
"""
# ensure get_user has been called, so we know if we're token-authenticated
handler.current_user # noqa
return getattr(handler, "_token_authenticated", False)

def get_user(self, handler):
"""Called by handlers.get_current_user for identifying the current user.
See tornado.web.RequestHandler.get_current_user for details.
"""
# Can't call this get_current_user because it will collide when
# called on LoginHandler itself.
if getattr(handler, "_user_id", None):
return handler._user_id
token_user_id = self.get_user_token(handler)
cookie_user_id = self.get_user_cookie(handler)
# prefer token to cookie if both given,
# because token is always explicit
user_id = token_user_id or cookie_user_id
if token_user_id:
# if token-authenticated, persist user_id in cookie
# if it hasn't already been stored there
if user_id != cookie_user_id:
self.set_login_cookie(handler, user_id)
# Record that the current request has been authenticated with a token.
# Used in is_token_authenticated above.
handler._token_authenticated = True

if user_id is None:
# If an invalid cookie was sent, clear it to prevent unnecessary
# extra warnings. But don't do this on a request with *no* cookie,
# because that can erroneously log you out (see gh-3365)
if handler.get_cookie(handler.cookie_name) is not None:
handler.log.warning("Clearing invalid/expired login cookie %s", handler.cookie_name)
handler.clear_login_cookie()
if not handler.login_available:
# Completely insecure! No authentication at all.
# No need to warn here, though; validate_security will have already done that.
user_id = "anonymous"

# cache value for future retrievals on the same request
handler._user_id = user_id
return user_id

def get_user_cookie(self, handler):
"""Get user-id from a cookie"""
get_secure_cookie_kwargs = handler.settings.get("get_secure_cookie_kwargs", {})
user_id = handler.get_secure_cookie(handler.cookie_name, **get_secure_cookie_kwargs)
if user_id:
user_id = user_id.decode()
return user_id

def get_user_token(self, handler):
"""Identify the user based on a token in the URL or Authorization header
Returns:
- uuid if authenticated
- None if not
"""
token = handler.token
if not token:
return
# check login token from URL argument or Authorization header
user_token = self.get_token(handler)
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.debug(
"Accepting token-authenticated connection from %s",
handler.request.remote_ip,
)
authenticated = True

if authenticated:
# token does not correspond to user-id,
# which is stored in a cookie.
# still check the cookie for the user id
user_id = self.get_user_cookie(handler)
if user_id is None:
# no cookie, generate new random user_id
user_id = uuid.uuid4().hex
handler.log.info(
f"Generating new user_id for token-authenticated request: {user_id}"
)
return user_id
else:
return None

def generate_user(self, hander):
"""Generate a random"""

def validate_security(self, app, ssl_options=None):
"""Check the application's security.
Show messages, or abort if necessary, based on the security configuration.
"""
if not app.ip:
warning = "WARNING: The Jupyter server is listening on all IP addresses"
if ssl_options is None:
app.log.warning(f"{warning} and not using encryption. This is not recommended.")
if not app.password and not app.token:
app.log.warning(
f"{warning} and not using authentication. "
"This is highly insecure and not recommended."
)
else:
if not app.password and not app.token:
app.log.warning(
"All authentication is disabled."
" Anyone who can connect to this server will be able to run code."
)

def get_login_available(self, settings):
"""Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
return False


class PasswordIdentityProvider(IdentityProvider):

password = Unicode(config=True)

@default("password")
def get_login_available(self, settings):
"""Whether this LoginHandler is needed - and therefore whether the login page should be displayed."""
return bool(self.password_from_settings(settings) or settings.get("token"))

0 comments on commit 8da140e

Please sign in to comment.