mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 13:03:01 +00:00
[wip] switch to oauthlib from python-oauth2
lower-level implementation, but more robust and gives us more control
This commit is contained in:
@@ -5,14 +5,20 @@
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from urllib.parse import quote
|
from urllib.parse import (
|
||||||
|
parse_qsl,
|
||||||
|
quote,
|
||||||
|
urlencode,
|
||||||
|
urlparse,
|
||||||
|
urlunparse,
|
||||||
|
)
|
||||||
|
|
||||||
from oauth2.web.tornado import OAuth2Handler
|
from oauthlib import oauth2
|
||||||
from tornado import web
|
from tornado import web
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..user import User
|
from ..user import User
|
||||||
from ..utils import token_authenticated
|
from ..utils import token_authenticated, compare_token
|
||||||
from .base import BaseHandler, APIHandler
|
from .base import BaseHandler, APIHandler
|
||||||
|
|
||||||
|
|
||||||
@@ -98,24 +104,149 @@ class CookieAPIHandler(APIHandler):
|
|||||||
self.write(json.dumps(self.user_model(user)))
|
self.write(json.dumps(self.user_model(user)))
|
||||||
|
|
||||||
|
|
||||||
class OAuthHandler(BaseHandler, OAuth2Handler):
|
class OAuthHandler(BaseHandler):
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthAuthorizeHandler(OAuthHandler):
|
||||||
"""Implement OAuth provider handlers
|
"""Implement OAuth provider handlers
|
||||||
|
|
||||||
OAuth2Handler sets `self.provider` in initialize,
|
OAuth2Handler sets `self.provider` in initialize,
|
||||||
but we are already passing the Provider object via settings.
|
but we are already passing the Provider object via settings.
|
||||||
"""
|
"""
|
||||||
@property
|
|
||||||
def provider(self):
|
|
||||||
return self.settings['oauth_provider']
|
|
||||||
|
|
||||||
def initialize(self):
|
@web.authenticated
|
||||||
|
def get(self):
|
||||||
|
# You need to define extract_params and make sure it does not
|
||||||
|
# include file like objects waiting for input. In Django this
|
||||||
|
# is request.META['wsgi.input'] and request.META['wsgi.errors']
|
||||||
|
uri, http_method, body, headers = self.extract_oauth_params()
|
||||||
|
|
||||||
|
try:
|
||||||
|
scopes, credentials = self.oauth_provider.validate_authorization_request(
|
||||||
|
uri, http_method, body, headers)
|
||||||
|
|
||||||
|
if scopes == ['identify']:
|
||||||
pass
|
pass
|
||||||
|
client_id = 'hmmm'
|
||||||
|
# You probably want to render a template instead.
|
||||||
|
self.write('<h1> Authorize access to %s </h1>' % client_id)
|
||||||
|
self.write('<form method="POST" action="">')
|
||||||
|
for scope in scopes or []:
|
||||||
|
self.write('<input type="checkbox" name="scopes" ' +
|
||||||
|
'value="%s"/> %s' % (scope, scope))
|
||||||
|
self.write('<input type="submit" value="Authorize"/>')
|
||||||
|
|
||||||
|
# Errors that should be shown to the user on the provider website
|
||||||
|
except oauth2.FatalClientError as e:
|
||||||
|
# TODO: human error page
|
||||||
|
raise
|
||||||
|
# return response_from_error(e)
|
||||||
|
|
||||||
|
# Errors embedded in the redirect URI back to the client
|
||||||
|
except oauth2.OAuth2Error as e:
|
||||||
|
self.log.error("oauth error: %s" % e)
|
||||||
|
self.redirect(e.in_uri(e.redirect_uri))
|
||||||
|
|
||||||
|
@web.authenticated
|
||||||
|
def post(self):
|
||||||
|
uri, http_method, body, headers = self.extract_oauth_params()
|
||||||
|
|
||||||
|
# The scopes the user actually authorized, i.e. checkboxes
|
||||||
|
# that were selected.
|
||||||
|
scopes = self.get_arguments('scopes')
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
'user': user,
|
||||||
|
'handler': self,
|
||||||
|
'session_id': session_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# The previously stored (in authorization GET view) credentials
|
||||||
|
# credentials.update(request.session.get('oauth2_credentials', {}))
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers, body, status = self.oauth_provider.create_authorization_response(
|
||||||
|
uri, http_method, body, headers, scopes, credentials)
|
||||||
|
self.set_status(status)
|
||||||
|
for key, value in headers.items():
|
||||||
|
self.set_header(key, value)
|
||||||
|
if body:
|
||||||
|
self.write(body)
|
||||||
|
|
||||||
|
except oauth2.FatalClientError as e:
|
||||||
|
# TODO: human error page
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthTokenHandler(OAuthHandler):
|
||||||
|
|
||||||
|
# get JSON error messages
|
||||||
|
write_error = APIHandler.write_error
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
uri, http_method, body, headers = self.extract_oauth_params()
|
||||||
|
credentials = {}
|
||||||
|
|
||||||
|
headers, body, status = self.oauth_provider.create_token_response(
|
||||||
|
uri, http_method, body, headers, credentials)
|
||||||
|
|
||||||
|
self.set_status(status)
|
||||||
|
for key, value in headers.items():
|
||||||
|
self.set_header(key, value)
|
||||||
|
if body:
|
||||||
|
self.write(body)
|
||||||
|
|
||||||
|
|
||||||
default_handlers = [
|
default_handlers = [
|
||||||
(r"/api/authorizations/cookie/([^/]+)(?:/([^/]+))?", CookieAPIHandler),
|
(r"/api/authorizations/cookie/([^/]+)(?:/([^/]+))?", CookieAPIHandler),
|
||||||
(r"/api/authorizations/token/([^/]+)", TokenAPIHandler),
|
(r"/api/authorizations/token/([^/]+)", TokenAPIHandler),
|
||||||
(r"/api/authorizations/token", TokenAPIHandler),
|
(r"/api/authorizations/token", TokenAPIHandler),
|
||||||
(r"/api/oauth2/authorize", OAuthHandler),
|
(r"/api/oauth2/authorize", OAuthAuthorizeHandler),
|
||||||
(r"/api/oauth2/token", OAuthHandler),
|
(r"/api/oauth2/token", OAuthTokenHandler),
|
||||||
]
|
]
|
||||||
|
@@ -1331,7 +1331,7 @@ class JupyterHub(Application):
|
|||||||
host = '%s://services.%s' % (parsed.scheme, parsed.netloc)
|
host = '%s://services.%s' % (parsed.scheme, parsed.netloc)
|
||||||
else:
|
else:
|
||||||
domain = host = ''
|
domain = host = ''
|
||||||
client_store = self.oauth_provider.client_authenticator.client_store
|
|
||||||
for spec in self.services:
|
for spec in self.services:
|
||||||
if 'name' not in spec:
|
if 'name' not in spec:
|
||||||
raise ValueError('service spec must have a name: %r' % spec)
|
raise ValueError('service spec must have a name: %r' % spec)
|
||||||
@@ -1389,7 +1389,7 @@ class JupyterHub(Application):
|
|||||||
service.orm.server = None
|
service.orm.server = None
|
||||||
|
|
||||||
if service.oauth_available:
|
if service.oauth_available:
|
||||||
client_store.add_client(
|
self.oauth_provider.add_client(
|
||||||
client_id=service.oauth_client_id,
|
client_id=service.oauth_client_id,
|
||||||
client_secret=service.api_token,
|
client_secret=service.api_token,
|
||||||
redirect_uri=service.oauth_redirect_uri,
|
redirect_uri=service.oauth_redirect_uri,
|
||||||
@@ -1489,9 +1489,9 @@ class JupyterHub(Application):
|
|||||||
def init_oauth(self):
|
def init_oauth(self):
|
||||||
base_url = self.hub.base_url
|
base_url = self.hub.base_url
|
||||||
self.oauth_provider = make_provider(
|
self.oauth_provider = make_provider(
|
||||||
lambda : self.db,
|
lambda: self.db,
|
||||||
url_prefix=url_path_join(base_url, 'api/oauth2'),
|
url_prefix=url_path_join(base_url, 'api/oauth2'),
|
||||||
login_url=url_path_join(base_url, 'login')
|
login_url=url_path_join(base_url, 'login'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def cleanup_oauth_clients(self):
|
def cleanup_oauth_clients(self):
|
||||||
@@ -1507,7 +1507,6 @@ class JupyterHub(Application):
|
|||||||
for spawner in user.spawners.values():
|
for spawner in user.spawners.values():
|
||||||
oauth_client_ids.add(spawner.oauth_client_id)
|
oauth_client_ids.add(spawner.oauth_client_id)
|
||||||
|
|
||||||
client_store = self.oauth_provider.client_authenticator.client_store
|
|
||||||
for i, oauth_client in enumerate(self.db.query(orm.OAuthClient)):
|
for i, oauth_client in enumerate(self.db.query(orm.OAuthClient)):
|
||||||
if oauth_client.identifier not in oauth_client_ids:
|
if oauth_client.identifier not in oauth_client_ids:
|
||||||
self.log.warning("Deleting OAuth client %s", oauth_client.identifier)
|
self.log.warning("Deleting OAuth client %s", oauth_client.identifier)
|
||||||
|
@@ -3,104 +3,412 @@
|
|||||||
implements https://python-oauth2.readthedocs.io/en/latest/store.html
|
implements https://python-oauth2.readthedocs.io/en/latest/store.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import threading
|
from datetime import datetime
|
||||||
|
|
||||||
from oauth2.datatype import Client, AuthorizationCode
|
from oauthlib.oauth2 import RequestValidator, WebApplicationServer
|
||||||
from oauth2.error import AuthCodeNotFound, ClientNotFoundError, UserNotAuthenticated
|
|
||||||
from oauth2.grant import AuthorizationCodeGrant
|
|
||||||
from oauth2.web import AuthorizationCodeGrantSiteAdapter
|
|
||||||
import oauth2.store
|
|
||||||
from oauth2 import Provider
|
|
||||||
from oauth2.tokengenerator import Uuid4 as UUID4
|
|
||||||
|
|
||||||
from sqlalchemy.orm import scoped_session
|
from sqlalchemy.orm import scoped_session
|
||||||
from tornado.escape import url_escape
|
from tornado.escape import url_escape
|
||||||
|
from tornado.log import app_log
|
||||||
|
from tornado import web
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import url_path_join, hash_token, compare_token
|
from ..utils import url_path_join, hash_token, compare_token
|
||||||
|
|
||||||
|
|
||||||
class JupyterHubSiteAdapter(AuthorizationCodeGrantSiteAdapter):
|
class JupyterHubRequestValidator(RequestValidator):
|
||||||
"""
|
|
||||||
This adapter renders a confirmation page so the user can confirm the auth
|
|
||||||
request.
|
|
||||||
"""
|
|
||||||
def __init__(self, login_url):
|
|
||||||
self.login_url = login_url
|
|
||||||
|
|
||||||
def render_auth_page(self, request, response, environ, scopes, client):
|
def __init__(self, db):
|
||||||
"""Auth page is a redirect to login page"""
|
self.db = db
|
||||||
response.status_code = 302
|
super().__init__()
|
||||||
response.headers['Location'] = self.login_url + '?next={}'.format(
|
|
||||||
url_escape(request.handler.request.path + '?' + request.handler.request.query)
|
def authenticate_client(self, request, *args, **kwargs):
|
||||||
|
"""Authenticate client through means outside the OAuth 2 spec.
|
||||||
|
Means of authentication is negotiated beforehand and may for example
|
||||||
|
be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization
|
||||||
|
header.
|
||||||
|
Headers may be accesses through request.headers and parameters found in
|
||||||
|
both body and query can be obtained by direct attribute access, i.e.
|
||||||
|
request.client_id for client_id in the URL query.
|
||||||
|
:param request: oauthlib.common.Request
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Resource Owner Password Credentials Grant (may be disabled)
|
||||||
|
- Client Credentials Grant
|
||||||
|
- Refresh Token Grant
|
||||||
|
.. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1
|
||||||
|
"""
|
||||||
|
app_log.debug("authenticate_client %s", request)
|
||||||
|
client_id = request.client_id
|
||||||
|
client_secret = request.client_secret
|
||||||
|
oauth_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
return response
|
if oauth_client is None:
|
||||||
|
raise web.HTTPError(400, "Bad OAuth Client")
|
||||||
|
if not compare_token(oauth_client.secret, client_secret):
|
||||||
|
raise web.HTTPError(400, "Bad OAuth Client")
|
||||||
|
|
||||||
def authenticate(self, request, environ, scopes, client):
|
request.client = oauth_client
|
||||||
handler = request.handler
|
return True
|
||||||
user = handler.get_current_user()
|
|
||||||
# ensure session_id is set
|
|
||||||
session_id = handler.get_session_cookie()
|
|
||||||
if session_id is None:
|
|
||||||
session_id = handler.set_session_cookie()
|
|
||||||
if user:
|
|
||||||
return {'session_id': session_id}, user.id
|
|
||||||
else:
|
|
||||||
raise UserNotAuthenticated()
|
|
||||||
|
|
||||||
def user_has_denied_access(self, request):
|
def authenticate_client_id(self, client_id, request, *args, **kwargs):
|
||||||
# user can't deny access
|
"""Ensure client_id belong to a non-confidential client.
|
||||||
|
A non-confidential client is one that is not required to authenticate
|
||||||
|
through other means, such as using HTTP Basic.
|
||||||
|
Note, while not strictly necessary it can often be very convenient
|
||||||
|
to set request.client to the client object associated with the
|
||||||
|
given client_id.
|
||||||
|
:param request: oauthlib.common.Request
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
"""
|
||||||
|
orm_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if orm_client is None:
|
||||||
|
return False
|
||||||
|
request.client = orm_client
|
||||||
|
return True
|
||||||
|
|
||||||
|
def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
|
||||||
|
*args, **kwargs):
|
||||||
|
"""Ensure that the authorization process represented by this authorization
|
||||||
|
code began with this 'redirect_uri'.
|
||||||
|
If the client specifies a redirect_uri when obtaining code then that
|
||||||
|
redirect URI must be bound to the code and verified equal in this
|
||||||
|
method, according to RFC 6749 section 4.1.3. Do not compare against
|
||||||
|
the client's allowed redirect URIs, but against the URI used when the
|
||||||
|
code was saved.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param code: Unicode authorization_code.
|
||||||
|
:param redirect_uri: Unicode absolute URI
|
||||||
|
:param client: Client object set by you, see authenticate_client.
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant (during token request)
|
||||||
|
"""
|
||||||
|
app_log.debug("confirm_redirect_uri: client_id=%s, code=%s, redirect_uri=%s",
|
||||||
|
client_id, code, redirect_uri,
|
||||||
|
)
|
||||||
|
orm_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if orm_client is None:
|
||||||
|
return False
|
||||||
|
# TODO: confirm redirect uri
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
||||||
|
"""Get the default redirect URI for the client.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: The default redirect URI for the client
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
|
"""
|
||||||
|
orm_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if orm_client is None:
|
||||||
|
return False
|
||||||
|
return orm_client.redirect_uri
|
||||||
|
|
||||||
|
def get_default_scopes(self, client_id, request, *args, **kwargs):
|
||||||
|
"""Get the default scopes for the client.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: List of default scopes
|
||||||
|
Method is used by all core grant types:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
|
- Resource Owner Password Credentials Grant
|
||||||
|
- Client Credentials grant
|
||||||
|
"""
|
||||||
|
return ['identify']
|
||||||
|
|
||||||
|
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||||
|
"""Get the list of scopes associated with the refresh token.
|
||||||
|
:param refresh_token: Unicode refresh token
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: List of scopes.
|
||||||
|
Method is used by:
|
||||||
|
- Refresh token grant
|
||||||
|
"""
|
||||||
|
token = find_token()
|
||||||
|
return token.scopes
|
||||||
|
|
||||||
|
def is_within_original_scope(self, request_scopes, refresh_token, request, *args, **kwargs):
|
||||||
|
"""Check if requested scopes are within a scope of the refresh token.
|
||||||
|
When access tokens are refreshed the scope of the new token
|
||||||
|
needs to be within the scope of the original token. This is
|
||||||
|
ensured by checking that all requested scopes strings are on
|
||||||
|
the list returned by the get_original_scopes. If this check
|
||||||
|
fails, is_within_original_scope is called. The method can be
|
||||||
|
used in situations where returning all valid scopes from the
|
||||||
|
get_original_scopes is not practical.
|
||||||
|
:param request_scopes: A list of scopes that were requested by client
|
||||||
|
:param refresh_token: Unicode refresh_token
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Refresh token grant
|
||||||
|
"""
|
||||||
|
return set(request_scopes)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||||
class HubDBMixin(object):
|
"""Invalidate an authorization code after use.
|
||||||
"""Mixin for connecting to the hub database"""
|
:param client_id: Unicode client identifier
|
||||||
def __init__(self, session_factory):
|
:param code: The authorization code grant (request.code).
|
||||||
self.db = session_factory()
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
class AccessTokenStore(HubDBMixin, oauth2.store.AccessTokenStore):
|
|
||||||
"""OAuth2 AccessTokenStore, storing data in the Hub database"""
|
|
||||||
|
|
||||||
def save_token(self, access_token):
|
|
||||||
"""
|
"""
|
||||||
Stores an access token in the database.
|
app_log.debug("Deleting oauth code %s for %s", code, client_id)
|
||||||
|
orm_code = self.db.query(orm.OAuthCode).filter_by(code=code).first()
|
||||||
:param access_token: An instance of :class:`oauth2.datatype.AccessToken`.
|
if orm_code is not None:
|
||||||
|
self.db.delete(orm_code)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
|
||||||
|
"""Revoke an access or refresh token.
|
||||||
|
:param token: The token string.
|
||||||
|
:param token_type_hint: access_token or refresh_token.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
Method is used by:
|
||||||
|
- Revocation Endpoint
|
||||||
"""
|
"""
|
||||||
|
app_log.debug("Revoking %s %s", token_type_hint, token[:3] + '...')
|
||||||
|
raise NotImplementedError('Subclasses must implement this method.')
|
||||||
|
|
||||||
user = self.db.query(orm.User).filter_by(id=access_token.user_id).first()
|
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||||
if user is None:
|
"""Persist the authorization_code.
|
||||||
raise ValueError("No user for access token: %s" % access_token.user_id)
|
The code should at minimum be stored with:
|
||||||
client = self.db.query(orm.OAuthClient).filter_by(identifier=access_token.client_id).first()
|
- the client_id (client_id)
|
||||||
|
- the redirect URI used (request.redirect_uri)
|
||||||
|
- a resource owner / user (request.user)
|
||||||
|
- the authorized scopes (request.scopes)
|
||||||
|
- the client state, if given (code.get('state'))
|
||||||
|
The 'code' argument is actually a dictionary, containing at least a
|
||||||
|
'code' key with the actual authorization code:
|
||||||
|
{'code': 'sdf345jsdf0934f'}
|
||||||
|
It may also have a 'state' key containing a nonce for the client, if it
|
||||||
|
chose to send one. That value should be saved and used in
|
||||||
|
'validate_code'.
|
||||||
|
It may also have a 'claims' parameter which, when present, will be a dict
|
||||||
|
deserialized from JSON as described at
|
||||||
|
http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
|
||||||
|
This value should be saved in this method and used again in 'validate_code'.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param code: A dict of the authorization code grant and, optionally, state.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
"""
|
||||||
|
app_log.debug("Saving authorization code %s, %s, %s, %s", client_id, code, args, kwargs)
|
||||||
|
orm_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if orm_client is None:
|
||||||
|
raise ValueError("No such client: %s" % client_id)
|
||||||
|
|
||||||
|
orm_code = orm.OAuthCode(
|
||||||
|
client=orm_client,
|
||||||
|
code=code['code'],
|
||||||
|
# oauth has 5 minutes to complete
|
||||||
|
expires_at=int(datetime.utcnow().timestamp() + 300),
|
||||||
|
user=request.user.orm_user,
|
||||||
|
redirect_uri=orm_client.redirect_uri,
|
||||||
|
session_id=request.session_id,
|
||||||
|
)
|
||||||
|
self.db.add(orm_code)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def get_authorization_code_scopes(self, client_id, code, redirect_uri, request):
|
||||||
|
""" Extracts scopes from saved authorization code.
|
||||||
|
The scopes returned by this method is used to route token requests
|
||||||
|
based on scopes passed to Authorization Code requests.
|
||||||
|
With that the token endpoint knows when to include OpenIDConnect
|
||||||
|
id_token in token response only based on authorization code scopes.
|
||||||
|
Only code param should be sufficient to retrieve grant code from
|
||||||
|
any storage you are using, `client_id` and `redirect_uri` can gave a
|
||||||
|
blank value `""` don't forget to check it before using those values
|
||||||
|
in a select query if a database is used.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param code: Unicode authorization code grant
|
||||||
|
:param redirect_uri: Unicode absolute URI
|
||||||
|
:return: A list of scope
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Token Grant Dispatcher
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_token(self, token, request, *args, **kwargs):
|
||||||
|
"""Persist the token with a token type specific method.
|
||||||
|
Currently, only save_bearer_token is supported.
|
||||||
|
"""
|
||||||
|
return self.save_bearer_token(token, request, *args, **kwargs)
|
||||||
|
|
||||||
|
def save_bearer_token(self, token, request, *args, **kwargs):
|
||||||
|
"""Persist the Bearer token.
|
||||||
|
The Bearer token should at minimum be associated with:
|
||||||
|
- a client and it's client_id, if available
|
||||||
|
- a resource owner / user (request.user)
|
||||||
|
- authorized scopes (request.scopes)
|
||||||
|
- an expiration time
|
||||||
|
- a refresh token, if issued
|
||||||
|
- a claims document, if present in request.claims
|
||||||
|
The Bearer token dict may hold a number of items::
|
||||||
|
{
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'access_token': 'askfjh234as9sd8',
|
||||||
|
'expires_in': 3600,
|
||||||
|
'scope': 'string of space separated authorized scopes',
|
||||||
|
'refresh_token': '23sdf876234', # if issued
|
||||||
|
'state': 'given_by_client', # if supplied by client
|
||||||
|
}
|
||||||
|
Note that while "scope" is a string-separated list of authorized scopes,
|
||||||
|
the original list is still available in request.scopes.
|
||||||
|
The token dict is passed as a reference so any changes made to the dictionary
|
||||||
|
will go back to the user. If additional information must return to the client
|
||||||
|
user, and it is only possible to get this information after writing the token
|
||||||
|
to storage, it should be added to the token dictionary. If the token
|
||||||
|
dictionary must be modified but the changes should not go back to the user,
|
||||||
|
a copy of the dictionary must be made before making the changes.
|
||||||
|
Also note that if an Authorization Code grant request included a valid claims
|
||||||
|
parameter (for OpenID Connect) then the request.claims property will contain
|
||||||
|
the claims dict, which should be saved for later use when generating the
|
||||||
|
id_token and/or UserInfo response content.
|
||||||
|
:param token: A Bearer token dict
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: The default redirect URI for the client
|
||||||
|
Method is used by all core grant types issuing Bearer tokens:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
|
- Resource Owner Password Credentials Grant (might not associate a client)
|
||||||
|
- Client Credentials grant
|
||||||
|
"""
|
||||||
|
app_log.debug("Saving bearer token %s", token)
|
||||||
|
if request.user is None:
|
||||||
|
raise ValueError("No user for access token: %s" % request.user)
|
||||||
|
client = self.db.query(orm.OAuthClient).filter_by(identifier=request.client.client_id).first()
|
||||||
orm_access_token = orm.OAuthAccessToken(
|
orm_access_token = orm.OAuthAccessToken(
|
||||||
client=client,
|
client=client,
|
||||||
grant_type=access_token.grant_type,
|
grant_type=orm.GrantType.authorization_code,
|
||||||
expires_at=access_token.expires_at,
|
expires_at=datetime.utcnow().timestamp() + token['expires_in'],
|
||||||
refresh_token=access_token.refresh_token,
|
refresh_token=token['refresh_token'],
|
||||||
refresh_expires_at=access_token.refresh_expires_at,
|
# refresh_expires_at=access_token.refresh_expires_at,
|
||||||
token=access_token.token,
|
token=token['access_token'],
|
||||||
session_id=access_token.data['session_id'],
|
session_id=request.session_id,
|
||||||
user=user,
|
user=request.user,
|
||||||
)
|
)
|
||||||
self.db.add(orm_access_token)
|
self.db.add(orm_access_token)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
return client.redirect_uri
|
||||||
|
|
||||||
|
def validate_bearer_token(self, token, scopes, request):
|
||||||
class AuthCodeStore(HubDBMixin, oauth2.store.AuthCodeStore):
|
"""Ensure the Bearer token is valid and authorized access to scopes.
|
||||||
|
:param token: A string of random characters.
|
||||||
|
:param scopes: A list of scopes associated with the protected resource.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
A key to OAuth 2 security and restricting impact of leaked tokens is
|
||||||
|
the short expiration time of tokens, *always ensure the token has not
|
||||||
|
expired!*.
|
||||||
|
Two different approaches to scope validation:
|
||||||
|
1) all(scopes). The token must be authorized access to all scopes
|
||||||
|
associated with the resource. For example, the
|
||||||
|
token has access to ``read-only`` and ``images``,
|
||||||
|
thus the client can view images but not upload new.
|
||||||
|
Allows for fine grained access control through
|
||||||
|
combining various scopes.
|
||||||
|
2) any(scopes). The token must be authorized access to one of the
|
||||||
|
scopes associated with the resource. For example,
|
||||||
|
token has access to ``read-only-images``.
|
||||||
|
Allows for fine grained, although arguably less
|
||||||
|
convenient, access control.
|
||||||
|
A powerful way to use scopes would mimic UNIX ACLs and see a scope
|
||||||
|
as a group with certain privileges. For a restful API these might
|
||||||
|
map to HTTP verbs instead of read, write and execute.
|
||||||
|
Note, the request.user attribute can be set to the resource owner
|
||||||
|
associated with this token. Similarly the request.client and
|
||||||
|
request.scopes attribute can be set to associated client object
|
||||||
|
and authorized scopes. If you then use a decorator such as the
|
||||||
|
one provided for django these attributes will be made available
|
||||||
|
in all protected views as keyword arguments.
|
||||||
|
:param token: Unicode Bearer token
|
||||||
|
:param scopes: List of scopes (defined by you)
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is indirectly used by all core Bearer token issuing grant types:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
|
- Resource Owner Password Credentials Grant
|
||||||
|
- Client Credentials Grant
|
||||||
"""
|
"""
|
||||||
OAuth2 AuthCodeStore, storing data in the Hub database
|
raise NotImplementedError('Subclasses must implement this method.')
|
||||||
"""
|
|
||||||
def fetch_by_code(self, code):
|
|
||||||
"""
|
|
||||||
Returns an AuthorizationCode fetched from a storage.
|
|
||||||
|
|
||||||
:param code: The authorization code.
|
def validate_client_id(self, client_id, request, *args, **kwargs):
|
||||||
:return: An instance of :class:`oauth2.datatype.AuthorizationCode`.
|
"""Ensure client_id belong to a valid and active client.
|
||||||
:raises: :class:`oauth2.error.AuthCodeNotFound` if no data could be retrieved for
|
Note, while not strictly necessary it can often be very convenient
|
||||||
given code.
|
to set request.client to the client object associated with the
|
||||||
|
given client_id.
|
||||||
|
:param request: oauthlib.common.Request
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
|
"""
|
||||||
|
app_log.debug("Validating client id %s", client_id)
|
||||||
|
orm_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if orm_client is None:
|
||||||
|
return False
|
||||||
|
request.client = orm_client
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||||
|
"""Verify that the authorization_code is valid and assigned to the given
|
||||||
|
client.
|
||||||
|
Before returning true, set the following based on the information stored
|
||||||
|
with the code in 'save_authorization_code':
|
||||||
|
- request.user
|
||||||
|
- request.state (if given)
|
||||||
|
- request.scopes
|
||||||
|
- request.claims (if given)
|
||||||
|
OBS! The request.user attribute should be set to the resource owner
|
||||||
|
associated with this authorization code. Similarly request.scopes
|
||||||
|
must also be set.
|
||||||
|
The request.claims property, if it was given, should assigned a dict.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param code: Unicode authorization code
|
||||||
|
:param client: Client object set by you, see authenticate_client.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
"""
|
"""
|
||||||
orm_code = (
|
orm_code = (
|
||||||
self.db
|
self.db
|
||||||
@@ -109,68 +417,95 @@ class AuthCodeStore(HubDBMixin, oauth2.store.AuthCodeStore):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if orm_code is None:
|
if orm_code is None:
|
||||||
raise AuthCodeNotFound()
|
app_log.debug("No such code: %s", code)
|
||||||
else:
|
return False
|
||||||
return AuthorizationCode(
|
if orm_code.client_id != client_id:
|
||||||
client_id=orm_code.client_id,
|
app_log.debug(
|
||||||
code=code,
|
"OAuth code client id mismatch: %s != %s",
|
||||||
expires_at=orm_code.expires_at,
|
client_id, orm_code.client_id,
|
||||||
redirect_uri=orm_code.redirect_uri,
|
|
||||||
scopes=[],
|
|
||||||
user_id=orm_code.user_id,
|
|
||||||
data={'session_id': orm_code.session_id},
|
|
||||||
)
|
)
|
||||||
|
return False
|
||||||
|
request.user = orm_code.user
|
||||||
|
request.session_id = orm_code.session_id
|
||||||
|
# TODO: record state on oauth codes
|
||||||
|
# TODO: specify scopes
|
||||||
|
request.scopes = ['identify']
|
||||||
|
return True
|
||||||
|
|
||||||
def save_code(self, authorization_code):
|
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
|
||||||
|
"""Ensure client is authorized to use the grant_type requested.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param grant_type: Unicode grant type, i.e. authorization_code, password.
|
||||||
|
:param client: Client object set by you, see authenticate_client.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Resource Owner Password Credentials Grant
|
||||||
|
- Client Credentials Grant
|
||||||
|
- Refresh Token Grant
|
||||||
"""
|
"""
|
||||||
Stores the data belonging to an authorization code token.
|
return True
|
||||||
|
|
||||||
:param authorization_code: An instance of
|
def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
|
||||||
:class:`oauth2.datatype.AuthorizationCode`.
|
"""Ensure client is authorized to redirect to the redirect_uri requested.
|
||||||
|
All clients should register the absolute URIs of all URIs they intend
|
||||||
|
to redirect to. The registration is outside of the scope of oauthlib.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param redirect_uri: Unicode absolute URI
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
"""
|
"""
|
||||||
orm_client = (
|
return True
|
||||||
self.db
|
raise NotImplementedError('Subclasses must implement this method.')
|
||||||
.query(orm.OAuthClient)
|
|
||||||
.filter_by(identifier=authorization_code.client_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if orm_client is None:
|
|
||||||
raise ValueError("No such client: %s" % authorization_code.client_id)
|
|
||||||
|
|
||||||
orm_user = (
|
def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
|
||||||
self.db
|
"""Ensure the Bearer token is valid and authorized access to scopes.
|
||||||
.query(orm.User)
|
OBS! The request.user attribute should be set to the resource owner
|
||||||
.filter_by(id=authorization_code.user_id)
|
associated with this refresh token.
|
||||||
.first()
|
:param refresh_token: Unicode refresh token
|
||||||
)
|
:param client: Client object set by you, see authenticate_client.
|
||||||
if orm_user is None:
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
raise ValueError("No such user: %s" % authorization_code.user_id)
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
orm_code = orm.OAuthCode(
|
- Authorization Code Grant (indirectly by issuing refresh tokens)
|
||||||
client=orm_client,
|
- Resource Owner Password Credentials Grant (also indirectly)
|
||||||
code=authorization_code.code,
|
- Refresh Token Grant
|
||||||
expires_at=authorization_code.expires_at,
|
|
||||||
user=orm_user,
|
|
||||||
redirect_uri=authorization_code.redirect_uri,
|
|
||||||
session_id=authorization_code.data.get('session_id', ''),
|
|
||||||
)
|
|
||||||
self.db.add(orm_code)
|
|
||||||
self.db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def delete_code(self, code):
|
|
||||||
"""
|
"""
|
||||||
Deletes an authorization code after its use per section 4.1.2.
|
return False
|
||||||
|
raise NotImplementedError('Subclasses must implement this method.')
|
||||||
|
|
||||||
http://tools.ietf.org/html/rfc6749#section-4.1.2
|
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
|
||||||
|
"""Ensure client is authorized to use the response_type requested.
|
||||||
:param code: The authorization code.
|
:param client_id: Unicode client identifier
|
||||||
|
:param response_type: Unicode response type, i.e. code, token.
|
||||||
|
:param client: Client object set by you, see authenticate_client.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
"""
|
"""
|
||||||
orm_code = self.db.query(orm.OAuthCode).filter_by(code=code).first()
|
# TODO
|
||||||
if orm_code is not None:
|
return True
|
||||||
self.db.delete(orm_code)
|
|
||||||
self.db.commit()
|
|
||||||
|
|
||||||
|
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
||||||
|
"""Ensure the client is authorized access to requested scopes.
|
||||||
|
:param client_id: Unicode client identifier
|
||||||
|
:param scopes: List of scopes (defined by you)
|
||||||
|
:param client: Client object set by you, see authenticate_client.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
:rtype: True or False
|
||||||
|
Method is used by all core grant types:
|
||||||
|
- Authorization Code Grant
|
||||||
|
- Implicit Grant
|
||||||
|
- Resource Owner Password Credentials Grant
|
||||||
|
- Client Credentials Grant
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
class HashComparable:
|
class HashComparable:
|
||||||
"""An object for storing hashed tokens
|
"""An object for storing hashed tokens
|
||||||
@@ -194,29 +529,10 @@ class HashComparable:
|
|||||||
return compare_token(self.hashed_token, other)
|
return compare_token(self.hashed_token, other)
|
||||||
|
|
||||||
|
|
||||||
class ClientStore(HubDBMixin, oauth2.store.ClientStore):
|
class JupyterHubOAuthServer(WebApplicationServer):
|
||||||
"""OAuth2 ClientStore, storing data in the Hub database"""
|
def __init__(self, db, validator, *args, **kwargs):
|
||||||
|
self.db = db
|
||||||
def fetch_by_client_id(self, client_id):
|
super().__init__(validator, *args, **kwargs)
|
||||||
"""Retrieve a client by its identifier.
|
|
||||||
|
|
||||||
:param client_id: Identifier of a client app.
|
|
||||||
:return: An instance of :class:`oauth2.datatype.Client`.
|
|
||||||
:raises: :class:`oauth2.error.ClientNotFoundError` if no data could be retrieved for
|
|
||||||
given client_id.
|
|
||||||
"""
|
|
||||||
orm_client = (
|
|
||||||
self.db
|
|
||||||
.query(orm.OAuthClient)
|
|
||||||
.filter_by(identifier=client_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if orm_client is None:
|
|
||||||
raise ClientNotFoundError()
|
|
||||||
return Client(identifier=client_id,
|
|
||||||
redirect_uris=[orm_client.redirect_uri],
|
|
||||||
secret=HashComparable(orm_client.secret),
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_client(self, client_id, client_secret, redirect_uri, description=''):
|
def add_client(self, client_id, client_secret, redirect_uri, description=''):
|
||||||
"""Add a client
|
"""Add a client
|
||||||
@@ -241,22 +557,20 @@ class ClientStore(HubDBMixin, oauth2.store.ClientStore):
|
|||||||
self.db.add(orm_client)
|
self.db.add(orm_client)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
|
def fetch_by_client_id(self, client_id):
|
||||||
|
"""Find a client by its id"""
|
||||||
|
return (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_provider(session_factory, url_prefix, login_url):
|
def make_provider(session_factory, url_prefix, login_url):
|
||||||
"""Make an OAuth provider"""
|
"""Make an OAuth provider"""
|
||||||
token_store = AccessTokenStore(session_factory)
|
db = session_factory()
|
||||||
code_store = AuthCodeStore(session_factory)
|
validator = JupyterHubRequestValidator(db)
|
||||||
client_store = ClientStore(session_factory)
|
server = JupyterHubOAuthServer(db, validator)
|
||||||
|
return server
|
||||||
provider = Provider(
|
|
||||||
access_token_store=token_store,
|
|
||||||
auth_code_store=code_store,
|
|
||||||
client_store=client_store,
|
|
||||||
token_generator=UUID4(),
|
|
||||||
)
|
|
||||||
provider.token_path = url_path_join(url_prefix, 'token')
|
|
||||||
provider.authorize_path = url_path_join(url_prefix, 'authorize')
|
|
||||||
site_adapter = JupyterHubSiteAdapter(login_url=login_url)
|
|
||||||
provider.add_grant(AuthorizationCodeGrant(site_adapter=site_adapter))
|
|
||||||
return provider
|
|
||||||
|
|
||||||
|
@@ -513,6 +513,7 @@ class OAuthCode(Base):
|
|||||||
expires_at = Column(Integer)
|
expires_at = Column(Integer)
|
||||||
redirect_uri = Column(Unicode(1023))
|
redirect_uri = Column(Unicode(1023))
|
||||||
session_id = Column(Unicode(255))
|
session_id = Column(Unicode(255))
|
||||||
|
# state = Column(Unicode(1023))
|
||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||||
|
|
||||||
|
|
||||||
@@ -524,6 +525,10 @@ class OAuthClient(Base):
|
|||||||
secret = Column(Unicode(255))
|
secret = Column(Unicode(255))
|
||||||
redirect_uri = Column(Unicode(1023))
|
redirect_uri = Column(Unicode(1023))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_id(self):
|
||||||
|
return self.identifier
|
||||||
|
|
||||||
access_tokens = relationship(
|
access_tokens = relationship(
|
||||||
OAuthAccessToken,
|
OAuthAccessToken,
|
||||||
backref='client',
|
backref='client',
|
||||||
|
@@ -6,7 +6,6 @@ from datetime import datetime, timedelta
|
|||||||
from urllib.parse import quote, urlparse
|
from urllib.parse import quote, urlparse
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from oauth2.error import ClientNotFoundError
|
|
||||||
from sqlalchemy import inspect
|
from sqlalchemy import inspect
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
@@ -372,14 +371,11 @@ class User:
|
|||||||
client_id = spawner.oauth_client_id
|
client_id = spawner.oauth_client_id
|
||||||
oauth_provider = self.settings.get('oauth_provider')
|
oauth_provider = self.settings.get('oauth_provider')
|
||||||
if oauth_provider:
|
if oauth_provider:
|
||||||
client_store = oauth_provider.client_authenticator.client_store
|
oauth_client = oauth_provider.fetch_by_client_id(client_id)
|
||||||
try:
|
|
||||||
oauth_client = client_store.fetch_by_client_id(client_id)
|
|
||||||
except ClientNotFoundError:
|
|
||||||
oauth_client = None
|
|
||||||
# create a new OAuth client + secret on every launch
|
# create a new OAuth client + secret on every launch
|
||||||
# containers that resume will be updated below
|
# containers that resume will be updated below
|
||||||
client_store.add_client(client_id, api_token,
|
oauth_provider.add_client(
|
||||||
|
client_id, api_token,
|
||||||
url_path_join(self.url, server_name, 'oauth_callback'),
|
url_path_join(self.url, server_name, 'oauth_callback'),
|
||||||
description="Server at %s" % (url_path_join(self.base_url, server_name) + '/'),
|
description="Server at %s" % (url_path_join(self.base_url, server_name) + '/'),
|
||||||
)
|
)
|
||||||
@@ -456,8 +452,8 @@ class User:
|
|||||||
)
|
)
|
||||||
# update OAuth client secret with updated API token
|
# update OAuth client secret with updated API token
|
||||||
if oauth_provider:
|
if oauth_provider:
|
||||||
client_store = oauth_provider.client_authenticator.client_store
|
oauth_provider.add_client(
|
||||||
client_store.add_client(client_id, spawner.api_token,
|
client_id, spawner.api_token,
|
||||||
url_path_join(self.url, server_name, 'oauth_callback'),
|
url_path_join(self.url, server_name, 'oauth_callback'),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
@@ -4,7 +4,7 @@ traitlets>=4.3.2
|
|||||||
tornado>=5.0
|
tornado>=5.0
|
||||||
jinja2
|
jinja2
|
||||||
pamela
|
pamela
|
||||||
python-oauth2>=1.0
|
oauthlib>=2.0
|
||||||
python-dateutil
|
python-dateutil
|
||||||
SQLAlchemy>=1.1
|
SQLAlchemy>=1.1
|
||||||
requests
|
requests
|
||||||
|
Reference in New Issue
Block a user