Files
jupyterhub/jupyterhub/apihandlers/auth.py
2018-09-10 17:12:08 +02:00

286 lines
10 KiB
Python

"""Authorization handlers"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from datetime import datetime
import json
from urllib.parse import (
parse_qsl,
quote,
urlencode,
urlparse,
urlunparse,
)
from oauthlib import oauth2
from tornado import web
from .. import orm
from ..user import User
from ..utils import token_authenticated, compare_token
from .base import BaseHandler, APIHandler
class TokenAPIHandler(APIHandler):
@token_authenticated
def get(self, token):
orm_token = orm.APIToken.find(self.db, token)
if orm_token is None:
orm_token = orm.OAuthAccessToken.find(self.db, token)
if orm_token is None:
raise web.HTTPError(404)
# record activity whenever we see a token
now = orm_token.last_activity = datetime.utcnow()
if orm_token.user:
orm_token.user.last_activity = now
model = self.user_model(self.users[orm_token.user])
elif orm_token.service:
model = self.service_model(orm_token.service)
else:
self.log.warning("%s has no user or service. Deleting..." % orm_token)
self.db.delete(orm_token)
self.db.commit()
raise web.HTTPError(404)
self.db.commit()
self.write(json.dumps(model))
async def post(self):
warn_msg = (
"Using deprecated token creation endpoint %s."
" Use /hub/api/users/:user/tokens instead."
) % self.request.uri
self.log.warning(warn_msg)
requester = user = self.get_current_user()
if user is None:
# allow requesting a token with username and password
# for authenticators where that's possible
data = self.get_json_body()
try:
requester = user = await self.login_user(data)
except Exception as e:
self.log.error("Failure trying to authenticate with form data: %s" % e)
user = None
if user is None:
raise web.HTTPError(403)
else:
data = self.get_json_body()
# admin users can request tokens for other users
if data and data.get('username'):
user = self.find_user(data['username'])
if user is not requester and not requester.admin:
raise web.HTTPError(403, "Only admins can request tokens for other users.")
if requester.admin and user is None:
raise web.HTTPError(400, "No such user '%s'" % data['username'])
note = (data or {}).get('note')
if not note:
note = "Requested via deprecated api"
if requester is not user:
kind = 'user' if isinstance(user, User) else 'service'
note += " by %s %s" % (kind, requester.name)
api_token = user.new_api_token(note=note)
self.write(json.dumps({
'token': api_token,
'warning': warn_msg,
'user': self.user_model(user),
}))
class CookieAPIHandler(APIHandler):
@token_authenticated
def get(self, cookie_name, cookie_value=None):
cookie_name = quote(cookie_name, safe='')
if cookie_value is None:
self.log.warning("Cookie values in request body is deprecated, use `/cookie_name/cookie_value`")
cookie_value = self.request.body
else:
cookie_value = cookie_value.encode('utf8')
user = self._user_for_cookie(cookie_name, cookie_value)
if user is None:
raise web.HTTPError(404)
self.write(json.dumps(self.user_model(user)))
class OAuthHandler:
def extract_oauth_params(self):
"""extract oauthlib params from a request
Returns:
(uri, http_method, body, headers)
"""
return (
self.make_absolute_redirect_uri(self.request.uri),
self.request.method,
self.request.body,
self.request.headers,
)
def make_absolute_redirect_uri(self, uri):
"""Make absolute redirect URIs
internal redirect uris, e.g. `/user/foo/oauth_handler`
are allowed in jupyterhub, but oauthlib prohibits them.
Add `$HOST` header to redirect_uri to make them acceptable.
"""
redirect_uri = self.get_argument('redirect_uri')
if not redirect_uri or not redirect_uri.startswith('/'):
return uri
# make absolute local redirects full URLs
# to satisfy oauthlib's absolute URI requirement
redirect_uri = self.request.protocol + "://" + self.request.headers['Host'] + redirect_uri
parsed_url = urlparse(uri)
query_list = parse_qsl(parsed_url.query, keep_blank_values=True)
for idx, item in enumerate(query_list):
if item[0] == 'redirect_uri':
query_list[idx] = ('redirect_uri', redirect_uri)
break
return urlunparse(
urlparse(uri)
._replace(query=urlencode(query_list))
)
def add_credentials(self, credentials=None):
"""Add oauth credentials
Adds user, session_id, client to oauth credentials
"""
if credentials is None:
credentials = {}
else:
credentials = credentials.copy()
session_id = self.get_session_cookie()
if session_id is None:
session_id = self.set_session_cookie()
user = self.get_current_user()
# Extra credentials we need in the validator
credentials.update({
'user': user,
'handler': self,
'session_id': session_id,
})
return credentials
def send_oauth_response(self, headers, body, status):
"""Send oauth response
headers, body, status are returned by provider method.
"""
self.set_status(status)
for key, value in headers.items():
self.set_header(key, value)
if body:
self.write(body)
class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
"""Implement OAuth authorization endpoint(s)"""
def _complete_login(self, uri, headers, scopes, credentials):
try:
headers, body, status = self.oauth_provider.create_authorization_response(
uri, 'POST', '', headers, scopes, credentials)
except oauth2.FatalClientError as e:
# TODO: human error page
raise
self.send_oauth_response(headers, body, status)
@web.authenticated
def get(self):
"""GET /oauth/authorization
Render oauth confirmation page:
"Server at ... would like permission to ...".
Users accessing their own server will skip confirmation.
"""
uri, http_method, body, headers = self.extract_oauth_params()
try:
scopes, credentials = self.oauth_provider.validate_authorization_request(
uri, http_method, body, headers)
credentials = self.add_credentials(credentials)
client = self.oauth_provider.fetch_by_client_id(credentials['client_id'])
if client.redirect_uri.startswith(self.get_current_user().url):
self.log.debug(
"Skipping oauth confirmation for %s accessing %s",
self.get_current_user(), client.description,
)
# access to my own server doesn't require oauth confirmation
# this is the pre-1.0 behavior for all oauth
self._complete_login(uri, headers, scopes, credentials)
return
# Render oauth 'Authorize application...' page
self.write(
self.render_template(
"oauth.html",
scopes=scopes,
oauth_client=client,
)
)
# Errors that should be shown to the user on the provider website
except oauth2.FatalClientError as e:
raise web.HTTPError(e.status_code, e.description)
# Errors embedded in the redirect URI back to the client
except oauth2.OAuth2Error as e:
self.log.error("OAuth error: %s", e.description)
self.redirect(e.in_uri(e.redirect_uri))
@web.authenticated
def post(self):
uri, http_method, body, headers = self.extract_oauth_params()
referer = self.request.headers.get('Referer', 'no referer')
full_url = self.request.full_url()
if referer != full_url:
self.log.error("OAuth POST from %s != %s", referer, full_url)
raise web.HTTPError(403, "Authorization form must come from authorization")
# The scopes the user actually authorized, i.e. checkboxes
# that were selected.
scopes = self.get_arguments('scopes')
# credentials we need in the validator
credentials = self.add_credentials()
try:
headers, body, status = self.oauth_provider.create_authorization_response(
uri, http_method, body, headers, scopes, credentials,
)
except oauth2.FatalClientError as e:
raise web.HTTPError(e.status_code, e.description)
else:
self.send_oauth_response(headers, body, status)
class OAuthTokenHandler(OAuthHandler, APIHandler):
def post(self):
uri, http_method, body, headers = self.extract_oauth_params()
credentials = {}
try:
headers, body, status = self.oauth_provider.create_token_response(
uri, http_method, body, headers, credentials)
except oauth2.FatalClientError as e:
raise web.HTTPError(e.status_code, e.description)
else:
self.send_oauth_response(headers, body, status)
default_handlers = [
(r"/api/authorizations/cookie/([^/]+)(?:/([^/]+))?", CookieAPIHandler),
(r"/api/authorizations/token/([^/]+)", TokenAPIHandler),
(r"/api/authorizations/token", TokenAPIHandler),
(r"/api/oauth2/authorize", OAuthAuthorizeHandler),
(r"/api/oauth2/token", OAuthTokenHandler),
]