Skip to content

Commit fb8640a

Browse files
committed
add token_authenticated property
indicates if a token is used for authentication, in which case xsrf checks should be skipped.
1 parent 8abc6df commit fb8640a

File tree

2 files changed

+69
-39
lines changed

2 files changed

+69
-39
lines changed

notebook/auth/login.py

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def set_login_cookie(cls, handler, user_id=None):
9797
auth_header_pat = re.compile('token\s+(.+)', re.IGNORECASE)
9898

9999
@classmethod
100-
def get_user_token(cls, handler):
100+
def get_token(cls, handler):
101101
"""Get the user token from a request
102102
103103
Default:
@@ -120,11 +120,22 @@ def should_check_origin(cls, handler):
120120
121121
Origin check should be skipped for token-authenticated requests.
122122
"""
123+
return not cls.is_token_authenticated(handler)
124+
125+
@classmethod
126+
def is_token_authenticated(cls, handler):
127+
"""Check if the handler has been authenticated by a token.
128+
129+
This is used to signal certain things, such as:
130+
131+
- permit access to REST API
132+
- xsrf protection
133+
- skip origin-checks for scripts
134+
"""
123135
if getattr(handler, '_user_id', None) is None:
124136
# ensure get_user has been called, so we know if we're token-authenticated
125137
handler.get_current_user()
126-
token_authenticated = getattr(handler, '_token_authenticated', False)
127-
return not token_authenticated
138+
return getattr(handler, '_token_authenticated', False)
128139

129140
@classmethod
130141
def get_user(cls, handler):
@@ -136,40 +147,51 @@ def get_user(cls, handler):
136147
# called on LoginHandler itself.
137148
if getattr(handler, '_user_id', None):
138149
return handler._user_id
139-
user_id = handler.get_secure_cookie(handler.cookie_name)
140-
if not user_id:
150+
user_id = cls.get_user_token(handler)
151+
if user_id is None:
152+
user_id = handler.get_secure_cookie(handler.cookie_name)
153+
else:
154+
cls.set_login_cookie(handler, user_id)
155+
# Record that we've been authenticated with a token.
156+
# Used in should_check_origin above.
157+
handler._token_authenticated = True
158+
if user_id is None:
141159
# prevent extra Invalid cookie sig warnings:
142160
handler.clear_login_cookie()
143-
token = handler.token
144-
if not token and not handler.login_available:
161+
if not handler.login_available:
145162
# Completely insecure! No authentication at all.
146163
# No need to warn here, though; validate_security will have already done that.
147-
return 'anonymous'
148-
if token:
149-
# check login token from URL argument or Authorization header
150-
user_token = cls.get_user_token(handler)
151-
one_time_token = handler.one_time_token
152-
authenticated = False
153-
if user_token == token:
154-
# token-authenticated, set the login cookie
155-
handler.log.info("Accepting token-authenticated connection from %s", handler.request.remote_ip)
156-
authenticated = True
157-
elif one_time_token and user_token == one_time_token:
158-
# one-time-token-authenticated, only allow this token once
159-
handler.settings.pop('one_time_token', None)
160-
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
161-
authenticated = True
162-
if authenticated:
163-
user_id = uuid.uuid4().hex
164-
cls.set_login_cookie(handler, user_id)
165-
# Record that we've been authenticated with a token.
166-
# Used in should_check_origin above.
167-
handler._token_authenticated = True
164+
user_id = 'anonymous'
168165

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

170+
@classmethod
171+
def get_user_token(cls, handler):
172+
"""Identify the user based on a token in the URL or Authorization header"""
173+
token = handler.token
174+
if not token:
175+
return
176+
# check login token from URL argument or Authorization header
177+
user_token = cls.get_token(handler)
178+
one_time_token = handler.one_time_token
179+
authenticated = False
180+
if user_token == token:
181+
# token-authenticated, set the login cookie
182+
handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip)
183+
authenticated = True
184+
elif one_time_token and user_token == one_time_token:
185+
# one-time-token-authenticated, only allow this token once
186+
handler.settings.pop('one_time_token', None)
187+
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
188+
authenticated = True
189+
190+
if authenticated:
191+
return uuid.uuid4().hex
192+
else:
193+
return None
194+
173195

174196
@classmethod
175197
def validate_security(cls, app, ssl_options=None):

notebook/base/handlers.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def log():
4848

4949
class AuthenticatedHandler(web.RequestHandler):
5050
"""A RequestHandler with an authenticated user."""
51-
51+
5252
@property
5353
def content_security_policy(self):
5454
"""The default Content-Security-Policy header
@@ -95,6 +95,13 @@ def skip_check_origin(self):
9595
return False
9696
return not self.login_handler.should_check_origin(self)
9797

98+
@property
99+
def token_authenticated(self):
100+
"""Have I been authenticated with a token?"""
101+
if self.login_handler is None or not hasattr(self.login_handler, 'is_token_authenticated'):
102+
return False
103+
return self.login_handler.is_token_authenticated(self)
104+
98105
@property
99106
def cookie_name(self):
100107
default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
@@ -314,7 +321,15 @@ def check_origin(self, origin_to_satisfy_tornado=""):
314321
self.request.path, origin, host,
315322
)
316323
return allow
317-
324+
325+
def check_xsrf_cookie(self):
326+
"""Bypass xsrf checks when token-authenticated"""
327+
if self.token_authenticated:
328+
# Token-authenticated requests do not need additional XSRF-check
329+
# Servers without authentication are vulnerable to XSRF
330+
return
331+
return super(IPythonHandler, self).check_xsrf_cookie()
332+
318333
#---------------------------------------------------------------
319334
# template rendering
320335
#---------------------------------------------------------------
@@ -343,6 +358,8 @@ def template_namespace(self):
343358
version_hash=self.version_hash,
344359
ignore_minified_js=self.ignore_minified_js,
345360
xsrf_form_html=self.xsrf_form_html,
361+
token=self.token,
362+
xsrf_token=self.xsrf_token,
346363
**self.jinja_template_vars
347364
)
348365

@@ -406,15 +423,6 @@ def prepare(self):
406423
raise web.HTTPError(404)
407424
return super(APIHandler, self).prepare()
408425

409-
def check_xsrf_cookie(self):
410-
"""Check non-empty body on POST for XSRF
411-
412-
instead of checking the cookie for forms.
413-
"""
414-
if self.request.method.upper() == 'POST' and not self.request.body:
415-
# Require non-empty POST body for XSRF
416-
raise web.HTTPError(400, "POST requests must have a JSON body. If no content is needed, use '{}'.")
417-
418426
@property
419427
def content_security_policy(self):
420428
csp = '; '.join([

0 commit comments

Comments
 (0)